1
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を作る~データ取得編~

Last updated at Posted at 2024-06-03

はじめに

こちらの記事で検証に用いた食材-食品データベースを利用して、Go開発環境で実際に食品管理APIを作ってみようと思います。

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

検証環境

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

air v1.52.0

MySQL 8.0.29

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

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

ディレクトリ構成

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

.
├── .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

-- 食品テーブル
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

ルーティング

main.go
package main

import (
    "go_project/routes"
)

func main() {
    router := routes.GetApiRouter()
    router.Run(":8080")
}
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/ -
1件取得 /v1/food/:foodname :foodnameの部分に取得したい食品名を指定します。
(ex. /v1/food/サラダ)

コントローラ

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

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(),
    })
}

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

サービス

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

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が不要だったので、このようにしました。

モデル

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

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はビルド時にデータベース接続を行う処理になります。

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件取得の処理では関係レコードの取得を行っていますが、これはフラグによって要否を判断するようにしています。

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

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/

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

1件取得のリクエスト

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

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

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

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

まとめ

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

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

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