92
113

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GoでCRUDアプリをMysql、GORM、Echo、Clean Architectureで作る

Last updated at Posted at 2020-11-02

#はじめに

本記事では Clean Architecture で、
DBは Mysql 、フレームワークは Echo 、ORMapperには GORM を用いて
CR(U)D機能を持つAPI 作っていきます。

作るもの

Create、Read、(Update)、Delete機能を持つAPI
Updateだけ未実装ですので、ぜひご自身で追加してみてください!

対象読者

Goの環境構築まで終えて、簡単なAPIを作ってみたい人

使用技術

技術 種類
DB Mysql
ORM GORM
Framework Echo

目次

Clean Architectureとは

Clean Architectureというと、以下の図が大変有名です。

clean.jpeg

Clean Architectureの目的は関心の分離で、
これを達成するために意識すべきことが各レイヤーの依存性です。

関心の分離によりコードの可読性が向上したり、変化に強い設計になります。
この辺のメリットやClean Architectureの詳細に関しては参考記事に載っていますのでそちら参照ください。

上記の図では、円の外側から内側に向かって矢印が向けられていますが、これが依存の向きで、
外側から内側への依存は可能ですが、内側から外側は不可能です。

言い方を変えると、内側で宣言したモノを外側から呼ぶことはできますが、外側で宣言したモノを内側から呼ぶことはできないという話です。

この記事では依存の向きに注意しながら コードを紹介していきます。

各機能に関して

各機能のエンドポイントは以下の通りです

POST: /users
GET: /users
DELETE: /users/:id

ディレクトリ構成

echoSample
│
│── src
│    ├── domain 
│    │    └── user.go
│    ├── infrastructure
│    │    ├── router.go
│    │    └── sqlHandler.go
│    ├── interfaces
│    │    ├── api
│    │        ├── context.go
│    │        └── user_controller.go
│    │    └── database
│    │        ├── sql_handler.go
│    │        └── user_repository.go
│    ├── usecase
│    │    ├── user_interactor.go
│    │    └── user_repository.go
│    ├── server.go
│        
└── docker-compose.yml 

Clean Arhictectureの レイヤーと今回のディレクトリ構成は
以下のように照らし合わせることができます。

ディレクトリ名 レイヤー
domain Entities
usecase Use Cases
interface Controllers Presenters
infrastructure External Interfaces

domain

domain層ではEntityの定義をします。
中心にあるので、どの層からでも呼び出せます。
entitys.png

src/domain/user.go
package domain

type User struct {
	ID   int    `json:"id" gorm:"primary_key"`
	Name string `json:"name"`
}

今回はカラムにIDとNameを持つ User を作り、idをプライマリキーに設定します。

json:"id" gorm:"primary_key" について

json:"id"ではjsonマッピングをしていて
gorm:"primary_key"ではgormでモデルにタグづけしています。
もちろん、primary_key の他にも not null unique default などsqlでテーブル定義する要領と同じようにできます。

参考
GORM Declearing Model

infrastructure

最も外側にある Infrastructure
アプリケーションが外部と関わる部分を書きます、今回の例ではDBとの接続と、Router をここで定義しています。
framework.png

一番外側なので、どの層も意識することなく呼び出せます。

src/infrastucture/sqlhandler.go
package infrastructure

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"

	"echoSample/src/interfaces/database"
)

type SqlHandler struct {
	db *gorm.DB
}

func NewSqlHandler() database.SqlHandler {
	dsn := "root:password@tcp(127.0.0.1:3306)/go_sample?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err.Error)
	}
	sqlHandler := new(SqlHandler)
	sqlHandler.db = db
	return sqlHandler
}

func (handler *SqlHandler) Create(obj interface{}) {
	handler.db.Create(obj)
}

func (handler *SqlHandler) FindAll(obj interface{}) {
	handler.db.Find(obj)
}

func (handler *SqlHandler) DeleteById(obj interface{}, id string) {
	handler.db.Delete(obj, id)
}

DB接続周りは 公式ドキュメント が参考になりました。

gorm.io

続いて、ルーティングです。
今回はWebフレームワークのEchoを使っています。こちらも 公式ドキュメント を参考にしました。APIのMethodやパスの定義をしています。
echo.labstack

src/infrastructure/router.go
package infrastructure

import (
	controllers "echoSample/src/interfaces/api"
	"net/http"

	"github.com/labstack/echo"
)

func Init() {
	// Echo instance
	e := echo.New()
	userController := controllers.NewUserController(NewSqlHandler())

	e.GET("/users", func(c echo.Context) error {
		users := userController.GetUser() 
		c.Bind(&users) 
		return c.JSON(http.StatusOK, users)
	})

	e.POST("/users", func(c echo.Context) error {
		userController.Create(c)
		return c.String(http.StatusOK, "created")
	})

	e.DELETE("/users/:id", func(c echo.Context) error {
		id := c.Param("id")
		userController.Delete(id)
		return c.String(http.StatusOK, "deleted")
	})

	// Start server
	e.Logger.Fatal(e.Start(":1323"))
}


interfaces

Controllers Presenters の層です。

ここからは、依存性を意識する 必要があります。

framework-controller.png

interface層からdomain層usecase層の呼び出しに関しては問題ありませんが

infrastructure層の呼び出しはできないので、直接呼び出すのではなく、interfaceを定義します。(ややこしいですが、inrastrucure層で定義しているsqlHandlerのinterfaceです)

src/interfaces/api/user_controller.go
package controllers

import (
	"echoSample/src/domain"
	"echoSample/src/interfaces/database"
	"echoSample/src/usecase"

	"github.com/labstack/echo"
)

