1
6

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 5 years have passed since last update.

GAE/Go1.11試行(その3:「GoでMVC+S構成のWebAPIを作る」)

Posted at

お題

前回はGAE/Go(v1.11)の"google.golang.org/appengine"パッケージを使って情報取得というのをやった。

今回、当初はクリーンアーキテクチャ導入にトライしてみようかと考えたものの、ディレクトリ構成を用意していくだけで大分しんどいことに気づいた。
他の記事を読んでいるだけでは特に何も感じなかったけど、実際に自分で試そうとした時、scaffoldが無いとあまりにキツい。
そして、慣れるまではどこに何があるのかわからず、コードを追うのも辛かった。

というわけで、まずは慣れ親しんだMVC+Sくらいの構成から作ってみようと思った。

GAE試行Index

開発環境

# OS

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="17.10 (Artful Aardvark)"

# Golang

$ go version
go version go1.11.2 linux/amd64

バージョンの切り替えはgoenvで行っている。

前提

  • MVCの何たるかはなんとなくでも知っている。

実践

今回のソース全量は↓
https://github.com/sky0621/go-webapi-for-gae-study/tree/191f864b949b2f4801d2659220ed2ae0f7dbd631/backend

プロジェクト構成

$ tree
.
├── app.yaml
├── controller
│   ├── apierror.go
│   ├── form
│   │   ├── form.go
│   │   ├── id.go
│   │   └── user.go
│   └── user.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── index.yaml
├── main.go
├── model
│   ├── dto.go
│   └── user.go
├── service
│   └── user.go
└── view

説明

今回想定したのは、「ユーザ」情報を登録・取得・更新・削除するWebAPI。永続化はMySQL。
各エンドポイントは以下。

機能 HTTPメソッド パス
登録 POST /api/v1/users
取得 GET /api/v1/users/【ユーザID】
更新 UPDATE /api/v1/users/【ユーザID】
削除 DELETE /api/v1/users/【ユーザID】

■ main.go

 1 func main()  {
 2 	db, err := gorm.Open("mysql", "testuser:testpass@tcp(127.0.0.1:3306)/testdb?charset=utf8&parseTime=True&loc=Local")
 3 	if err != nil {
 4 		panic(err)
 5 	}
 6 	defer db.Close()
 7 
 8 	db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8").AutoMigrate(&model.User{})
 9 
10 	e := echo.New()
11 	defer e.Close()
12 
13 	http.Handle("/", e)
14 	g := e.Group("/api/v1")
15 
16 	controller.NewUser(db).Handle(g)
17 
18 	appengine.Main()
19 }

まず、2行目でMySQLに接続。
この接続情報は、あくまでローカル(docker-composeで立ち上げるMySQLへの接続限定)用。
GAE環境にデプロイする時は、接続情報はもとより、形式も下記のように変える必要がある。
"【ユーザ】:【パスワード】@unix(/cloudsql/【インスタンス接続文字列(※おそらく「【プロジェクト名】:【リージョン】:【インスタンスID】」)】)/【DB名】"

ちなみに、 docker-compose.yml の中身は下記。

version: '3'
services:
  db:
    image: mysql:5.7.24
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_USER: testuser
      MYSQL_PASSWORD: testpass
      MYSQL_DATABASE: testdb

8行目でGoの構造体をベースにMySQL上にテーブルを自動生成。

16行目で今回WebAPIとして提供する「ユーザ」に関するコントローラー(MVC+Sで言うCの部分)をHTTPリクエストの処理ハンドラーとして登録。

18行目でWebサーバ起動。 appengine.Main() を使っているけど、GAEでのDatastoreを使おうとしなければ、おそらく直接 http.ListenAndServe(〜〜) を呼んでも大丈夫。

■ controller/user.go

main.go の16行目で「 controller.NewUser(db).Handle(g) 」という形で呼び出されている。

Userの生成部分】

[controller/user.go]
func NewUser(db *gorm.DB) User {
	return &user{db: db}
}

type user struct {
	db *gorm.DB
}

type User interface {
	Handle(g *echo.Group)
}

【Handle】

[controller/user.go]
func (u *user) Handle(g *echo.Group) {
	g.POST("/users", u.createUser)
	g.GET("/users/:id", u.getUser)
	g.PUT("/users/:id", u.updateUser)
	g.DELETE("/users/:id", u.deleteUser)
}

それぞれのパスに対して、対応する処理メソッドを登録している。
以下、登録のメソッドをピックアップ。

