Go+Gin+GORM開発環境で食品管理APIを作る～GitHubCopilotChat編～

# はじめに
[こちらの記事](https://qiita.com/sedori/items/5f3c89e934ff30ad6067)で作成した食品管理APIを、今度はGitHub Copilot Chatを用いて作成してみたのでその内容をまとめました。

「見せてもらおうか GitHubのCopilotの性能とやらを」という気持ちです。

# GitHub Copilotについて
GitHubが提供する、AIを使ってプログラミングを支援するサービスです。
利用には課金が必要で主要言語は英語ですが、最近では日本語対応も進んできているので、以前に比べれば使いやすく、便利になってきています。

https://docs.github.com/ja/copilot

# GitHub Copilot Chatについて
GitHub Copilotと対話して、GitHub.com およびサポートされている IDE 内でコーディング関連の質問をしたり、回答を受け取ったりできるチャットインターフェイスです。
GitHub Copilotに課金していれば追加料金なしで利用することができます。

https://docs.github.com/ja/copilot/github-copilot-chat/copilot-chat-in-github/about-github-copilot-chat-in-githubcom

GitHub CopilotおよびGitHub Copilot Chatに関する詳細な使い方などは本書では割愛します。

# 初期環境構築

https://qiita.com/sedori/items/f3cd7c41b97f9202f842

# 実行条件

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

air v1.52.0

MySQL 8.0.29
```

# 要件定義および機能仕様

次のような感じで作成して、GitHub Copilotに作ってもらいました。

```markdown
# 1. アプリケーションの概要
- 本アプリケーションは、食品名から食材を取得するREST APIです。
- フレームワークには`github.com/gin-gonic/gin@latest`を使用します。
- データベースアクセスには`gorm.io/gorm@latest`を使用します。
- 開発言語はGoです。

# 2. API

- 5つのAPIを作成します。APIの概要は次のとおりです。
  - 食品全件取得API
  - 食品1件取得API
  - 食品追加API
  - 食品更新API
  - 食品削除API

# 3. データベース定義書

- データベース設定
  - ユーザー名は`root`です。
  - パスワードは指定なしです。
  - データベース名は`local_db`です。
  - ホストは`mysql`です。

- 食品テーブルのDLLは次のとおりです。

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


- 食材テーブルのDLLは次のとおりです。

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

- 中間テーブルのDLLは次のとおりです。

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


# 4. 食品全件取得APIの仕様

- HTTPメソッドとエンドポイント

| API名 | HTTPメソッド | エンドポイント |
| ---- | ---- | ---- |
| 食品全件取得API | GET | /food |


- JSONレスポンス

{
    "food_name" : ["食品名"]
}

- 処理概要
  - エンドポイントから食品名を取得する。食品名は必須とする。
  - 食品名をもとに食品テーブルと食材テーブルから食品名と食材名を取得する。
  - JSON形式にして返却する。

# 5. 食品1件取得APIの仕様

- HTTPメソッドとエンドポイント

| API名 | HTTPメソッド | エンドポイント |
| ---- | ---- | ---- |
| 食品1件取得API | GET | /food/食品名 |

- JSONレスポンス

{
    "food_name" : "食品名",
    "ingredients" : [
        "食材名"
    ]
}

- 処理概要
  - エンドポイントから食品名を取得する。食品名は必須とする。
  - 食品名を使って食品テーブルと食材テーブルから食品名と食材名を取得する。
  - 取得できない場合はエラーとする。
  - JSON形式にして返却する。

# 6. 食品追加APIの仕様

- HTTPメソッドとエンドポイント

| API名 | HTTPメソッド | エンドポイント |
| ---- | ---- | ---- |
| 食品追加API | POST | /food |

- JSONリクエスト

{
    "food_name" : "食品名",
    "ingredients" : [
        "食材名"
    ]
}

- レスポンス

{
    "food_name" : "食品名",
    "ingredients" : [
        "食材名"
    ]
}

- 処理概要
  - JSONリクエストから食品名と食材名を取得する。
    - 食品名は必須とする。
    - 食材名は1件以上の指定を必須とする。
  - データベース登録を行う。
    - 食品が既に食品テーブルに存在している場合はエラーとする。
    - 食品テーブルに食品を登録する。
    - 食材テーブルに食材を登録する。ただし、登録済みであれば更新する。
    - 食品と食材で中間テーブルを登録する。
  - 登録結果をJSON形式にして返却する。

# 7. 食品更新APIの仕様

- HTTPメソッドとエンドポイント

| API名 | HTTPメソッド | エンドポイント |
| ---- | ---- | ---- |
| 食品更新API | PUT | /food/食品名 |

- JSONリクエスト

{
    "ingredients" : [
        "食材名"
    ]
}

- レスポンス

{
    "food_name" : "食品名",
    "ingredients" : [
        "食材名"
    ]
}

- 処理概要
  - エンドポイントとJSONリクエストから食品名と食材名を取得する。
    - 食品名が取得できない場合はエラーとする。
  - データベース登録を行う。
    - 食品が存在していない場合はエラーとする。
    - 食品テーブルの食品を更新する。
    - 食材テーブルに食材を登録する。ただし、登録済みであれば更新する。
    - 食品と食材で中間テーブルを登録する。
  - 登録結果をJSON形式にして返却する。


# 8. 食品削除APIの仕様

- HTTPメソッドとエンドポイント

| API名 | HTTPメソッド | エンドポイント |
| ---- | ---- | ---- |
| 食品削除API | DELETE | /food/食品名 |

- 処理概要
  - エンドポイントから食品名を取得する。
    - 食品名が取得できない場合はエラーとする。
  - データベースから削除する。
    - 中間テーブルから削除する。
    - 食品テーブルから削除する。

# 5.エラー制御
- エラー時のJSONレスポンスとは次の通りとする。

{
    "message" : "エラー内容"
}


- エラーパターンとメッセージは次のとおりとする。

| エラーパターン | メッセージ |
| ---- | ---- |
| リクエストパラメータが不正の場合 | リクエストパラメータ不正です。|
| 食品取得結果が0件の場合 | 食品が登録されていません。|
| 食品がすでに存在している場合 | その食品は既に登録されています。|



上記の要件を全て満たす、アプリケーションの完全なソースコードを提示してください。
```

# GitHub Copilotが出力したプログラム

```golang
package main

import (
    "github.com/gin-gonic/gin"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

type Food struct {
    gorm.Model
    FoodName    string `gorm:"type:varchar(45);unique"`
    Ingredients []Ingredient `gorm:"many2many:ingredients_foods;"`
}

type Ingredient struct {
    gorm.Model
    IngredientName string `gorm:"type:varchar(45);unique"`
}

type FoodRequest struct {
    FoodName    string   `json:"food_name"`
    Ingredients []string `json:"ingredients"`
}

func main() {
    dsn := "root:@tcp(127.0.0.1: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")
    }

    db.AutoMigrate(&Food{}, &Ingredient{})

    r := gin.Default()

    r.GET("/foods", func(c *gin.Context) {
        var foods []Food
        if err := db.Preload("Ingredients").Find(&foods).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品の取得に失敗しました。"})
            return
        }
        c.JSON(200, foods)
    })

    r.GET("/food/:foodname", func(c *gin.Context) {
        foodname := c.Param("foodname")
        var food Food
        if err := db.Preload("Ingredients").Where("food_name = ?", foodname).First(&food).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品の取得に失敗しました。"})
            return
        }
        c.JSON(200, food)
    })

    r.PUT("/food/:foodname", func(c *gin.Context) {
        foodname := c.Param("foodname")
        var req FoodRequest
        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(400, gin.H{"message": "リクエストパラメータ不正です。"})
            return
        }

        var food Food
        if err := db.Where("food_name = ?", foodname).First(&food).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品が存在していない場合はエラーとする。"})
            return
        }

        food.Ingredients = []Ingredient{}
        for _, ingredientName := range req.Ingredients {
            var ingredient Ingredient
            if err := db.Where("ingredient_name = ?", ingredientName).FirstOrCreate(&ingredient).Error; err != nil {
                c.JSON(400, gin.H{"message": "食材の登録に失敗しました。"})
                return
            }
            food.Ingredients = append(food.Ingredients, ingredient)
        }

        if err := db.Save(&food).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品の更新に失敗しました。"})
            return
        }

        c.JSON(200, food)
    })

    r.DELETE("/food/:foodname", func(c *gin.Context) {
        foodname := c.Param("foodname")
        var food Food
        if err := db.Where("food_name = ?", foodname).First(&food).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品名が取得できない場合はエラーとする。"})
            return
        }

        if err := db.Select("Ingredients").Delete(&food).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品の削除に失敗しました。"})
            return
        }

        c.JSON(200, gin.H{"message": "削除しました"})
    })

    r.Run()
}
```

`食品追加API`を書いてくれなかったので、追加の依頼をしました。

```shell:プロンプト
食品追加APIの処理がありません。追加してください。
```

```golang:追加で出力されたソース
    r.POST("/food", func(c *gin.Context) {
        var req FoodRequest
        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(400, gin.H{"message": "リクエストパラメータ不正です。"})
            return
        }
    
        var food Food
        food.FoodName = req.FoodName
        for _, ingredientName := range req.Ingredients {
            if ingredientName == "" {
                c.JSON(400, gin.H{"message": "食材名が空です。"})
                return
            }
            var ingredient Ingredient
            if err := db.Where("ingredient_name = ?", ingredientName).First(&ingredient).Error; err != nil {
                ingredient = Ingredient{IngredientName: ingredientName}
                if err := db.Create(&ingredient).Error; err != nil {
                    c.JSON(400, gin.H{"message": "食材の登録に失敗しました。"})
                    return
                }
            }
            food.Ingredients = append(food.Ingredients, ingredient)
        }
    
        if err := db.Create(&food).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品の追加に失敗しました。"})
            return
        }
    
        c.JSON(200, food)
    })
