0
1

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を作る~データ登録・更新・削除編~

Posted at

はじめに

こちらの記事に引き続き、Go開発環境で食品管理APIを作っていきます。

今回は食品の追加、更新、削除APIを作ります。

検証環境および開発環境

前回記事を参照

食品の登録、更新、削除APIの作成

今回はREST APIとして作成を進めていきます。

なお、処理は前回作成した取得APIのソースに付け足していきますので、重複する部分は割愛します。

RESTについて

REpresentational State Transferの略です。

RESTの4原則

  • Stateless (状態を保持せず、各リクエストやレスポンスが完結していること)
  • Uniform Interface (情報の操作(取得、作成、更新、削除)を統一メソッドを使用して行うこと)
  • Connectability (情報に他のリソースへのリンクを含めることができること)
  • Addressability (提供する情報がURIを通して表現できること)

ルーティング

main.goは変更なしです。

routes/api_route.go
...
        food := v1.Group("/food")
        {
            // 食品全件取得API
            food.GET("/", controllers.GetFoods)

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

            // 食品登録API
            food.POST("/", controllers.RegisterForFood)
            // 食品更新API
            food.PUT("/:foodname", controllers.UpdateFood)
            // 食品削除API
            food.DELETE("/:foodname", controllers.DeleteFood)

        }
...

新たに追加したエンドポイントは次のとおりです。

API メソッド エンドポイント
登録 POST /v1/food/
更新 PUT /v1/food/:foodname
削除 DELETE /v1/food/:foodname

コントローラ

食品の登録、更新、削除のコントローラを追加していきます。

controller/cooking_controller.go

...

// 食品登録APIのリクエスト
type RegisterForFoodRequest struct {
    Food string `json:"food_name" binding:"required"`
    Ingredients []string `json:"ingredients_name" binding:"required,min=1,dive,gte=1"`
}

// 食品更新APIのリクエスト
type UpdateFoodRequest struct {
    Ingredients []string `json:"ingredients_name"`
}

...


// 食品登録
func RegisterForFood(c *gin.Context) {

    // リクエストの解析
    var json RegisterForFoodRequest
    if err := c.ShouldBindJSON(&json); err != nil {
        MakeErrorJson(c, err)
        return
    }

    // データ登録
    data,err := services.RegisterForFood(json.Food, json.Ingredients);
    if err != nil {
        MakeErrorJson(c, err)
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "data" : data,
    })
}

// 食品更新
func UpdateFood(c *gin.Context) {
    food_name := c.Param("foodname")

    // リクエストの解析
    var json UpdateFoodRequest
    if err := c.ShouldBindJSON(&json); err != nil {
        MakeErrorJson(c, err)
        return
    }

    // データ更新
    data,err := services.UpdateFood(food_name, json.Ingredients);
    if err != nil {
        MakeErrorJson(c, err)
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "data" : data,
    })
}

// 食品削除
func DeleteFood(c *gin.Context) {
    food_name := c.Param("foodname")

    // データ削除
    err := services.DeleteFood(food_name);
    if err != nil {
        MakeErrorJson(c, err)
        return
    }
    c.JSON(http.StatusOK, gin.H{})
}

登録と更新のJSONリクエストをバインド、バリデーションするための構造体を追加します。

食材は食品に対して1つ以上存在し、名前は1文字以上であることを検証しています。
配列の要素を検証する場合はdiveの後ろに書きます

更新と削除の処理はエンドポイントから食品名を取得するため、c.Param(":キー名")を使用しています。

サービス

登録、更新、削除のためのビジネスロジックを追加します。

services/cooking_service.go

...

