Go+Gin+GORM開発環境で食品管理APIを作る～データ取得編～

# はじめに

[こちらの記事](https://qiita.com/sedori/items/68608e3b57a6a0467b4f)で検証に用いた食材-食品データベースを利用して、Go開発環境で実際に食品管理APIを作ってみようと思います。

※今回は食品取得APIのみです。

# 検証環境

```shell
Golang v1.22.3
Gin v1.10.0
GORM v1.25.10

air v1.52.0

MySQL 8.0.29
```

開発環境の参考情報はこちら

https://qiita.com/sedori/items/840e39a0cbf9d5bff006

:::note info
Goモジュールの初期化は `go mod init go_project` としています。
※もし異なるモジュール名にした場合、以下のソースのgo_projectの部分を変更した値に書き換えてください。
:::


# ディレクトリ構成

最終的には次のようになりました。
 ~~(名前の突っ込みは受け付けません）~~

```shell
.
├── .air.toml
├── Dockerfile
├── controllers
│   └── cooking_controller.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
├── models
│   ├── cooking_model.go
│   └── database.go
├── routes
│   └── api_route.go
├── services
│   └── cooking_service.go
└── tmp
```


# 食品取得APIの作成

`登録済みの食品をすべて取得するAPI`と、`指定した食品の詳細を取得するAPI`を実装します。

### データベース情報

DB設定

| DB名 | ユーザー名 | パスワード |
| --- | --- | --- |
| local_db | root | （指定なし） |


DLL

```sql
-- 食品テーブル
CREATE TABLE `foods` (
  `id` int NOT NULL AUTO_INCREMENT,
  `food_name` varchar(45) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `id_UNIQUE` (`id`),
  UNIQUE KEY `food_name_UNIQUE` (`food_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

-- 食材テーブル
CREATE TABLE `ingredients` (
  `id` int NOT NULL AUTO_INCREMENT,
  `ingredient_name` varchar(45) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `id_UNIQUE` (`id`),
  UNIQUE KEY `ingredient_name_UNIQUE` (`ingredient_name`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

-- 食材-食品 中間テーブル
CREATE TABLE `ingredients_foods` (
  `ingredient_id` int NOT NULL,
  `food_id` int NOT NULL,
  PRIMARY KEY (`ingredient_id`,`food_id`),
  KEY `ifff_idx` (`food_id`),
  CONSTRAINT `ifff` FOREIGN KEY (`food_id`) REFERENCES `foods` (`id`),
  CONSTRAINT `ifif` FOREIGN KEY (`ingredient_id`) REFERENCES `ingredients` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
```


### ルーティング

```golang:main.go
package main

import (
    "go_project/routes"
)

func main() {
    router := routes.GetApiRouter()
    router.Run(":8080")
}
```

```golang:routes/api_route.go
package routes

import (
    "github.com/gin-gonic/gin"
    "go_project/controllers"
)

func GetApiRouter() *gin.Engine {
    r := gin.Default()

    // グループ化
    v1 := r.Group("/v1")
    {
        food := v1.Group("/food")
        {
            // 食品全件取得API
            food.GET("/", controllers.GetFoods)

            // 食品取得API
            food.GET("/:foodname", controllers.GetOneFood)
        }
    }

    return r
}
```

今回、複数のAPIを作成するにあたってルーティングをグループ化しています。

エンドポイントは次のとおりです。

| API | エンドポイント | 備考 |
| --- | --- | --- |
| 全件取得 | /v1/food/ | - |
| １件取得 | /v1/food/:foodname | :foodnameの部分に取得したい食品名を指定します。<br>(`ex. /v1/food/サラダ`) |

https://gin-gonic.com/ja/docs/examples/grouping-routes/

### コントローラ

JSONリクエストを受け付けてビジネスロジックに渡す処理をしています。
今回はビジネスロジックからの戻り値をそのまま返却するようにしています。

```golang:controller/cooking_controller.go
package controllers

import (
    "github.com/gin-gonic/gin"
    "net/http"
    "go_project/services"
)

// 食品取得
func GetOneFood(c *gin.Context) {
    food_name := c.Param("foodname")

    // データ取得
    data, err := services.GetOneFood(food_name, true);
    if err != nil {
        MakeErrorJson(c, err)
        return
    }

    // 今回は特に整形の必要はないためそのまま出力
    c.JSON(http.StatusOK, gin.H{
        "data" : data,
    })
}

// 食品全件取得
func GetFoods(c *gin.Context) {

    // データ取得
    datas, err := services.GetFoods();
    if err != nil {
        MakeErrorJson(c, err)
        return
    }

    // 整形の必要はないためそのまま出力
    c.JSON(http.StatusOK, gin.H{
        "datas" : datas,
    })
}

// エラー時に返却するJSON
func MakeErrorJson(c *gin.Context, err error) {
    c.JSON(http.StatusBadRequest, gin.H{
        "error" : err.Error(),
    })
}
```

利用可能なステータスコードは以下で確認が可能です。

https://go.dev/src/net/http/status.go

### サービス

ここではデータの取得と整形を行います。

```golang:services/cooking_service.go
package services

import (
    "gorm.io/gorm"
    "errors"
    "go_project/models"
    "fmt"
)

type Food struct {
    FoodName string
    IngredientList []string
}

// 食品全件取得
func GetFoods() ([]string, error) {
    // 全件取得
    datas, err := models.GetAllTheFood()
    if err != nil {
        return nil, fmt.Errorf("予期せぬエラーが発生しました。")
    }
    var foods []string
    for _, data := range datas {
        foods = append(foods, data.FoodName)
    }
    return foods, nil
}

// 食品1件取得
func GetOneFood(food_name string, preload_flag bool) (food Food, err error) {
    // データ取得
    data := models.Food{}
    if data, err = models.GetOneFood(food_name, preload_flag); err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            err = fmt.Errorf("データが見つかりませんでした。")
            return
        }
    }
    // データの整形
    food.FoodName = data.FoodName
    var ingredient_list []string
    for _, ingredient := range data.Ingredients {
        ingredient_list = append(ingredient_list, ingredient.IngredientName)
    }
    food.IngredientList = ingredient_list

    return
}

```

データベースアクセス後のエラーハンドリングとして、レコードが0件の判断を `errors.Is(err, gorm.ErrRecordNotFound)` で行っています。
それ以外のエラーはそのままの値を返却していますが、その場合はエラーメッセージが英文で出力されます（今回はそのまま）。

取得したデータは、読みやすいように整形しています。
できればデータベースから持ってきた値をそのまま返したかったのですが、IDが不要だったので、このようにしました。

### モデル

データベース操作はこちらでします。

```golang:models/database.go
package models

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var Db *gorm.DB

func init() {
    Db = Connect()
}

func Connect() *gorm.DB {
    //DB接続
    dsn := "root@tcp(mysql:3306)/local_db?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

    if err != nil {
        panic("failed to connect database")
    }
    return db
}
```

`database.go`はビルド時にデータベース接続を行う処理になります。


```golang:models/cooking_model.go
package models

type Ingredient struct {
    Id int
    IngredientName   string
    Foods []Food `gorm:"many2many:ingredients_foods;"`
}

type Food struct {
    Id int
    FoodName string
    Ingredients []Ingredient `gorm:"many2many:ingredients_foods;"`
}

type IngredientsFoods struct {
    IngredientId int `gorm:"primaryKey"`
    FoodId int `gorm:"primaryKey"`
}

// 食品全件取得
func GetAllTheFood() (foods []Food, err error) {
    err = Db.Debug().
        // Preload("Ingredients").
        Find(&foods).Error
    return
}

// 食品1件取得
func GetOneFood(food_name string, preload_flag bool) (food Food, err error) {
    queryDB := Db.Debug()
    if preload_flag {
        queryDB = queryDB.Preload("Ingredients")
    }
    err = queryDB.Where("foods.food_name = ?", food_name).First(&food).Error

    return
}
```

食材(ingredients)テーブルと食品(foods)テーブルは、中間テーブル(ingredients_foods)によって多対多の構成となっています。

関連レコードはGORMの`Preload`を利用して取得します。

食品1件取得の処理では関係レコードの取得を行っていますが、これはフラグによって要否を判断するようにしています。

:::note warn
Go言語で**メソッドチェーンを途中で改行したい場合は、コロンの後ろで改行**します。見落としがちなので要注意です。

```shell:NGパターン!
err := Db.Debug()
    .Preload("Foods")
    .Find(&ingredients).Error
```
:::

## 動作確認

実装が完了したら、以下のようなデータを登録して動作確認してみましょう。

食材テーブル

| id | ingredient_name |
| ---- | ---- |
| 1 | キャベツ |
| 2 | 人参 |

食品テーブル

| id | food_name |
| ---- | ---- |
| 1 | サラダ |
| 2 | お好み焼き |
| 3 | 回鍋肉 |
| 4 | 野菜炒め |
| 5 | ポトフ |


中間テーブル

| ingredient_id | food_id |
| ---- | ---- |
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 1 | 4 |
| 1 | 5 |
| 2 | 1 |


#### 全件取得のリクエスト

`GET http://localhost:8080/v1/food/`

```json:レスポンス
{
    "datas": [
        "お好み焼き",
        "サラダ",
        "ポトフ",
        "回鍋肉",
        "野菜炒め"
    ]
}
```

#### １件取得のリクエスト

`GET http://localhost:8080/v1/food/サラダ`
もしくは
`GET http://localhost:8080/v1/food/%E3%82%B5%E3%83%A9%E3%83%80` 

```json:レスポンス
{
    "data": {
        "FoodName": "サラダ",
        "IngredientList": [
            "キャベツ",
            "人参"
        ]
    }
}
```



:::note info
foodnameに誤った値を指定した場合は次のようになります。

```json
{
    "error": "データが見つかりませんでした。"
}
```
:::

# まとめ

いかがだったでしょうか。

今回は食品から食材を取得するAPIの作成だけですが、次回は食品の追加と更新、削除を実装してみたいと思います。