Edited at

Gin と GORM で作るオレオレ構成 API

Gopher道場 Advent Calendar 2018 の 23 日目のエントリです。

昨日は po3rin さんの Go 1.12 beta 1 の標準パッケージの中で気になったマイナーチェンジを Docker で試したので紹介する。 でした!!


TL;DR


  • Gin, Gorm で API を作るチュートリアル

  • そこそこ管理しやすい構成で作る


Go で開発しやすい API を作りたい

みなさん Go 書いてますか?

昨今 Go は CLI に限らず、 Web の API としても活用される事例が多くなってきています。

今年は Go を勉強して色々試している最中なので、この流れに乗って API を作成してみたいと思います。

というわけで今日は Go, Gin, GORM を使って API を作りましょう!

ただ作ると言っても、ネット上には多くの似たような記事があります。しかし、 main.go にコードが全部書いてあるものが多く、実際の開発の構成には遠い気がしました。

いくつかの FW を使って API を作ったりネット上のコードを読んだりして、オレオレ構成の構築してみました。

このチュートリアルではエンドポイントが増えても管理しやすいようなディレクトリ構成を目指しました。

リポジトリは以下になります。

Asuforce/gin-gorm-tutorial


API の構成

今日の記事を書くために、いくつかのボイラープレートコードを参考にしました。

ディレクトリ名は Spring Boot っぽくしています。

.

├── controller
│ └── user_controller.go
├── db
│ └── db.go
├── docker-compose.yml
├── entity
│ └── user.go
├── main.go
├── server
│ └── server.go
└── service
└── user_service.go

この構成のポイントは model 層を entity と service に分離しているところです。

model で db のインスタンスを取得、db の初期化時に autoMigration をするために model の構造体が必要という処理が発生するのですが、双方の package を import するため import cycle not allowed が発生してしまいました。

このエラーを回避するために、 entity の package を作成し、 model の struct だけを分離するようにしています。

ここに関しては、正直これでいいのかあまり自信がありません、というのも service, controller は同一 package 名で entity は entity package という命名規則が不規則だなと感じるからです。

しかし、今回はこれ以上の解決策を考えられなかったので、コメントなどで指摘していただけると嬉しいです。

あとの主要なディレクトリを紹介すると service には model の動作を実装し、 server には gin の設定(ルーティングなど)を実装、controller はアクションを定義し service の呼び出しを行うという構成になります。

では下準備としてファイルを作りましょう。

$ mkdir db entity service controller server

$ touch db/db.go entity/user.go service/user_service.go controller/user_controller.go server/server.go


Gin で Hello world

まずは今回使う Gin の Hello, World からです。

$ go get github.com/gin-gonic/gin


main.go

package main

import "github.com/gin-gonic/gin"

func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.String(200, "Hello, World")
})
r.Run()
}


サーバを起動して、エンドポイントを叩いてみましょう

$ go run main.go

$ curl localhost:8080
Hello, World

メッセージが確認できたら、完了です。


User API の作成

User model を作成し、CRUD 操作ができる API を作成します。


DB の設定の作成

Go の O/RM の GORM を使って DB との接続を行います。

今回は PostgreSQL との接続を行います。他の RDBMS との接続はこちらをご覧ください。

PostgreSQL は Docker で作成しましょう。


docker-compose.yml

version: "3.7"

services:
db:
image: postgres:alpine
environment:
POSTGRES_USER: gorm
POSTGRES_PASSWORD: gorm
POSTGRES_DB: gorm
ports:
- 5432:5432

PostgreSQL を起動しておきましょう。

$ docker-compose up -d

次に gorm と postgres の設定をインストールします。

$ go get github.com/jinzhu/gorm

$ go get github.com/jinzhu/gorm/dialects/postgres

DB の設定はこのようになります。

db のインスタンスは GetDB で取得します。


db/db.go

package db

import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres" // Use PostgreSQL in gorm
)

var (
db *gorm.DB
err error
)

// Init is initialize db from main function
func Init() {
db, err = gorm.Open("postgres", "host=0.0.0.0 port=5432 user=gorm dbname=gorm password=gorm sslmode=disable")
if err != nil {
panic(err)
}
}

