🦥

【Go】GinでCRUDなREST API作ってみた #2 (Create編)

2024/09/18に公開

はじめに

この記事は前回の記事の続きとなっているので、まだ見てない方は以下の記事から見てみてください。
https://zenn.dev/daino/articles/41524068ce68bf

今回の記事からCRUD操作の実装に入っていきたいと思います。
この記事はCreateを実装していきます。

今回作業するファイルの説明

プロジェクト構成

.
├── controllers
│   └── item_controller.go   <-  今回作業するファイル
├── docker-compose.yaml
├── dto
│   └── item_dto.go          <-  今回作業するファイル
├── go.mod
├── go.sum
├── infra
│   ├── db.go
│   └── initializer.go
├── main.go                  <-  今回作業するファイル
├── migrations
│   └── migration.go
├── models
│   └── item.go
├── repositories
│   └── item_repository.go   <-  今回作業するファイル
└── services
    └── item_service.go      <-  今回作業するファイル

処理の流れ順にファイル紹介

main.go

main.go
package main

import (
	"<プロジェクト名>/controllers"
	"<プロジェクト名>/infra"
	"<プロジェクト名>/repositories"

	"github.com/gin-gonic/gin"
)

func main() {
	infra.Initialize()
	db := infra.SetupDB()
	
	...

	r := gin.Default()
    // Postリクエスト(Createメソッド)のルーティング
	r.POST("/items", itemController.Create)
    r.Run("localhost:8080")
}

上記のコードの「 r.POST("/items", itemController.Create) 」でPOSTリクエストを受け付け、Controller層(item_controller.go)にリクエストボディでItem情報(Name, Price, Description)を送ります。

item_controller.go

item_controller.goではまず、Item情報を受け取ります。item_dto.goで定義したbiding(必須項目、最大文字数、最小文字数などのチェック)もここで行います。
それが終えたら、service層(item_service.go)へデータを転送します。

item_service.go

item_service.goではビジネスロジックを管理します。
controller層(item_controller.go)から受け取ったItem情報からデータベースに保存しやすいオブジェクトに新しく生成し、そのデータをRepository層(item_repository.go)へ転送します。

item_repository.go

item_repository.goでは、service層(item_service.go)から受け取ったItemオブジェクトをデータベース(今回のプロジェクトではpostgreSQLのデータベースサーバー)にGormのCreateメソッドでservice層(item_service.go)から受け取ったItemオブジェクトをデータベースに挿入します。
そしてデータベース挿入が成功したら、新しく作成されたItemオブジェクトをresponseします。

作っていく

Repository層(item_repository.go)

item_repository.go
package repositories

import (
	"<プロジェクト名>/models"

	"gorm.io/gorm"
)


type IItemRepository interface {
	Create(newItem models.Item) (*models.Item, error)
}

type ItemRepository struct {
	db *gorm.DB
}

func NewItemRepository(db *gorm.DB) IItemRepository {
	return &ItemRepository{db: db}
}

func (r *ItemRepository) Create(newItem models.Item) (*models.Item, error) {
	result := r.db.Create(&newItem)
	if result.Error != nil {
		return nil, result.Error
	}
	return &newItem, nil
}
  1. IItemRepositoryインターフェース
item_repository.go
type IItemRepository interface {
	Create(newItem models.Item) (*models.Item, error)
}

IItemRepositoryインターフェースで、Createメソッドの抽象化を行います。上位層(今回の場合だとService層)でCreateメソッドを使用する際に役立ちます。

  1. ItemRepository構造体
item_repository.go
type ItemRepository struct {
	db *gorm.DB
}

ItemRepository構造体はgorm.DBを使用してデータベースとのやり取りを行うので、こちらに定義しています。*gorm.DBのようにポインタ型で定義するように。

  1. ItemRepository構造体のオブジェクトを生成するファクトリー関数
item_repository.go
func NewItemRepository(db *gorm.DB) IItemRepository {
	return &ItemRepository{db: db}
}

こちらのメソッドで生成されたItemRepositoryオブジェクトは、IItemRepositoryインターフェースを満たしているため、Createメソッドが使えます。
こうして他のファイルなどでこのメソッドを実行すると、IItemRepositoryで定義したメソッド群を利用することができるのです。
例)こちらのコードは今回のプロジェクトに関係ありません

example.go
itemRepository := repositories.NewItemRepository(db)
itemRepository.Create(itemData)
  1. Createメソッド(Repository層)