```

これを書き足し、さらにDB設定に不足があったので、以下を手動で修正。
（直してもらっても良かったのですが、小規模であれば手修正のほうが早い）

```shell:修正箇所
dsn := "root:@tcp(127.0.0.1:3306)/local_db?charset=utf8mb4&parseTime=True&loc=Local"
↓
dsn := "root:@tcp(mysql:3306)/local_db?charset=utf8mb4&parseTime=True&loc=Local"
```

# 動作確認

さて、エラーがないことを確認してから、実際に動かしてみましょう。

なお、初期データは[こちらの記事](https://qiita.com/sedori/items/f3cd7c41b97f9202f842)と同じものを使用しています。

### 食品全件取得API

`GET localhost:8080/foods`

```json:jsonレスポンス
[
    {
        "ID": 1,
        "CreatedAt": "0001-01-01T00:00:00Z",
        "UpdatedAt": "0001-01-01T00:00:00Z",
        "DeletedAt": null,
        "FoodName": "サラダ",
        "Ingredients": [
            {
                "ID": 1,
                "CreatedAt": "0001-01-01T00:00:00Z",
                "UpdatedAt": "0001-01-01T00:00:00Z",
                "DeletedAt": null,
                "IngredientName": "キャベツ"
            },
            {
                "ID": 2,
                "CreatedAt": "0001-01-01T00:00:00Z",
                "UpdatedAt": "0001-01-01T00:00:00Z",
                "DeletedAt": null,
                "IngredientName": "人参"
            }
        ]
    },
    {
        "ID": 2,
        "CreatedAt": "0001-01-01T00:00:00Z",
        "UpdatedAt": "0001-01-01T00:00:00Z",
        "DeletedAt": null,
        "FoodName": "お好み焼き",
        "Ingredients": [
            {
                "ID": 1,
                "CreatedAt": "0001-01-01T00:00:00Z",
                "UpdatedAt": "0001-01-01T00:00:00Z",
                "DeletedAt": null,
                "IngredientName": "キャベツ"
            }
        ]
    },
    {
        "ID": 3,
        "CreatedAt": "0001-01-01T00:00:00Z",
        "UpdatedAt": "0001-01-01T00:00:00Z",
        "DeletedAt": null,
        "FoodName": "回鍋肉",
        "Ingredients": [
            {
                "ID": 1,
                "CreatedAt": "0001-01-01T00:00:00Z",
                "UpdatedAt": "0001-01-01T00:00:00Z",
                "DeletedAt": null,
                "IngredientName": "キャベツ"
            }
        ]
    },
    {
        "ID": 4,
        "CreatedAt": "0001-01-01T00:00:00Z",
        "UpdatedAt": "0001-01-01T00:00:00Z",
        "DeletedAt": null,
        "FoodName": "野菜炒め",
        "Ingredients": [
            {
                "ID": 1,
                "CreatedAt": "0001-01-01T00:00:00Z",
                "UpdatedAt": "0001-01-01T00:00:00Z",
                "DeletedAt": null,
                "IngredientName": "キャベツ"
            }
        ]
    },
    {
        "ID": 5,
        "CreatedAt": "0001-01-01T00:00:00Z",
        "UpdatedAt": "0001-01-01T00:00:00Z",
        "DeletedAt": null,
        "FoodName": "ポトフ",
        "Ingredients": [
            {
                "ID": 1,
                "CreatedAt": "0001-01-01T00:00:00Z",
                "UpdatedAt": "0001-01-01T00:00:00Z",
                "DeletedAt": null,
                "IngredientName": "キャベツ"
            }
        ]
    }
]
```

エンドポイント名やレスポンスが違いますが、おおむね想定通りだと思います(AIに寄り添っていくスタイル)。

### 食品1件取得API

次は食品を1件取得してみましょう。

`GET localhost:8080/food/サラダ`

```json:jsonレスポンス
{
    "ID": 1,
    "CreatedAt": "0001-01-01T00:00:00Z",
    "UpdatedAt": "0001-01-01T00:00:00Z",
    "DeletedAt": null,
    "FoodName": "サラダ",
    "Ingredients": [
        {
            "ID": 1,
            "CreatedAt": "0001-01-01T00:00:00Z",
            "UpdatedAt": "0001-01-01T00:00:00Z",
            "DeletedAt": null,
            "IngredientName": "キャベツ"
        },
        {
            "ID": 2,
            "CreatedAt": "0001-01-01T00:00:00Z",
            "UpdatedAt": "0001-01-01T00:00:00Z",
            "DeletedAt": null,
            "IngredientName": "人参"
        }
    ]
}
```

これもレスポンスはmodelの情報を返してしまっていますが、おおむね正しいですね。

### 食品追加API

いよいよ食品の追加です。どうなるか。

`POST http://localhost:8080/food`