// GetDB is called in models
func GetDB() *gorm.DB {
return db
}

// Close is closing db
func Close() {
if err := db.Close(); err != nil {
panic(err)
}
}


作成した db package を main.go に import します。(パスは適宜書き換えてください


main.go

-import "github.com/gin-gonic/gin"

+import (
+ "github.com/gin-gonic/gin"
+
+ "github.com/asuforce/gin-gorm-tutorial/db"
+)

初期化処理を追加します。


main.go

                c.String(200, "Hello, World")

})

+ db.Init()
r.Run()

+ db.Close()


以上で db の設定は完了です。


User model の作成

id, FirstName, LastName を持つ User model を作成します。

entity ディレクトリは model の定義だけを扱い、動作に関しては service ディレクトリで管理します。

struct で model を定義します。


entity/user.go

package entity

// User is user models property
type User struct {
ID uint `json:"id"`
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
}



Migration の設定

gorm には auto migration 機能が用意されているので、それを使います。


db/db.go

 import (

"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres" // Use PostgreSQL in gorm
+
+ "github.com/asuforce/gin-gorm-tutorial/entity"
)

// ...

if err != nil {
panic(err)
}
+
+ autoMigration()
}

// GetDB is called in models
func GetDB() *gorm.DB {
return db
}

// Close is closing db
func Close() {
if err := db.Close(); err != nil {
panic(err)
}
}

+func autoMigration() {
+ db.AutoMigrate(&entity.User{})
+}


entity を追加するたびに autoMigration に追記することで、テーブルの作成を自動で行うことができます。

以下のコマンドでテーブルが作成されているかを確認できます。

$ go run main.go

$ psql -h 0.0.0.0 -U gorm gorm
Password for user gorm:
psql (11.1)
Type "help" for help.

gorm=# show tables;
ERROR: unrecognized configuration parameter "tables"
gorm=# show tables;\d
ERROR: unrecognized configuration parameter "tables"
List of relations
Schema | Name | Type | Owner
--------+--------------+----------+-------
public | users | table | gorm
public | users_id_seq | sequence | gorm


User service の作成

User model の振るまいを service ディレクトリ以下に定義します。


service/user_service.go

package user

import (
"github.com/gin-gonic/gin"

"github.com/asuforce/gin-gorm-tutorial/db"
"github.com/asuforce/gin-gorm-tutorial/entity"
)

// Service procides user's behavior
type Service struct{}

// User is alias of entity.User struct
type User entity.User

// GetAll is get all User
func (s Service) GetAll() ([]User, error) {
db := db.GetDB()
var u []User

if err := db.Find(&u).Error; err != nil {
return nil, err
}

return u, nil
}

// CreateModel is create User model
func (s Service) CreateModel(c *gin.Context) (User, error) {
db := db.GetDB()
var u User

if err := c.BindJSON(&u); err != nil {
return u, err
}

if err := db.Create(&u).Error; err != nil {
return u, err
}

return u, nil
}

// GetByID is get a User
func (s Service) GetByID(id string) (User, error) {
db := db.GetDB()
var u User

if err := db.Where("id = ?", id).First(&u).Error; err != nil {
return u, err
}

return u, nil
}

// UpdateByID is update a User
func (s Service) UpdateByID(id string, c *gin.Context) (User, error) {
db := db.GetDB()
var u User

if err := db.Where("id = ?", id).First(&u).Error; err != nil {
return u, err
}

if err := c.BindJSON(&u); err != nil {
return u, err
}

db.Save(&u)

return u, nil
}

// DeleteByID is delete a User
func (s Service) DeleteByID(id string) error {
db := db.GetDB()
var u User

if err := db.Where("id = ?", id).Delete(&u).Error; err != nil {
return err
}

return nil
}


これで User の動作が実装できました。


Controller に action を実装する

Controller にアクションを追加します。


controller/user_controller.go

package user

import (
"fmt"

"github.com/gin-gonic/gin"

"github.com/asuforce/gin-gorm-tutorial/service"
)