type UserController struct {
	Interactor usecase.UserInteractor
}

func NewUserController(sqlHandler database.SqlHandler) *UserController {
	return &UserController{
		Interactor: usecase.UserInteractor{
			UserRepository: &database.UserRepository{
				SqlHandler: sqlHandler,
			},
		},
	}
}

func (controller *UserController) Create(c echo.Context) {
	u := domain.User{}
	c.Bind(&u)
	controller.Interactor.Add(u)
	createdUsers := controller.Interactor.GetInfo()
	c.JSON(201, createdUsers)
	return
}

func (controller *UserController) GetUser() []domain.User {
	res := controller.Interactor.GetInfo()
	return res
}

func (controller *UserController) Delete(id string) {
	controller.Interactor.Delete(id)
}

controllerではusecase層、domain層から呼び出しをしているので問題ありません。

src/interfaces/api/context.go
package controllers

type Context interface {
	Param(string) string
	Bind(interface{}) error
	Status(int)
	JSON(int, interface{})
}

database関連

src/interfaces/database/user_repository.go
package database
package database

import (
	"echoSample/src/domain"
)

type UserRepository struct {
	SqlHandler
}

func (db *UserRepository) Store(u domain.User) {
	db.Create(&u)
}

func (db *UserRepository) Select() []domain.User {
	user := []domain.User{}
	db.FindAll(&user)
	return user
}
func (db *UserRepository) Delete(id string) {
	user := []domain.User{}
	db.DeleteById(&user, id)
}

repositoryでは、sqlHandlerを呼んでいますが、infrastructure層のモノを直接呼んでいるのではなく
同階層に定義したsqlhandlerのinterfaceを通じて呼んでいます。

これを依存性逆転の原則と言うそうです。

src/interfaces/db/sql_handler.go
package database

type SqlHandler interface {
	Create(object interface{})
	FindAll(object interface{})
	DeleteById(object interface{}, id string)
}

これでsql_handlerの処理を呼び出せますね。

usecase

最後になりましたが、usecase層です。

controller-usecase.png

src/usecase/user_interactor.go
package usecase

import "echoSample/src/domain"

type UserInteractor struct {
	UserRepository UserRepository
}

func (interactor *UserInteractor) Add(u domain.User) {
	interactor.UserRepository.Store(u)
}

func (interactor *UserInteractor) GetInfo() []domain.User {
	return interactor.UserRepository.Select()
}

func (interactor *UserInteractor) Delete(id string) {
	interactor.UserRepository.Delete(id)
}

ここでも先ほどと同じように、依存性逆転の原則を適用する必要があります。

なのでuser_repository.goを定義します。

src/usecase/user_repository.go
package usecase

import (
	"echoSample/src/domain"
)

type UserRepository interface {
	Store(domain.User)
	Select() []domain.User
	Delete(id string)
}

これで実装が一通り終了しました。

あとはdocker-compose.ymlでmysqlを立ち上げ、サーバを起動すれば動くはずです。

docker-compose.yml
version: "3.6"
services:
  db:
    image: mysql:5.7
    container_name: go_sample
    volumes:
      # mysqlの設定
      - ./mysql/conf:/etc/mysql/conf.d
      - ./mysql/data:/var/lib/mysql
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - 3306:3306
    environment:
      MYSQL_DATABASE: go_sample
      MYSQL_ROOT_PASSWORD: password
      MYSQL_USER: root
      TZ: "Asia/Tokyo"

src/server.go
package main

import (
	"echoSample/src/domain"
	"echoSample/src/infrastructure"

	"github.com/labstack/echo/v4"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var (
	db  *gorm.DB
	err error
	dsn = "root:password@tcp(127.0.0.1:3306)/go_sample?charset=utf8mb4&parseTime=True&loc=Local"
)

func main() {
	dbinit()
	infrastructure.Init()
	e := echo.New()
	e.Logger.Fatal(e.Start(":1323"))
}

func dbinit() {
	db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
	}
	db.Migrator().CreateTable(domain.User{})
}


以下のコマンドでmysql起動

docker-compose up -d

サーバ起動

go run server.go

動作確認

POST: /users

curl --location --request POST 'localhost:1323/users' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name":"J.Y Park"
}'

curl --location --request POST 'localhost:1323/users' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name":"Eron Mask"
}'


docker-compose

created%                                                                                                                                                                                                               

GET: /users

curl --location --request GET 'localhost:1323/users'
[{"id":1,"name":"J.Y Park"},{"id":2,"name":"Eron Mask"}]

DELETE: /users/:id

curl --location --request DELETE 'localhost:1323/users/1'
deleted

getして確かめる

{"id":2,"name":"Eron Mask"}]

まとめ

Clean Architectureに関する記事を読むだけではなく、実際に簡単なAPIを作って動作確認まで行ってみると、理解が深まりますね。

ただ、CRUDアプリ程度の規模だと、Clean Architectureの魅力が体感できないと言うのが正直な感想でした。

Clean Architectureは、コードの可読性や生産性の向上にもつながるだけではなく、変化に強いという性質も持っているので、今回作ったモノにいろいろ付け加えて良さを実感したいなと思います...!

English Version
https://dev.to/michinoins/building-a-crud-app-with-mysql-gorm-echo-and-clean-architecture-in-go-h6d

参考記事

世界一わかりやすいClean Architecture
クリーンアーキテクチャ(The Clean Architecture翻訳)
Clean ArchitectureでAPI Serverを構築してみる
Golang - EchoとGORMでClean Architecture APIを構築する

92
113
2

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
92
113

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?