```json:jsonリクエスト
{
    "food_name": "温野菜",
    "ingredients": [
        "じゃがいも",
        "人参",
        "なす"
    ]
}
```

```json:jsonレスポンス
{
    "ID": 6,
    "CreatedAt": "2024-06-03T08:00:57.861Z",
    "UpdatedAt": "2024-06-03T08:00:57.861Z",
    "DeletedAt": null,
    "FoodName": "温野菜",
    "Ingredients": [
        {
            "ID": 3,
            "CreatedAt": "2024-06-03T08:00:57.852Z",
            "UpdatedAt": "2024-06-03T08:00:57.852Z",
            "DeletedAt": null,
            "IngredientName": "じゃがいも"
        },
        {
            "ID": 2,
            "CreatedAt": "2024-06-03T08:00:57.862Z",
            "UpdatedAt": "2024-06-03T08:00:57.862Z",
            "DeletedAt": null,
            "IngredientName": "人参"
        },
        {
            "ID": 4,
            "CreatedAt": "2024-06-03T08:00:57.858Z",
            "UpdatedAt": "2024-06-03T08:00:57.858Z",
            "DeletedAt": null,
            "IngredientName": "なす"
        }
    ]
}
```

どうやらうまくいったように見えます。取得APIでも見てみましょう。

