はじめに
メリークリスマス!
クリーンアーキテクチャというものを参考にしつつ、簡単なAPIのCRUDを作成しました。
自分の理解を整理するためと今後も引き続いてAPIを作成していくことを踏まえ、簡単にまとめてみました。
開発環境の構築は以前に出しているのでそちらを参考にしてください。
ディレクトリ構成
go/src
┣ controller
┃ ┗ post_controller.go
┣ db
┃ ┗ db.go
┣ models
┃ ┗ post.go
┣ server
┃ ┗ server.go
┣ service
┃ ┗ post.go
┣ go.mod
┣ go.sum
┣ main.go
PostmanでAPIにリクエストを送る
GET(read)
最初はPostテーブルが空であることを確認します
POST(create)
次にレコードを1件作成してみます
再びGETメソッドでリクエストを送ると、作成したレコードを取得できていることが確認できました
PUT(update)
次は先ほど作成したレコードのWeightカラムとHeightカラムの値を変更してみます
GETでリクエストを送ると、変更後のレコードを取得できていることが確認できました
DELETE(delete)
最後にレコードを削除できることを確認します
こちらも問題なく確認できました
この時点でDBが以下の状態であればOKです
今回は物理削除ではなく論理削除のため、レコードが削除されずにdeleted_atの値に削除日時が入る形になっています
大まかな処理の流れ
- 始めにdbとserverのセットアップを行う
- db処理を行うCRUDの実行にはgorm構造体が必要なのでグローバル変数として定義する
- route設定を行う
- 指定パスにアクセスした際に、serviceで定義したCRUDの"実行"をcontrollerから呼び出す
- controllerとserviceでテストを分けることができる(controllerは通信のテスト、serviceはdb処理のテスト)
詰まったポイント
dbの参照について
- 以下のようにdb接続情報をgorm構造体のDBとして受取ることでCRUDを呼び出せるのですが、
受け取りをDB :=
のようにポインタにしてしまうと、グローバル変数のDBと別になってしまいます。
そのため、外部でdb.DBを呼び出しても参照しているdbが異なるのでCRUDの実行結果が本来のdbに反映されないということがありました
var DB *gorm.DB
略
DB, err = gorm.Open(mysql.Open(dbConn), &gorm.Config{})
メソッドの実装とメソッドの使用を分けたい
インターフェースや型の理解でかなり詰まってしまいましたが、最終的には次のような方針で実装しました
やりたいこと
// serviceに依存せずにGetModelを呼び出したい
func (controller *PostController) Index(c *gin.Context) {
p, err := controller.GetModel()
略
}
やりたくないこと
// serviceに依存してGetModelを呼び出してしまっている
func Index(c *gin.Context) {
p, err := service.GetModel()
略
}
方法
以下のようなインターフェースを定義します
type PostServiceInterface interface {
GetModel() (models.Post, error);
CreateModel(map[string]int) (models.Post, error);
UpdateModel(map[string]int) (models.Post, error);
DeleteModel(string) (error);
}
このインターフェースを引数として受け取りcontroller型を返すNewPostControllerを作成することで、
serviceに依存せずにcontrollerからCRUDを呼び出すことができます
略
s := service.PostService{}
c := controller.NewPostController(s)
r.GET("", c.Index)
そのためにはいくつか準備があるので、順を追っていきます
まずPostService構造体にインタフェースを実装します
type PostService struct {
PostServiceInterface
}
次にPostService構造体からCRUDを実行できるよう、
CRUDメソッドにレシーバ(s *PostService)を定義します
func (s *PostService) GetModel() ([]models.Post, error) {
var p []models.Post
略
次にこのPostService構造体をcontrollerで呼び出したいので、以下のPostController構造体を定義します
type PostController struct {
service.PostService
}
そして以下のNewPostControllerを利用することで、
controllerからserviceで定義したCRUDをインターフェース経由で呼び出すことができます
func NewPostController(s service.PostService) (*PostController) {
return &PostController{
PostService: s,
}
}
各ファイルについて
main.go
package main
import (
"sample_go/db"
"sample_go/server"
)
func main() {
db.Init()
server.Init()
db.Close()
}
db/db.go
package db
import (
"fmt"
"sample_go/models"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
var err error
func Init() {
// dbはコンテナ名
// mydevはデータベース名
dbConn := "root:password@tcp(db:3306)/mydev?charset=utf8mb4&parseTime=True&loc=Local"
DB, err = gorm.Open(mysql.Open(dbConn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
DB.AutoMigrate(&models.Post{})
}
func Close() {
db, err := DB.DB()
if err != nil {
fmt.Println(err)
}
defer db.Close()
}
server/server.go
package server
import (
"sample_go/controller"
"sample_go/service"
"github.com/gin-gonic/gin"
)
func Init() {
r := router()
r.Run()
}
func router() *gin.Engine {
r := gin.Default()
s := service.PostService{}
c := controller.NewPostController(s)
r.GET("", c.Index)
r.POST("/post", c.Create)
r.PUT("/:id", c.Update)
r.DELETE("/:id", c.Delete)
return r
}
service/post.go
package service
import (
"log"
"sample_go/db"
"sample_go/models"
)
type PostServiceInterface interface {
GetModel() (models.Post, error);
CreateModel(map[string]int) (models.Post, error);
UpdateModel(map[string]int) (models.Post, error);
DeleteModel(string) (error);
}
type PostService struct {
PostServiceInterface
}
func (s *PostService) GetModel() ([]models.Post, error) {
var p []models.Post
if err := db.DB.Find(&p).Error; err != nil {
return nil, err
}
return p, nil
}
func (s *PostService) CreateModel(postData map[string]int) (models.Post, error) {
p := models.Post{}
p.Weight, _ = postData["weight"]
p.Height, _ = postData["height"]
err := db.DB.Create(&p).Error
return p, err
}
func (s *PostService) UpdateModel(postData map[string]int) (models.Post, error) {
p := models.Post{}
err := db.DB.Model(&p).Where("id = ?", postData["id"]).Updates(models.Post{Weight: postData["weight"], Height: postData["height"]}).Error
if err != nil {
log.Println(err)
}
return p, err
}
func (s *PostService) DeleteModel(id string) (error) {
p := models.Post{}
err := db.DB.Where("id = ?", id).Delete(&p).Error
if err != nil {
log.Println(err)
}
return err
}
models/post.go
package models
import (
"gorm.io/gorm"
)
type Post struct {
gorm.Model
Weight int `json:"weight"`
Height int `json:"height"`
}
controller/post_controller.go
package controller
import (
"fmt"
"sample_go/service"
"strconv"
"github.com/gin-gonic/gin"
)
type PostController struct {
service.PostService
}
func NewPostController(s service.PostService) (*PostController) {
return &PostController{
PostService: s,
}
}
func (controller *PostController) Index(c *gin.Context) {
p, err := controller.GetModel()
if err != nil {
c.AbortWithStatus(400)
fmt.Println(err)
} else {
c.JSON(200, p)
}
}
func (controller *PostController) Create(c *gin.Context) {
weight, _ := strconv.Atoi(c.PostForm("Weight"))
height, _ := strconv.Atoi(c.PostForm("Height"))
postData := map[string]int {
"weight" :weight,
"height" : height,
}
p, err := controller.CreateModel(postData)
if err != nil {
c.AbortWithStatus(400)
fmt.Println(err)
} else {
c.JSON(200, p)
}
}
func (controller *PostController) Update(c *gin.Context) {
id, _ := strconv.Atoi(c.Params.ByName("id"))
weight, _ := strconv.Atoi(c.PostForm("Weight"))
height, _ := strconv.Atoi(c.PostForm("Height"))
postData := map[string]int {
"id" : id,
"weight" :weight,
"height" : height,
}
p, err := controller.UpdateModel(postData)
if err != nil {
c.AbortWithStatus(400)
fmt.Println(err)
} else {
c.JSON(200, p)
}
}
func (controller *PostController) Delete(c *gin.Context) {
id := c.Params.ByName("id")
err := controller.DeleteModel(id)
if err != nil {
c.AbortWithStatus(400)
fmt.Println(err)
} else {
c.JSON(200, "OK")
}
}
おわりに
テストで楽をしたいので初めてアーキテクチャを考えてAPIを作成してみましたが、型やインターフェースなどで詰まってしまいました。
今後はインターフェースの理解を深めたり、リファクタリングやCRUDのテストを作成していきます。
参考