item_repository.go
func (r *ItemRepository) Create(newItem models.Item) (*models.Item, error) {
	result := r.db.Create(&newItem)
	if result.Error != nil {
		return nil, result.Error
	}
	return &newItem, nil
}

Createメソッドの引数であるnewItemはService層から送られてきたItemオブジェクト(今回データベースに保存したいオブジェクト)である。
こちらをr.db.Create(&newItem)でデータベースに保存している。

Service層

item_service.go
package services

import (
	"<プロジェクト名>/dto"
	"<プロジェクト名>/models"
	"<プロジェクト名>/repositories"
)

type IItemService interface {
	Create(createItemInput dto.CreateItemInput) (*models.Item, error)
}

type ItemService struct {
	repository repositories.IItemRepository
}

func NewItemService(repository repositories.IItemRepository) IItemService {
	return &ItemService{repository: repository}
}

func (s *ItemService) Create(createItemInput dto.CreateItemInput) (*models.Item, error) {
	newItem := models.Item{
		Name:        createItemInput.Name,
		Price:       createItemInput.Price,
		Description: createItemInput.Description,
		SoldOut:     false,
	}
	return s.repository.Create(newItem)
}

  1. IItemServiceインターフェース
item_service.go
type IItemService interface {
	Create(createItemInput dto.CreateItemInput) (*models.Item, error)
}

こちらでもCreateメソッドの抽象化。Controller層でのCreateメソッド利用に役立つ。
2. ItemService構造体

item_service.go
type ItemService struct {
	repository repositories.IItemRepository
}

repository属性がIItemRepositoryインターフェース型なので、Repository層のCreateメソッドにアクセスすることができる。

  1. ItemService構造体のオブジェクトを生成するファクトリー関数
item_service.go
func NewItemService(repository repositories.IItemRepository) IItemService {
	return &ItemService{repository: repository}
}

Repository層でも解説したのと同様、ItemServiceオブジェクトを生成するファクトリー関数で、main.goでこちらは使用します。

  1. Createメソッド(Service層)
item_service.go
func (s *ItemService) Create(createItemInput dto.CreateItemInput) (*models.Item, error) {
	newItem := models.Item{
		Name:        createItemInput.Name,
		Price:       createItemInput.Price,
		Description: createItemInput.Description,
		SoldOut:     false,
	}
	return s.repository.Create(newItem)
}

こちらでは、上位層(Controller層)から送られてきたItem情報(createItemInput)を引数とし、newItemオブジェクトを作成。そしてそのデータをs.repository.Create(newItem)でRepository層のCreateメソッドに転送。

Controller層

item_controller.go
package controllers

import (
	"<プロジェクト名>/dto"
	"<プロジェクト名>/services"
	"net/http"

	"github.com/gin-gonic/gin"
)

type IItemController interface {
	Create(ctx *gin.Context)
}

type ItemController struct {
	service services.IItemService
}

func NewItemController(service services.IItemService) IItemController {
	return &ItemController{service: service}
}

func (c *ItemController) Create(ctx *gin.Context) {
	var input dto.CreateItemInput
	if err := ctx.ShouldBindJSON(&input); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	newItem, err := c.service.Create(input)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	ctx.JSON(http.StatusCreated, gin.H{"data": newItem})
}
  1. IItemControllerインターフェース
item_controller.go
type IItemController interface {
	Create(ctx *gin.Context)
}

ここで使用されているgin.Contextとはgin.Contextを使うことで、URLに付随したパラメータの取得やPOSTで送信されたデータの取得などを行うことができます。
Engineと並んでginの重要な要素です。
今回はCreateメソッド内でItemに関するリクエスト情報を取得する時にもこちらを使います。
gin.Contextについては あじゃぱーさん の以下の記事を参照させていただきました。
https://zenn.dev/ajapa/articles/6471ac0c612fda

  1. ItemController構造体
item_controller.go
type ItemController struct {
	service services.IItemService
}

この構造体のservice属性はService層のCreateメソッドを使用することができるようになります。

  1. ItemController構造体のオブジェクトを生成するファクトリー関数
item_controller.go
func NewItemController(service services.IItemService) IItemController {
	return &ItemController{service: service}
}

Repository層、Service層でも話したのと同様、こちらの関数でItemControllerオブジェクトを生成し、IItemControllerインターフェースで定義したメソッド群を利用することができるようになります。

  1. Createメソッド(Controller層)
item_controller.go
func (c *ItemController) Create(ctx *gin.Context) {
	var input dto.CreateItemInput
	if err := ctx.ShouldBindJSON(&input); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	newItem, err := c.service.Create(input)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	ctx.JSON(http.StatusCreated, gin.H{"data": newItem})
}

