#はじめに
本記事では 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 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の定義をします。
中心にあるので、どの層からでも呼び出せます。
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でテーブル定義する要領と同じようにできます。
infrastructure
最も外側にある Infrastructure
アプリケーションが外部と関わる部分を書きます、今回の例ではDBとの接続と、Router をここで定義しています。
一番外側なので、どの層も意識することなく呼び出せます。
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接続周りは 公式ドキュメント
が参考になりました。
続いて、ルーティングです。
今回はWebフレームワークのEchoを使っています。こちらも 公式ドキュメント
を参考にしました。APIのMethodやパスの定義をしています。
echo.labstack
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 の層です。
ここからは、依存性を意識する 必要があります。
interface層からdomain層、usecase層の呼び出しに関しては問題ありませんが
infrastructure層の呼び出しはできないので、直接呼び出すのではなく、interfaceを定義します。(ややこしいですが、inrastrucure層で定義しているsqlHandlerのinterfaceです)
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層から呼び出しをしているので問題ありません。
package controllers
type Context interface {
Param(string) string
Bind(interface{}) error
Status(int)
JSON(int, interface{})
}
database関連
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を通じて呼んでいます。
これを依存性逆転の原則と言うそうです。
package database
type SqlHandler interface {
Create(object interface{})
FindAll(object interface{})
DeleteById(object interface{}, id string)
}
これでsql_handlerの処理を呼び出せますね。
usecase
最後になりましたが、usecase層です。
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を定義します。
package usecase
import (
"echoSample/src/domain"
)
type UserRepository interface {
Store(domain.User)
Select() []domain.User
Delete(id string)
}
これで実装が一通り終了しました。
あとはdocker-compose.ymlでmysqlを立ち上げ、サーバを起動すれば動くはずです。
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"
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を構築する