LoginSignup
3
0

More than 1 year has passed since last update.

はじめに

メリークリスマス!
クリーンアーキテクチャというものを参考にしつつ、簡単な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テーブルが空であることを確認します

l_9007702_170_6dc288d243da5280df3c7f8da1a3a2ed.png

POST(create)

次にレコードを1件作成してみます

l_9007702_171_aca9fd2ab13edd6715708fccdc822270.png

再びGETメソッドでリクエストを送ると、作成したレコードを取得できていることが確認できました

l_9007702_172_921e39f7a201fb8337dbc21f6544d81c.png

PUT(update)

次は先ほど作成したレコードのWeightカラムとHeightカラムの値を変更してみます

l_9007702_174_73dd9d8aad500e6cbb8fce3215cf0510.png

GETでリクエストを送ると、変更後のレコードを取得できていることが確認できました

l_9007702_175_15f521369d82b5185d191b303cc89e13.png

DELETE(delete)

最後にレコードを削除できることを確認します

l_9007702_176_0fe99aa37ebf5438c0aa47a246e54691.png

こちらも問題なく確認できました

l_9007702_177_06c132f80e0378f19cee6603259c78a3.png

この時点でDBが以下の状態であればOKです
今回は物理削除ではなく論理削除のため、レコードが削除されずにdeleted_atの値に削除日時が入る形になっています

l_9007702_179_e169cf341275ea62aac7ec7f70d29f92.png

大まかな処理の流れ

  • 始めに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に反映されないということがありました
db/db.go
var DB *gorm.DB

DB, err = gorm.Open(mysql.Open(dbConn), &gorm.Config{})

メソッドの実装とメソッドの使用を分けたい

インターフェースや型の理解でかなり詰まってしまいましたが、最終的には次のような方針で実装しました

やりたいこと

controller/post_controller.go
// serviceに依存せずにGetModelを呼び出したい
func (controller *PostController) Index(c *gin.Context) {
	p, err := controller.GetModel()

}

やりたくないこと

controller/post_controller.go
// serviceに依存してGetModelを呼び出してしまっている
func Index(c *gin.Context) {
	p, err := service.GetModel()

}

方法

以下のようなインターフェースを定義します

service/post.go
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を呼び出すことができます

server/server.go

    s := service.PostService{}
    c := controller.NewPostController(s)
    r.GET("", c.Index)

そのためにはいくつか準備があるので、順を追っていきます

まずPostService構造体にインタフェースを実装します

service/post.go
type PostService struct {
	PostServiceInterface
}

次にPostService構造体からCRUDを実行できるよう、
CRUDメソッドにレシーバ(s *PostService)を定義します

service/post.go
func (s *PostService) GetModel() ([]models.Post, error) {
	var p []models.Post

次にこのPostService構造体をcontrollerで呼び出したいので、以下のPostController構造体を定義します

controller/post_controller.go
type PostController struct {
	service.PostService
}

そして以下のNewPostControllerを利用することで、
controllerからserviceで定義したCRUDをインターフェース経由で呼び出すことができます

controller/post_controller.go
func NewPostController(s service.PostService) (*PostController) {
 return &PostController{
	PostService: s,
 }
}

各ファイルについて

main.go

main.go
package main

import (
	"sample_go/db"
	"sample_go/server"
)

func main() {

	db.Init()
	server.Init()
	db.Close()
}

db/db.go

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

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

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

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

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のテストを作成していきます。

参考

3
0
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
3
0