// 食品登録
func RegisterForFood(food_name string, ingredients []string) (food Food, err error) {

    // 存在確認
    data := models.Food{}
    data, err = models.GetOneFood(food_name, false);
    if data.Id != 0 {
        err = fmt.Errorf("その食品は既に登録されています。")
        return
    }
    // レコード0件以外はエラーとする
    if !errors.Is(err, gorm.ErrRecordNotFound) {
        return
    }

    // 食品、食材の登録
    if err = models.RegisterForFood(food_name, ingredients); err != nil {
        return
    }
    // 登録結果を再取得
    food, err = GetOneFood(food_name, true)
    return food, err
}

...

登録処理で行っているのは次のとおりです。

  • models.GetOneFood(food_name, false);で登録済チェックを行う。第二引数をfalseとすることでPreloadを省略しています
  • エラーコードを判別し、特定のエラー(レコードなし)以外はエラーとする ※レコードなしは正しい挙動のため無視する
  • 食品と食材を登録する
  • 登録した情報でPreloadも含めて再取得し、整形した結果を返す
services/cooking_service.go

...

// 食品更新
func UpdateFood(food_name string, ingredients []string) (food Food, err error) {

    data := models.Food{}
    data, err = models.GetOneFood(food_name, false);
    if err != nil {
        // 未登録の場合はエラー制御する
        if errors.Is(err, gorm.ErrRecordNotFound) {
            err = fmt.Errorf("その食品は登録されていません。")
            return
        }
        return
    }
    // 食品、食材の更新
    if err = models.UpdateFood(data, ingredients); err != nil {
        return
    }
    // 更新結果を再取得
    food, err = GetOneFood(food_name, true)

    return food, err
}

...

更新処理で行っているのは次のとおりです。

  • models.GetOneFood(food_name, false);で登録済チェックを行う。こちらは更新処理なのでエラーがあれば処理を終了する
  • 食品と食材を更新する
  • 更新した情報でPreloadも含めて再取得し、整形した結果を返す
services/cooking_service.go

...

// 食品削除
func DeleteFood(food_name string) (err error) {

    food, err := models.GetOneFood(food_name, false)
    if err != nil {
        return
    }

    // 食品と中間テーブルの削除
    if err = models.DeleteFood(food); err != nil {
        return
    }
    return
}

...

削除処理で行っているのは次のとおりです。

  • models.GetOneFood(food_name, false);で登録済チェックを行う。DeleteFood内でも実施可能ですが、他と処理を揃えました(DBアクセスは増える)
  • 食品と中間テーブルを削除する。食材は削除しない(食材は他の食品で使用している可能性があるため)。

モデル

models/database.goは変更なしです。

models/cooking_model.go

...

import (
    "gorm.io/gorm"
    "gorm.io/gorm/clause"
)

...

データの登録、更新、削除ではトランザクション(データ一貫性の確保)を行うため、importを追加しています。

models/cooking_model.go

...

// 食品登録(食材と中間テーブルも登録)
func RegisterForFood(food_name string, ingredients []string) (err error) {

    food := Food{FoodName: food_name}

    // トランザクション処理
    err = Db.Transaction(func(tx *gorm.DB) error {
        // 食品登録
        if err = tx.Debug().Create(&food).Error; err != nil {
            return err
        }
        // 食材登録
        db_ingredients := []Ingredient{}

        if db_ingredients, err = UpsertIngredients(tx, ingredients); err != nil {
            return err
        }

        // 中間テーブルの登録
        tx.Debug().Model(&food).Association("Ingredients").Append(db_ingredients)

        return nil
    })
    return
}

...

// 食材の登録(既に存在していれば上書き)
func UpsertIngredients(tx *gorm.DB, ingredients []string) (db_ingredients []Ingredient, err error) {
    for _, ingredient := range ingredients {
        // 食材が未登録であれば追加
        db_ingredient := Ingredient{}
        tx.Debug().Where("ingredient_name = ?", ingredient).First(&db_ingredient)

        db_ingredient.IngredientName = ingredient

        // 食材テーブルにUpsert
        if err = tx.Debug().Save(&db_ingredient).Error; err != nil {
            return
        }
        db_ingredients = append(db_ingredients, db_ingredient)
    }
    return
}