ctx.ShouldBindJSON(&input)でinputのポインタを渡し、item_dto.goで定義したバインディングの検証とinput変数にユーザーの入力したリクエスト情報を格納します。そして、newItem, err := c.service.Create(input)でnewItem変数に結果を格納、Service層にinput(Item情報)を送ります。
以下はこちらで使用したitem_dto.goのファイルです。

item_dto.go
package dto

type CreateItemInput struct {
	Name        string `json:"name" biding:"required,min=2"`
	Price       uint   `json:"price" biding:"required,min=1,max=99999"`
	Description string `json:"description"`
}

main.goでルーティング

main.go
package main

import (
	"<プロジェクト名>/controllers"
	"<プロジェクト名>/infra"
	"<プロジェクト名>/repositories"
	"<プロジェクト名>/services"

	"github.com/gin-gonic/gin"
)

func main() {
	infra.Initialize()
	db := infra.SetupDB()

	itemRepository := repositories.NewItemRepository(db)
	itemService := services.NewItemService(itemRepository)
	itemController := controllers.NewItemController(itemService)

	r := gin.Default()
	r.POST("/items", itemController.Create)
    r.Run("localhost:8080")
}

  1. envファイルをロード
main.go
infra.Initialize()

こちらでenvファイルをロードします。
そうすることで、データベースセットアップ時に.envファイルを読み込めるようになります。

  1. データベースのセットアップ
main.go
db := infra.SetupDB()

こちらでdb.goで定義したSetupDB関数により、変数dbを生成。
変数dbはRepository層のデータベース接続をするために必要であり、.envファイルで定義したデータベース(自分で作成したpostgresqlのデータベース)の環境変数を利用し適切なデータベースにアクセスできるようになります。

  1. Controller層、Service層、Repository層を繋げる
main.go
itemRepository := repositories.NewItemRepository(db)
itemService := services.NewItemService(itemRepository)
itemController := controllers.NewItemController(itemService)

各階層のファクトリー関数(NewItemRepository, NewItemService, NewItemController)を使用して、動作がひと繋ぎになるように定義します。
NewItemRepositoryの引数にはデータベース接続に必要な変数dbを、NewItemServiceの引数にはRepository層と連結するために先ほどの変数itemRepositoryを、NewItemControllerの引数にはService層とRepository層が連結されている変数itemServiceを入れることによって、
全ての層が連結され、連動して動くようになります。

  1. ルーティング(今回はグルーピングなし)
main.go
r := gin.Default()
r.POST("/items", itemController.Create)
r.Run("localhost:8080")

ここで、r.(リクエストメソッド)の各引数について解説。
第一引数にはエンドポイント、第二引数にはControllerと呼ばれる関数を指定します。
今回は3層連結されたCreateメソッドであるitemController.Createを指定します。

この第二引数であるControllerと呼ばれる関数は、ユーザーのリクエストをハンドルし、レスポンスを返すための関数のことを指します。
今回の場合は、ユーザーがリクエストしたItem情報を取得し、データベースに保存、レスポンスに保存したオブジェクトを返す。

r.Run("localhost:8080")でlocalhost:8080番でリクエストを受け付けています。

実際の動作確認

airコマンドでホットリロード

1〜2はairコマンドの導入手順です。

  1. Airのインストール
sh
$ go install github.com/air-verse/air@latest

こちらはAirの公式のgithubページです。
https://github.com/air-verse/air

  1. airの初期化
sh
air init

こちらのコマンドで.air.tomlファイルが作成されている。

パスの通り方で困っている方は以下のサイトが参考になるかもしれません。
https://maku77.github.io/p/s258beh/#google_vignette

ちなみに私は以下のコマンドでパスを通しています。

sh
export PATH=$PATH:$(go env GOPATH)/bin 
  1. airコマンドでホットリロードを行う
sh
air

これで以下のようなログが表示されるはずです。

Postmanで動作確認

リクエストボディはこのようにしました。

{
    "name": "商品1",
    "price": 1000,
    "description": "Postの動作確認"
}

結果は、、、

しっかり動作しています。

終わりに

次にRead(読み込み)をやっていきたいと思います。

参考

今回はYu Shinozakiさんの「Gin入門 Go言語ではじめるサーバーサイド開発」というUdemyの講義を見ながら作成いたしました。とてもわかりやすくておすすめです!
https://www.udemy.com/course/gin-golang/?couponCode=KEEPLEARNING
ginフレームワークの公式ドキュメント
https://gin-gonic.com/ja/

Discussion