```json:jsonレスポンス
{
    "ID": 6,
    "CreatedAt": "2024-06-03T08:00:57.861Z",
    "UpdatedAt": "2024-06-03T08:00:57.861Z",
    "DeletedAt": null,
    "FoodName": "温野菜",
    "Ingredients": [
        {
            "ID": 2,
            "CreatedAt": "0001-01-01T00:00:00Z",
            "UpdatedAt": "0001-01-01T00:00:00Z",
            "DeletedAt": null,
            "IngredientName": "人参"
        },
        {
            "ID": 3,
            "CreatedAt": "2024-06-03T08:00:57.852Z",
            "UpdatedAt": "2024-06-03T08:00:57.852Z",
            "DeletedAt": null,
            "IngredientName": "じゃがいも"
        },
        {
            "ID": 4,
            "CreatedAt": "2024-06-03T08:00:57.858Z",
            "UpdatedAt": "2024-06-03T08:00:57.858Z",
            "DeletedAt": null,
            "IngredientName": "なす"
        }
    ]
}
```

大丈夫そうですね。一安心です。

### 食品更新API

さて、更新処理を行う前に、ソースを見た感じ、既に実装が誤っていることがわかるので、先に直してもらいましょう。
（今のプログラムだと重複したデータを登録する際にエラーとなってしまう）