[controller/user.go]
 1 func (u *user) createUser(c echo.Context) error {
 2 	uf := &form.User{}
 3 	if err := c.Bind(uf); err != nil {
 4 		return c.JSON(http.StatusBadRequest, errorJSON(http.StatusBadRequest, err.Error()))
 5 	}
 6 
 7 	id, err := service.NewUser(u.db).CreateUser(uf.ParseToDto())
 8 	if err != nil {
 9 		return c.JSON(http.StatusBadRequest, errorJSON(http.StatusBadRequest, err.Error()))
10 	}
11 
12 	return c.JSON(http.StatusCreated, &form.ID{ID: id})
13 }

3行目でリクエストパラメータを構造体 form.User にパースしている。

[controller/form/user.go]
type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

7行目で「 service.NewUser(u.db).CreateUser(uf.ParseToDto()) 」としてサービス(MVC+Sで言うSの部分)を使ってリクエストを処理。
uf.ParseToDto() 」の実装は下記の通り。

[controller/form/user.go]
func (n *User) ParseToDto() *model.User {
	// フォーマット変換等は、ここで吸収
	return &model.User{
		ID:   n.ID,
		Name: n.Name,
	}
}

リクエストパラメータをパースしたままの構造でDB登録まで受け渡してもいいのだけど、例えば日付のような項目をJSONで文字列の「2018/12/08」として受け取って、DB登録時はTimestampに変換して登録したいとかの都合に耐える設計にするために用意。
また、このように form と model(のDTO) とを分けることで、JSONパラメータとのマッピング用のタグとDBの登録時のカラムとのマッピング(や主キー、Not Null等の制約付け)用のタグとが混在して煩雑になるのを防げる。

■ service/user.go

ここは、イメージとしてはデザインパターンのFacadeに当たる。複数のモデル(=ビジネスロジック)を扱い、1つのユースケースを実現する。
トランザクション境界としての役割も担う。
とはいえ、今回のように「ユーザ」の情報を扱うだけなら、むしろ不要な”層”と言える。

[service/user.go]
func (n *userService) CreateUser(m *model.User) (string, error) {
	return model.NewUserDao(n.db).CreateUser(m)
}

■ model/user.go

リクエストパラメータをパースした構造(=form/user.go)から変換されたのが↓

[model/user.go]
type User struct {
	ID string `gorm:"column:id;primary_key"`
	Name string `gorm:"column:name;type:varchar(255);not null"`
}

いわゆる DataAccessObject として振る舞う構造体の生成に関する実装は下記。

[model/user.go]
func NewUserDao(db *gorm.DB) UserDao {
	return &userDao{db: db}
}

type userDao struct {
	db *gorm.DB
}

type UserDao interface {
	CreateUser(m *User) (string, error)
	GetUser(id string) (*User, error)
	UpdateUser(m *User) (*User, error)
	DeleteUser(id string) error
}

で、登録に関するメソッドが下記。

[model/user.go]
func (n *userDao) CreateUser(m *User) (string, error) {
	m.ID = strings.Replace(uuid.New().String(), "-", "", -1)
	n.db.Create(&m)
	if n.db.Error != nil {
		return "", n.db.Error
	}
	return m.ID, nil
}

動作確認

Postmanを使って確認。

登録

こんな curl を叩いて、

curl -X POST \
  http://localhost:8080/api/v1/users \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -H 'Postman-Token: fb182b6d-1ee3-4e1e-91ad-1c8b426e7dba' \
  -d '{
	"name": "さとう"
}'

こんなJSONが返る。

{
    "id": "7f8a46635aad48ccb668d8f1e6c0751b"
}

取得

こんな curl を叩いて、

curl -X GET \
  http://localhost:8080/api/v1/users/7f8a46635aad48ccb668d8f1e6c0751b \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -H 'Postman-Token: 0e933df9-5c71-4170-b888-7f4b1c368b51'

こんなJSONが返る。

{
    "ID": "7f8a46635aad48ccb668d8f1e6c0751b",
    "Name": "さとう"
}

まとめ

テストコードを書いていないのと、やはり1機能のCRUDレベルではクリーンアーキテクチャと比べたときの効用は計り知れない。
ただ、チームメンバーが誰も実際にクリーンアーキテクチャで実装したことがない、かつ、プロジェクトとしてそこに時間を費やせない場合、とにかくスピーディーに、かつ、ある程度、機能毎に分業できることを考えると、いい構成かも。

1
6
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
1
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?