【Go】GinでCRUDなREST API作ってみた #2 (Create編)
はじめに
この記事は前回の記事の続きとなっているので、まだ見てない方は以下の記事から見てみてください。
今回の記事から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
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)
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
}
- IItemRepositoryインターフェース
type IItemRepository interface {
Create(newItem models.Item) (*models.Item, error)
}
IItemRepositoryインターフェースで、Createメソッドの抽象化を行います。上位層(今回の場合だとService層)でCreateメソッドを使用する際に役立ちます。
- ItemRepository構造体
type ItemRepository struct {
db *gorm.DB
}
ItemRepository構造体はgorm.DBを使用してデータベースとのやり取りを行うので、こちらに定義しています。*gorm.DBのようにポインタ型で定義するように。
- ItemRepository構造体のオブジェクトを生成するファクトリー関数
func NewItemRepository(db *gorm.DB) IItemRepository {
return &ItemRepository{db: db}
}
こちらのメソッドで生成されたItemRepositoryオブジェクトは、IItemRepositoryインターフェースを満たしているため、Createメソッドが使えます。
こうして他のファイルなどでこのメソッドを実行すると、IItemRepositoryで定義したメソッド群を利用することができるのです。
例)こちらのコードは今回のプロジェクトに関係ありません
itemRepository := repositories.NewItemRepository(db)
itemRepository.Create(itemData)
- Createメソッド(Repository層)
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層
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)
}
- IItemServiceインターフェース
type IItemService interface {
Create(createItemInput dto.CreateItemInput) (*models.Item, error)
}
こちらでもCreateメソッドの抽象化。Controller層でのCreateメソッド利用に役立つ。
2. ItemService構造体
type ItemService struct {
repository repositories.IItemRepository
}
repository属性がIItemRepositoryインターフェース型なので、Repository層のCreateメソッドにアクセスすることができる。
- ItemService構造体のオブジェクトを生成するファクトリー関数
func NewItemService(repository repositories.IItemRepository) IItemService {
return &ItemService{repository: repository}
}
Repository層でも解説したのと同様、ItemServiceオブジェクトを生成するファクトリー関数で、main.goでこちらは使用します。
- Createメソッド(Service層)
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層
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})
}
- IItemControllerインターフェース
type IItemController interface {
Create(ctx *gin.Context)
}
ここで使用されているgin.Contextとはgin.Contextを使うことで、URLに付随したパラメータの取得やPOSTで送信されたデータの取得などを行うことができます。
Engineと並んでginの重要な要素です。
今回はCreateメソッド内でItemに関するリクエスト情報を取得する時にもこちらを使います。
gin.Contextについては あじゃぱーさん の以下の記事を参照させていただきました。
- ItemController構造体
type ItemController struct {
service services.IItemService
}
この構造体のservice属性はService層のCreateメソッドを使用することができるようになります。
- ItemController構造体のオブジェクトを生成するファクトリー関数
func NewItemController(service services.IItemService) IItemController {
return &ItemController{service: service}
}
Repository層、Service層でも話したのと同様、こちらの関数でItemControllerオブジェクトを生成し、IItemControllerインターフェースで定義したメソッド群を利用することができるようになります。
- Createメソッド(Controller層)
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のファイルです。
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でルーティング
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")
}
- envファイルをロード
infra.Initialize()
こちらでenvファイルをロードします。
そうすることで、データベースセットアップ時に.envファイルを読み込めるようになります。
- データベースのセットアップ
db := infra.SetupDB()
こちらでdb.goで定義したSetupDB関数により、変数dbを生成。
変数dbはRepository層のデータベース接続をするために必要であり、.envファイルで定義したデータベース(自分で作成したpostgresqlのデータベース)の環境変数を利用し適切なデータベースにアクセスできるようになります。
- Controller層、Service層、Repository層を繋げる
itemRepository := repositories.NewItemRepository(db)
itemService := services.NewItemService(itemRepository)
itemController := controllers.NewItemController(itemService)
各階層のファクトリー関数(NewItemRepository, NewItemService, NewItemController)を使用して、動作がひと繋ぎになるように定義します。
NewItemRepositoryの引数にはデータベース接続に必要な変数dbを、NewItemServiceの引数にはRepository層と連結するために先ほどの変数itemRepositoryを、NewItemControllerの引数にはService層とRepository層が連結されている変数itemServiceを入れることによって、
全ての層が連結され、連動して動くようになります。
- ルーティング(今回はグルーピングなし)
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コマンドの導入手順です。
- Airのインストール
$ go install github.com/air-verse/air@latest
こちらはAirの公式のgithubページです。
- airの初期化
air init
こちらのコマンドで.air.tomlファイルが作成されている。
パスの通り方で困っている方は以下のサイトが参考になるかもしれません。
ちなみに私は以下のコマンドでパスを通しています。
export PATH=$PATH:$(go env GOPATH)/bin
- airコマンドでホットリロードを行う
air
これで以下のようなログが表示されるはずです。
Postmanで動作確認
リクエストボディはこのようにしました。
{
"name": "商品1",
"price": 1000,
"description": "Postの動作確認"
}
結果は、、、
しっかり動作しています。
終わりに
次にRead(読み込み)をやっていきたいと思います。
参考
今回はYu Shinozakiさんの「Gin入門 Go言語ではじめるサーバーサイド開発」というUdemyの講義を見ながら作成いたしました。とてもわかりやすくておすすめです!
ginフレームワークの公式ドキュメント
Discussion