```shell:プロンプト
PUTの処理もPOSTと同じように修正してください。
```

```golang:GitHub Copilotからの修正案
r.PUT("/food/:foodname", func(c *gin.Context) {
    foodname := c.Param("foodname")
    var req FoodRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"message": "リクエストパラメータ不正です。"})
        return
    }

    var food Food
    if err := db.Where("food_name = ?", foodname).First(&food).Error; err != nil {
        c.JSON(400, gin.H{"message": "食品が存在していない場合はエラーとする。"})
        return
    }

    food.Ingredients = []Ingredient{}
    for _, ingredientName := range req.Ingredients {
        if ingredientName == "" {
            c.JSON(400, gin.H{"message": "食材名が空です。"})
            return
        }
        var ingredient Ingredient
        if err := db.Where("ingredient_name = ?", ingredientName).First(&ingredient).Error; err != nil {
            ingredient = Ingredient{IngredientName: ingredientName}
            if err := db.Create(&ingredient).Error; err != nil {
                c.JSON(400, gin.H{"message": "食材の登録に失敗しました。"})
                return
            }
        }
        food.Ingredients = append(food.Ingredients, ingredient)
    }

    if err := db.Save(&food).Error; err != nil {
        c.JSON(400, gin.H{"message": "食品の更新に失敗しました。"})
        return
    }

    c.JSON(200, food)
})
```

更新の処理部分(`r.PUT("/food/:foodname", func(c *gin.Context) {～}`)をまるごと置き換えてから、更新APIを実行してみます。

`PUT http://localhost:8080/food/温野菜`

```json:jsonリクエスト
{
    "food_name": "温野菜",
    "ingredients": [
        "じゃがいも",
        "人参",
        "ピーマン"
    ]
}
```

```json:jsonレスポンス
{
    "ID": 6,
    "CreatedAt": "2024-06-03T08:00:57.861Z",
    "UpdatedAt": "2024-06-03T08:04:52.33Z",
    "DeletedAt": null,
    "FoodName": "温野菜",
    "Ingredients": [
        {
            "ID": 3,
            "CreatedAt": "2024-06-03T08:00:57.852Z",
            "UpdatedAt": "2024-06-03T08:00:57.852Z",
            "DeletedAt": null,
            "IngredientName": "じゃがいも"
        },
        {
            "ID": 2,
            "CreatedAt": "2024-06-03T08:04:52.331Z",
            "UpdatedAt": "2024-06-03T08:04:52.331Z",
            "DeletedAt": null,
            "IngredientName": "人参"
        },
        {
            "ID": 5,
            "CreatedAt": "2024-06-03T08:04:52.326Z",
            "UpdatedAt": "2024-06-03T08:04:52.326Z",
            "DeletedAt": null,
            "IngredientName": "ピーマン"
        }
    ]
}
```

同じように取得APIで更新内容を見てみましょう。


```json:jsonレスポンス
{
    "ID": 6,
    "CreatedAt": "2024-06-03T08:00:57.861Z",
    "UpdatedAt": "2024-06-03T08:04:52.33Z",
    "DeletedAt": null,
    "FoodName": "温野菜",
    "Ingredients": [
        {
            "ID": 2,
            "CreatedAt": "0001-01-01T00:00:00Z",
            "UpdatedAt": "0001-01-01T00:00:00Z",
            "DeletedAt": null,
            "IngredientName": "人参"
        },
        {
            "ID": 3,
            "CreatedAt": "2024-06-03T08:00:57.852Z",
            "UpdatedAt": "2024-06-03T08:00:57.852Z",
            "DeletedAt": null,
            "IngredientName": "じゃがいも"
        },
        {
            "ID": 4,
            "CreatedAt": "2024-06-03T08:00:57.858Z",
            "UpdatedAt": "2024-06-03T08:00:57.858Z",
            "DeletedAt": null,
            "IngredientName": "なす"
        },
        {
            "ID": 5,
            "CreatedAt": "2024-06-03T08:04:52.326Z",
            "UpdatedAt": "2024-06-03T08:04:52.326Z",
            "DeletedAt": null,
            "IngredientName": "ピーマン"
        }
    ]
}
```

