はじめに
こちらの記事で作成した食品管理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
[
{
"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/サラダ
{
"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
{
"food_name": "温野菜",
"ingredients": [
"じゃがいも",
"人参",
"なす"
]
}
{
"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でも見てみましょう。
{
"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と同じように修正してください。
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/温野菜
{
"food_name": "温野菜",
"ingredients": [
"じゃがいも",
"人参",
"ピーマン"
]
}
{
"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で更新内容を見てみましょう。
{
"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/温野菜
{
"message": "削除しました"
}
特に指示はしていませんが。削除した旨のメッセージを書いてくれたようですね。
本当に削除されたのか、取得APIで見てみましょう。
{
"message": "食品の取得に失敗しました。"
}
まとめ
いかがだったでしょうか?
ところどころおかしな点はあるものの、ざっくり作ってもらう分にはかなり有用な感じがします。
ただ、当たり前の話ですが、ソースコードで誤っている部分は自分自身で修正するか、指示を変えて直してもらうかしないといけないので、いずれにしてもプログラミング知識やコミュニケーション力は必要といったところです。
しかしながらローコードで実装できるという点では大変役に立ちますので、GitHub Copilotにうまく仕事をさせて、効率よく開発を進めていきたいですね。
おまけ
以下のプロンプトを与えて修正を依頼。
- JSONレスポンスを直す
- jsonレスポンスにgorm.Modelは不要です
- jsonレスポンスのingredientsはingredient_nameの配列にしてください
- r.GET("/food/:foodname", func(c *gin.Context) {とr.POST("/food", func(c *gin.Context) {のjsonレスポンスも同じように直してください
- r.GET("/foods", func(c *gin.Context) {のjsonレスポンスはfood_nameの配列にしてください
- 更新処理を直す
- r.PUT("/food/:foodname", func(c *gin.Context) {})の処理で、食材をupsertする前に中間テーブルの削除を行ってください。その後upsertして、中間テーブルも作成してください
- Clear().Errorは使えないようです。修正してください
- Append(&ingredient).Errorはgorm.io/gormでは使えないようです。修正してください
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をするようになったかと思います。
以上。