0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

こちらの記事で作成した食品管理APIを、今度はGitHub Copilot Chatを用いて作成してみたのでその内容をまとめました。

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

GitHub Copilotについて

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

GitHub Copilot Chatについて

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

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

初期環境構築

実行条件

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

air v1.52.0

MySQL 8.0.29

要件定義および機能仕様

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

# 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が出力したプログラム

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を書いてくれなかったので、追加の依頼をしました。

プロンプト
食品追加APIの処理がありません。追加してください。
追加で出力されたソース
    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設定に不足があったので、以下を手動で修正。
(直してもらっても良かったのですが、小規模であれば手修正のほうが早い)

修正箇所
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"

動作確認

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

なお、初期データはこちらの記事と同じものを使用しています。

食品全件取得API

GET localhost:8080/foods

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レスポンス
{
    "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リクエスト
{
    "food_name": "温野菜",
    "ingredients": [
        "じゃがいも",
        "人参",
        "なす"
    ]
}
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レスポンス
{
    "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

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

プロンプト
PUTの処理もPOSTと同じように修正してください。
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リクエスト
{
    "food_name": "温野菜",
    "ingredients": [
        "じゃがいも",
        "人参",
        "ピーマン"
    ]
}
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レスポンス
{
    "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": "ピーマン"
        }
    ]
}

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

食材に関する更新処理の概要は次の通りとなっていた。

  • 食材テーブルに食材を登録する。ただし、登録済みであれば更新する。
  • 食品と食材で中間テーブルを登録する。

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

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

DELETE localhost:8080/food/温野菜

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

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

取得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では使えないようです。修正してください
最終的な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をするようになったかと思います。

以上。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?