おや…`なす`が残ってしまっていますね。
想定では`なす`が消えるはずでしたが、これは仕様書のどおりなので、こちらが悪いですね…。

:::note info
食材に関する更新処理の概要は次の通りとなっていた。
 - 食材テーブルに食材を登録する。ただし、登録済みであれば更新する。
 - 食品と食材で中間テーブルを登録する。

**実装ミスの原因**
更新前に紐づいていたデータに対する扱いが設計されていない。
:::

まぁここはいったんおいておいて、最後に削除APIの動作を見てみます。

`DELETE localhost:8080/food/温野菜`

```json:jsonレスポンス
{
    "message": "削除しました"
}
```

特に指示はしていませんが。削除した旨のメッセージを書いてくれたようですね。
本当に削除されたのか、取得APIで見てみましょう。

```json:取得APIのレスポンス
{
    "message": "食品の取得に失敗しました。"
}
```

# まとめ
いかがだったでしょうか？

ところどころおかしな点はあるものの、ざっくり作ってもらう分にはかなり有用な感じがします。

ただ、当たり前の話ですが、ソースコードで誤っている部分は自分自身で修正するか、指示を変えて直してもらうかしないといけないので、いずれにしてもプログラミング知識やコミュニケーション力は必要といったところです。

しかしながらローコードで実装できるという点では大変役に立ちますので、GitHub Copilotにうまく仕事をさせて、効率よく開発を進めていきたいですね。

# おまけ

以下のプロンプトを与えて修正を依頼。