// Controller is user controlller
type Controller struct{}

// Index action: GET /users
func (pc Controller) Index(c *gin.Context) {
var s user.Service
p, err := s.GetAll()

if err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, p)
}
}

// Create action: POST /users
func (pc Controller) Create(c *gin.Context) {
var s user.Service
p, err := s.CreateModel(c)

if err != nil {
c.AbortWithStatus(400)
fmt.Println(err)
} else {
c.JSON(201, p)
}
}

// Show action: GET /users/:id
func (pc Controller) Show(c *gin.Context) {
id := c.Params.ByName("id")
var s user.Service
p, err := s.GetByID(id)

if err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, p)
}
}

// Update action: PUT /users/:id
func (pc Controller) Update(c *gin.Context) {
id := c.Params.ByName("id")
var s user.Service
p, err := s.UpdateByID(id, c)

if err != nil {
c.AbortWithStatus(400)
fmt.Println(err)
} else {
c.JSON(200, p)
}
}

// Delete action: DELETE /users/:id
func (pc Controller) Delete(c *gin.Context) {
id := c.Params.ByName("id")
var s user.Service

if err := s.DeleteByID(id); err != nil {
c.AbortWithStatus(403)
fmt.Println(err)
} else {
c.JSON(204, gin.H{"id #" + id: "deleted"})
}
}



Routing を設定する

server ディレクトリを作成し、ルーティングを設定します。

$ mkdir server

routing の実装はこのようになります。


server/server.go

package server

import (
"github.com/gin-gonic/gin"

"github.com/asuforce/gin-gorm-tutorial/controller"
)

// Init is initialize server
func Init() {
r := router()
r.Run()
}

func router() *gin.Engine {
r := gin.Default()

u := r.Group("/users")
{
ctrl := user.Controller{}
u.GET("", ctrl.Index)
u.GET("/:id", ctrl.Show)
u.POST("", ctrl.Create)
u.PUT("/:id", ctrl.Update)
u.DELETE("/:id", ctrl.Delete)
}

return r
}


最後に main.go を修正します。


main.go

 import (

- "github.com/gin-gonic/gin"
-
"github.com/asuforce/gin-gorm-tutorial/db"
+ "github.com/asuforce/gin-gorm-tutorial/server"
)

func main() {
- r := gin.Default()
- r.GET("/", func(c *gin.Context) {
- c.String(200, "Hello, World")
- })
-
db.Init()
- r.Run()
+ server.Init()
}


error handling や validation など、できてない部分も多々ありますが、以上で API の実装は完了です。


curl で試す

サーバを起動して、以下のような挙動を確認できれば OK です。

$ go run main.go

# Index
$ curl http://localhost:8080/users -X GET
[]%

# Create
$ curl http://localhost:8080/users -X POST -H "Content-Type: application/json" -d '{"FirstName": "Rikka", "LastName": "Takarada"}'
{"id":1,"firstname":"Rikka","lastname":"Takarada"}

# Update
$ curl http://localhost:8080/users/1 -X PUT -H "Content-Type: application/json" -d '{"FirstName": "Akane", "LastName": "Shinjo"}'
{"id":1,"firstname":"Akane","lastname":"Shinjo"}

# Show
$ curl http://localhost:8080/users/1 -X GET
{"id":1,"firstname":"Akane","lastname":"Shinjo"}

# Delete
$ curl http://localhost:8080/users/1 -X DELETE


まとめ

そこそこ管理しやすい構成の API が完成したと思います。

他のモデルを追加するなどして、遊んでみてください。

今回、この API を作ってみて面白かったのは、go のフレームワークには特にこれだと決まった構成はないんだなという発見でした。

自分自身で、最適なディレクトリ構成が組み立てられるのは他の言語では体験したことがない感覚だったので新鮮でした。

MVC だけでなく他のアーキテクチャも自身で設計、構成まで自由に作り込めそうだなと感じました。

今後も Go の API を研究して、自分やチームにベストな構成を編み出していければと思います。

もっと「こういう構成があるよ」などの情報をお待ちしております。

最後まで読んでいただきありがとうございました!