...

登録の処理はトランザクション(Db.Transaction(func(tx *gorm.DB) error { /*処理*/ }))内で行います。

また、トランザクション内でのDB操作は、Dbではなくtxを用いて行います。

データ登録はCreate(&food)で行います。引数に渡している変数(構造体)は、テーブルと同じ構成をしたデータを指定します。

食材は食品に対して複数存在しているため、UpsertIngredientsで繰り返し処理を行っています。
Saveを使うことで、渡したデータが存在していれば上書き(Update)を、存在していなければ登録(Insert)をするようにしています。

食品と食材を登録するだけではそれぞれが紐づかないため、中間テーブル(ingredients_foods)の登録も必要です。
これはアソシエーションを用いることで実現しています。

アソシエーションの挙動はわかりづらい点が多いので、ログを出力しながら確認するのが良いと思います。
(もちろんアソシエーションを使わず、中間テーブルにInsertすることも可能です)

models/cooking_model.go
...

// 食品更新(食材と中間テーブルも更新)
func UpdateFood(food Food, ingredients []string) (err error) {
    // トランザクション処理
    err = Db.Transaction(func(tx *gorm.DB) error {
        // // 食品テーブルを更新する場合
        // if err = tx.Debug().Save(&food).Error; err != nil {
        //     return err
        // }

        // 食材登録
        db_ingredients, err := UpsertIngredients(tx, ingredients);
        if err != nil {
            return err
        }
        // 中間テーブルの登録
        tx.Debug().Model(&food).Association("Ingredients").Replace(db_ingredients)
        return nil;
    })
    return
}

...

更新処理も登録処理と同様、トランザクション内で処理を行います。
基本的な流れは登録処理と同じで、Saveを用いて食品情報を更新し、食材情報を更新または登録します。

中間テーブルはアソシエーションのReplaceを用いることで、既存の紐づけを削除し、新たな情報で紐づけを行うことができます。

models/cooking_model.go
...

// 食品削除(中間テーブルも削除)
func DeleteFood(food Food) (err error) {

    Db.Transaction(func(tx *gorm.DB) error {
        // 中間テーブルと食品を削除
        if err := tx.Debug().Select(clause.Associations).Delete(&food).Error; err != nil {
            return err
        }
        return nil
    })
    return
}

...

削除処理でもアソシエーションを利用しています。
この方法ではDeleteで渡した情報を外部キーとしている中間テーブルのレコードも同時に消すことができます。

動作確認

実装が完了したら、動作確認をしてみましょう。

食品登録API

POST http://localhost:8080/v1/food

jsonリクエスト
{
    "food_name" : "カレー(牛肉)",
    "ingredients_name" : [
        "じゃがいも",
        "人参",
        "牛肉",
        "カレールー"
    ]
}
レスポンス
{
    "data": {
        "FoodName": "カレー(牛肉)",
        "IngredientList": [
            "カレールー",
            "じゃがいも",
            "人参",
            "牛肉"
        ]
    }
}

食品更新API

PUT http://localhost:8080/v1/food/カレー(牛肉)

jsonリクエスト
{
    "ingredients_name" : [
        "じゃがいも",
        "牛肉",
        "カレールー",
        "水"
    ]
}
レスポンス
{
    "data": {
        "FoodName": "カレー(牛肉)",
        "IngredientList": [
            "カレールー",
            "じゃがいも",
            "水",
            "牛肉"
        ]
    }
}

食品削除API

DELETE http://localhost:8080/v1/food/カレー(牛肉)

レスポンス
{}

まとめ

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

今回は自己学習もかねてアソシエーションを用いたり、部分的にエラー判定を入れてみたりしましたが、これがもっと複雑で規模も大きいものとなってきたら、こううまくはいかないかもしれません。

シンプルで要件を満たしたプログラムを書くのは難しいですね…兎にも角にも慣れが必要です。

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?