- JSONレスポンスを直す
    1. jsonレスポンスにgorm.Modelは不要です
    2. jsonレスポンスのingredientsはingredient_nameの配列にしてください
    3. r.GET("/food/:foodname", func(c *gin.Context) {とr.POST("/food", func(c *gin.Context) {のjsonレスポンスも同じように直してください
    4. r.GET("/foods", func(c *gin.Context) {のjsonレスポンスはfood_nameの配列にしてください
- 更新処理を直す
    1. r.PUT("/food/:foodname", func(c *gin.Context) {})の処理で、食材をupsertする前に中間テーブルの削除を行ってください。その後upsertして、中間テーブルも作成してください
    2. Clear().Errorは使えないようです。修正してください
    3. Append(&ingredient).Errorはgorm.io/gormでは使えないようです。修正してください

```golang:最終的なmain.go
package main

import (
    "github.com/gin-gonic/gin"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

type Food struct {
    gorm.Model
    FoodName    string `gorm:"type:varchar(45);unique"`
    Ingredients []Ingredient `gorm:"many2many:ingredients_foods;"`
}

type Ingredient struct {
    gorm.Model
    IngredientName string `gorm:"type:varchar(45);unique"`
}

type FoodRequest struct {
    FoodName    string   `json:"food_name"`
    Ingredients []string `json:"ingredients"`
}

type FoodResponse struct {
    FoodName    string   `json:"food_name"`
    Ingredients []string `json:"ingredients"`
}

type IngredientResponse struct {
    IngredientName string `json:"ingredient_name"`
}

func main() {
    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")
    }

    db.AutoMigrate(&Food{}, &Ingredient{})

    r := gin.Default()

    r.GET("/foods", func(c *gin.Context) {
        var foods []Food
        if err := db.Preload("Ingredients").Find(&foods).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品の取得に失敗しました。"})
            return
        }
        // Create response
        var response []string
        for _, food := range foods {
            response = append(response, food.FoodName)
        }

        c.JSON(200, gin.H{"foods": response})
    })

    r.GET("/food/:foodname", func(c *gin.Context) {
        foodname := c.Param("foodname")
        var food Food
        if err := db.Preload("Ingredients").Where("food_name = ?", foodname).First(&food).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品の取得に失敗しました。"})
            return
        }
        // Create response
        var response FoodResponse
        response.FoodName = food.FoodName
        for _, ingredient := range food.Ingredients {
            response.Ingredients = append(response.Ingredients, ingredient.IngredientName)
        }

        c.JSON(200, gin.H{"food": response})
    })

    r.POST("/food", func(c *gin.Context) {
        var req FoodRequest
        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(400, gin.H{"message": "リクエストパラメータ不正です。"})
            return
        }
    
        var food Food
        food.FoodName = req.FoodName
        for _, ingredientName := range req.Ingredients {
            if ingredientName == "" {
                c.JSON(400, gin.H{"message": "食材名が空です。"})
                return
            }
            var ingredient Ingredient
            if err := db.Where("ingredient_name = ?", ingredientName).First(&ingredient).Error; err != nil {
                ingredient = Ingredient{IngredientName: ingredientName}
                if err := db.Create(&ingredient).Error; err != nil {
                    c.JSON(400, gin.H{"message": "食材の登録に失敗しました。"})
                    return
                }
            }
            food.Ingredients = append(food.Ingredients, ingredient)
        }
    
        if err := db.Create(&food).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品の追加に失敗しました。"})
            return
        }
    
        // Create response
        var response FoodResponse
        response.FoodName = food.FoodName
        for _, ingredient := range food.Ingredients {
            response.Ingredients = append(response.Ingredients, ingredient.IngredientName)
        }

        c.JSON(200, gin.H{"food": response})
    })

    r.PUT("/food/:foodname", func(c *gin.Context) {
        foodname := c.Param("foodname")
        var req FoodRequest
        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(400, gin.H{"error": "リクエストパラメータ不正です。"})
            return
        }
    
        var food Food
        if err := db.Where("food_name = ?", foodname).First(&food).Error; err != nil {
            c.JSON(400, gin.H{"error": "食品が存在していない場合はエラーとする。"})
            return
        }
    
        // Delete the existing associations
        if err := db.Model(&food).Association("Ingredients").Clear(); err != nil {
            c.JSON(400, gin.H{"error": "中間テーブルの削除に失敗しました。"})
            return
        }

        // Re-create the associations
        for _, ingredientName := range req.Ingredients {
            if ingredientName == "" {
                c.JSON(400, gin.H{"error": "食材名が空です。"})
                return
            }
            var ingredient Ingredient
            if err := db.Where("ingredient_name = ?", ingredientName).First(&ingredient).Error; err != nil {
                ingredient = Ingredient{IngredientName: ingredientName}
                if err := db.Create(&ingredient).Error; err != nil {
                    c.JSON(400, gin.H{"error": "食材の登録に失敗しました。"})
                    return
                }
            }
            if err := db.Model(&food).Association("Ingredients").Append(&ingredient); err != nil {
                c.JSON(400, gin.H{"error": "食材の追加に失敗しました。"})
                return
            }
        }

        if err := db.Save(&food).Error; err != nil {
            c.JSON(400, gin.H{"error": "食品の更新に失敗しました。"})
            return
        }
    
        // Create response
        var response FoodResponse
        response.FoodName = food.FoodName
        for _, ingredient := range food.Ingredients {
            response.Ingredients = append(response.Ingredients, ingredient.IngredientName)
        }

        c.JSON(200, gin.H{"food": response})
    })

    r.DELETE("/food/:foodname", func(c *gin.Context) {
        foodname := c.Param("foodname")
        var food Food
        if err := db.Where("food_name = ?", foodname).First(&food).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品名が取得できない場合はエラーとする。"})
            return
        }

        if err := db.Select("Ingredients").Delete(&food).Error; err != nil {
            c.JSON(400, gin.H{"message": "食品の削除に失敗しました。"})
            return
        }

        c.JSON(200, gin.H{"message": "削除しました"})
    })

    r.Run()
}
```

エラーハンドリングなど細かい部分は動作を見ていませんが、おおむね要求通りのIn/Outをするようになったかと思います。

以上。