はじめに
こちらの記事で検証に用いた食材-食品データベースを利用して、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
ルーティング
package main
import (
"go_project/routes"
)
func main() {
router := routes.GetApiRouter()
router.Run(":8080")
}
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リクエストを受け付けてビジネスロジックに渡す処理をしています。
今回はビジネスロジックからの戻り値をそのまま返却するようにしています。
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(),
})
}
利用可能なステータスコードは以下で確認が可能です。
サービス
ここではデータの取得と整形を行います。
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が不要だったので、このようにしました。
モデル
データベース操作はこちらでします。
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
はビルド時にデータベース接続を行う処理になります。
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言語でメソッドチェーンを途中で改行したい場合は、コロンの後ろで改行します。見落としがちなので要注意です。
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の作成だけですが、次回は食品の追加と更新、削除を実装してみたいと思います。