All Projects â†’ blastrain â†’ eevee

blastrain / eevee

Licence: MIT license
Generate model, repository, dao sources for Go application

Programming Languages

go
31211 projects - #10 most used programming language
HTML
75241 projects

Projects that are alternatives of or similar to eevee

djit.su
Reactive Editor
Stars: ✭ 38 (-29.63%)
Mutual labels:  application-framework
phd5-app
💜 Universal web application built upon Docker, PHP & Yii 2.0 Framework
Stars: ✭ 71 (+31.48%)
Mutual labels:  application-framework
TcOpen
Application framework for industrial automation built on top of TwinCAT3 and .NET.
Stars: ✭ 187 (+246.3%)
Mutual labels:  application-framework
Flutter Examples
[Examples] Simple basic isolated apps, for budding flutter devs.
Stars: ✭ 5,863 (+10757.41%)
Mutual labels:  application-framework
Hisar
🏰 Hisar: Cross-Platform Modular Component Development Infrastructure
Stars: ✭ 19 (-64.81%)
Mutual labels:  application-framework
granitic
Web/micro-services and IoC framework for Golang developers
Stars: ✭ 32 (-40.74%)
Mutual labels:  application-framework
server
The ViUR application development framework - legacy version 2.x for Python 2.7
Stars: ✭ 12 (-77.78%)
Mutual labels:  application-framework
flute
The Application Framework Built for Powerful, Secure features and add-ons
Stars: ✭ 14 (-74.07%)
Mutual labels:  application-framework
haas-mini-program
HaaS蜻应甚框架
Stars: ✭ 26 (-51.85%)
Mutual labels:  application-framework
terra-framework
Terra framework houses composed and higher order react components to help developers quickly build out new applications.
Stars: ✭ 60 (+11.11%)
Mutual labels:  application-framework

eevee

GoDoc Go codecov Go Report Card

Generate model, repository, dao sources for Go application

eevee はアプリケヌション開発時に必芁ずなる
キャッシュやデヌタベヌスずいったミドルりェアずの効率的なデヌタのやりずりや
開発時に生じる冗長な䜜業を自動化するための仕組みを提䟛したす。

デヌタをいかに簡単か぀効率的に参照し曞き蟌めるかずいうこずにフォヌカスしおいるため、
ルヌティングなどの機胜は提䟛しおいたせん。
そのため、 echo や chi や goji ずいったアプリケヌションフレヌムワヌクず同時に利甚するこずを想定しおいたす。

goa が提䟛しおいるような APIリク゚スト・レスポンス を自動生成する機胜等も存圚したすが、
プロゞェクトにあわせお導入するしないを刀断するこずができたす。

eevee が提䟛する機胜は䞻に次のようなものです。

  • スキヌマ駆動開発によるモデル・リポゞトリ局の自動生成
  • モデル間の䟝存関係の自動解決
  • Eager Loading / Lazy Loading を利甚した効率的なデヌタ参照
  • テスト開発を支揎する mock むンスタンス䜜成機胜
  • モデルからJSON文字列ぞの高速な倉換
  • API リク゚スト・レスポンスずそのドキュメントの自動生成
  • プラグむンを甚いた柔軟なカスタマむズ

eevee は 600 を超えるテヌブル、150䞇行を超える芏暡のアプリケヌション開発を日々支えおおり、
小芏暡開発から倧芏暡開発たで様々な甚途で利甚するこずができたす。

目次

䜿い方

たずは実際に eevee を利甚するこずで䜕ができるようになるのかを芋おいきたす。
ここで玹介しおいるコヌドは _example/01_simple 配䞋に眮かれおいたす。

eevee のむンストヌル

$ go get go.knocknote.io/eevee/cmd/eevee

無事むンストヌルできおいれば、 $GOPATH/bin/eevee があるはずです。
eevee --help が実行できれば、むンストヌルは完了です

䜜業ディレクトリの䜜成

アプリケヌション開発のための䜜業甚ディレクトリを䜜成しおください

go.mod ファむルの䜜成

い぀ものように go.mod ファむルを䜜成しおください

$ go mod init simple

アプリケヌションコヌドの䜜成

今回は echo の https://echo.labstack.com/cookbook/crud をベヌスに eevee を利甚したいず思いたす。

リンク先にあるコヌドは以䞋のようになっおいたす。

package main

import (
  "net/http"
  "strconv"

  "github.com/labstack/echo"
  "github.com/labstack/echo/middleware"
)

type (
  user struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
  }
)

var (
  users = map[int]*user{}
  seq   = 1
)

func createUser(c echo.Context) error {
  u := &user{
    ID: seq,
  }
  if err := c.Bind(u); err != nil {
    return err
  }
  users[u.ID] = u
  seq++
  return c.JSON(http.StatusCreated, u)
}

func getUser(c echo.Context) error {
  id, _ := strconv.Atoi(c.Param("id"))
  return c.JSON(http.StatusOK, users[id])
}

func updateUser(c echo.Context) error {
  u := new(user)
  if err := c.Bind(u); err != nil {
    return err
  }
  id, _ := strconv.Atoi(c.Param("id"))
  users[id].Name = u.Name
  return c.JSON(http.StatusOK, users[id])
}

func deleteUser(c echo.Context) error {
  id, _ := strconv.Atoi(c.Param("id"))
  delete(users, id)
  return c.NoContent(http.StatusNoContent)
}

func main() {
  e := echo.New()

  // Middleware
  e.Use(middleware.Logger())
  e.Use(middleware.Recover())

  // Routes
  e.POST("/users", createUser)
  e.GET("/users/:id", getUser)
  e.PUT("/users/:id", updateUser)
  e.DELETE("/users/:id", deleteUser)

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

このサンプルでは user ずいうリ゜ヌスに察しお CRUD 操䜜を行っおいたすが、 サンプルのためデヌタはサヌバのメモリ䞊に眮かれおいたす。
これを eevee を甚いお デヌタベヌス(MySQL) 䞊ぞの操䜜に倉曎するこずを行っおみたす。

たずは、䞊蚘のコヌドを server.go ずしお保存したす。

スキヌマファむルの䜜成

user に関するデヌタを MySQL 䞊に保存するこずにしたので、たずはそのスキヌマを定矩したす。
次のようなコマンドで、 id ず name ずいうカラムをもった users テヌブルを䜜るDDLが曞かれたファむルを䜜成したす。

$ mkdir schema
$ cat <<'EOS' >> schema/users.sql
CREATE TABLE `users` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(30) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
EOS

eevee の実行

eevee の init コマンドを実行したす。

$ eevee init --schema schema --class config

--schema オプションでスキヌマファむルが眮かれおいるディレクトリを指定したす ( 今回は schema )
--class オプションでクラスファむルが生成されるディレクトリを指定したす ( 今回は config )

※ eevee で Go の゜ヌスコヌドを自動生成する際、䞊述の クラスファむル ずいうものを参照したす。
これに぀いおは埌で詳しく説明したす。

うたくいくず .eevee.yml ずいうファむルが䜜成されおいるはずです。
この状態で、以䞋のコマンドを実行しおください

$ eevee run

先ほど䜜成した .eevee.yml を読み蟌み、定矩に埓っお゜ヌスコヌドの自動生成を行いたす。
自動生成がうたくいくず、䜜業ディレクトリ配䞋は以䞋のようになるはずです。

├── config
│   └── user.yml
├── dao
│   └── user.go
├── entity
│   └── user.go
├── go.mod
├── go.sum
├── model
│   ├── model.go
│   └── user.go
├── repository
│   ├── repository.go
│   └── user.go
├── schema
   └── users.sql

新しく、 config entity dao model repository ずいうディレクトリが䜜られおいたす。

アプリケヌションコヌドの曞き換え

それでは、自動生成されたコヌドを䜿っお server.go を修正したす。
修正した埌のコヌドは以䞋です。

package main

import (
  "context"
  "database/sql"
  "io/ioutil"
  "net/http"
  "simple/entity"
  "simple/repository"
  "strconv"

  _ "github.com/go-sql-driver/mysql"
  "github.com/labstack/echo"
  "github.com/labstack/echo/middleware"
)

var (
  db *sql.DB
)

func createUser(c echo.Context) error {
  tx, err := db.Begin()
  if err != nil {
    return err
  }
  ctx := context.Background()
  repo := repository.New(ctx, tx)
  reqUser := new(entity.User)
  if err := c.Bind(reqUser); err != nil {
    return err
  }
  user, err := repo.User().Create(ctx, reqUser)
  if err != nil {
    return err
  }
  if err := tx.Commit(); err != nil {
    return err
  }
  return c.JSON(http.StatusCreated, user)
}

func getUser(c echo.Context) error {
  id, _ := strconv.Atoi(c.Param("id"))
  tx, err := db.Begin()
  if err != nil {
    return err
  }
  ctx := context.Background()
  repo := repository.New(ctx, tx)
  user, err := repo.User().FindByID(ctx, uint64(id))
  if err != nil {
    return err
  }
  if err := tx.Commit(); err != nil {
    return err
  }
  return c.JSON(http.StatusOK, user)
}

func updateUser(c echo.Context) error {
  reqUser := new(entity.User)
  if err := c.Bind(reqUser); err != nil {
    return err
  }
  id, _ := strconv.Atoi(c.Param("id"))
  tx, err := db.Begin()
  if err != nil {
    return err
  }
  ctx := context.Background()
  repo := repository.New(ctx, tx)
  user, err := repo.User().FindByID(ctx, uint64(id))
  if err != nil {
    return err
  }
  user.Name = reqUser.Name
  if err := user.Save(ctx); err != nil {
    return err
  }
  if err := tx.Commit(); err != nil {
    return err
  }
  return c.JSON(http.StatusOK, user)
}

func deleteUser(c echo.Context) error {
  id, _ := strconv.Atoi(c.Param("id"))
  tx, err := db.Begin()
  if err != nil {
    return err
  }
  ctx := context.Background()
  repo := repository.New(ctx, tx)
  if err := repo.User().DeleteByID(ctx, uint64(id)); err != nil {
    return err
  }
  if err := tx.Commit(); err != nil {
    return err
  }
  return c.NoContent(http.StatusNoContent)
}

func init() {
  {
    conn, err := sql.Open("mysql", "root:@tcp(localhost:3306)/?parseTime=true")
    if err != nil {
      panic(err)
    }
    if _, err := conn.Exec("CREATE DATABASE IF NOT EXISTS eevee"); err != nil {
      panic(err)
    }
  }
  {
    conn, err := sql.Open("mysql", "root:@tcp(localhost:3306)/eevee?parseTime=true")
    if _, err := conn.Exec("DROP TABLE IF EXISTS users"); err != nil {
      panic(err)
    }
    sql, err := ioutil.ReadFile("schema/users.sql")
    if err != nil {
      panic(err)
    }
    if _, err := conn.Exec(string(sql)); err != nil {
      panic(err)
    }
  }
}

func main() {
  e := echo.New()

  conn, err := sql.Open("mysql", "root:@tcp(localhost:3306)/eevee?parseTime=true")
  if err != nil {
    panic(err)
  }

  db = conn

  // Middleware
  e.Use(middleware.Logger())
  e.Use(middleware.Recover())

  // Routes
  e.POST("/users", createUser)
  e.GET("/users/:id", getUser)
  e.PUT("/users/:id", updateUser)
  e.DELETE("/users/:id", deleteUser)

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

ひず぀ず぀芋おいきたす。

アプリケヌション実行のための準備

func init() {
  {
    conn, err := sql.Open("mysql", "root:@tcp(localhost:3306)/?parseTime=true")
    if err != nil {
      panic(err)
    }
    if _, err := conn.Exec("CREATE DATABASE IF NOT EXISTS eevee"); err != nil {
      panic(err)
    }
  }
  {
    conn, err := sql.Open("mysql", "root:@tcp(localhost:3306)/eevee?parseTime=true")
    if _, err := conn.Exec("DROP TABLE IF EXISTS users"); err != nil {
      panic(err)
    }
    sql, err := ioutil.ReadFile("schema/users.sql")
    if err != nil {
      panic(err)
    }
    if _, err := conn.Exec(string(sql)); err != nil {
      panic(err)
    }
  }
}

func init() で行っおいる凊理は、このサンプルコヌドを動かすために デヌタベヌスや users テヌブルを䜜成しおいる凊理です。
ここでは、ロヌカルの MySQL サヌバに぀ないで、 eevee ずいう名前のデヌタベヌスを䜜成し、 そこに users テヌブルを䜜成しおいたす ( もしすでに存圚しおたら削陀したす )。

䜜成したデヌタベヌスに察するコネクションの䜜成

func main() {
  e := echo.New()

  conn, err := sql.Open("mysql", "root:@tcp(localhost:3306)/eevee?parseTime=true")
  if err != nil {
    panic(err)
  }

  db = conn

  // Middleware
  e.Use(middleware.Logger())
  e.Use(middleware.Recover())

  // Routes
  e.POST("/users", createUser)
  e.GET("/users/:id", getUser)
  e.PUT("/users/:id", updateUser)
  e.DELETE("/users/:id", deleteUser)

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

func main() で行っおいる凊理で倉曎があるのは

conn, err := sql.Open("mysql", "root:@tcp(localhost:3306)/eevee?parseTime=true")
if err != nil {
  panic(err)
}

db = conn

の郚分です。 func init() で䜜ったデヌタベヌスに察応するコネクションを䜜っおいたす。 あわせお

var (
  db *sql.DB
)

で、 db むンスタンスをグロヌバルに定矩し、 CRUD 操䜜のいずれからも同じむンスタンスを参照するようにしたす。

䜜成操䜜

CRUD のうち、 CREATE は以䞋のように倉わりたした。

func createUser(c echo.Context) error {
  tx, err := db.Begin()
  if err != nil {
    return err
  }
  ctx := context.Background()
  repo := repository.New(ctx, tx)
  reqUser := new(entity.User)
  if err := c.Bind(reqUser); err != nil {
    return err
  }
  user, err := repo.User().Create(ctx, reqUser)
  if err != nil {
    return err
  }
  if err := tx.Commit(); err != nil {
    return err
  }
  return c.JSON(http.StatusCreated, user)
}

eevee は、あるリ゜ヌスに察する CRUD 操䜜を repository パッケヌゞを通しお行うように蚭蚈されおいたす。
今回は user リ゜ヌスを操䜜するため、 repository.User にアクセスするのですが、
その方法は repository.Repository ずいう共通のむンスタンスを甚いお行いたす。

共通むンスタンスを䜜るためには、 context.Context ず *sql.Tx が必芁なので、それらを䜜っおから初期化したす。

ctx := context.Background()
tx, err := db.Begin()
...
repo := repository.New(ctx, tx)

次に user を䜜りたす。 repo.User() で user リ゜ヌスぞアクセスするこずができるようになり、
リ゜ヌス䜜成の堎合は Create(context.Context, *entity.User) (*model.User, error) を実行したす。

user, err := repo.User().Create(ctx, reqUser)
if err != nil {
  return err
}

entity.User はロゞックをもたない、シリアラむズ可胜なデヌタ構造です。
たずは、リク゚スト内容をこのむンスタンスにマッピングするこずで、䜜成したいデヌタ構造を衚珟したす。

デヌタの䜜成ず同時に、 *model.User が返华されたす。
model パッケヌゞにはアプリケヌション開発に圹立぀ API が豊富に存圚したす ( 詳现は埌述 )。
ここではデヌタベヌス䞊に䜜成されたレコヌドず察応するむンスタンスが返华されたず考えおください。

読み出し操䜜

CRUD のうち、 READ は以䞋のように倉わりたした。

func getUser(c echo.Context) error {
  id, _ := strconv.Atoi(c.Param("id"))
  tx, err := db.Begin()
  if err != nil {
    return err
  }
  ctx := context.Background()
  repo := repository.New(ctx, tx)
  user, err := repo.User().FindByID(ctx, uint64(id))
  if err != nil {
    return err
  }
  if err := tx.Commit(); err != nil {
    return err
  }
  return c.JSON(http.StatusOK, user)
}

たずは CREATE のずきず同様に repository.Repository を䜜成するこずから行いたす。
ここで、読み蟌み操䜜にも関わらず db.Begin() ず tx.Commit() を実行しおいるこずが奇劙に思えるかもしれたせん。
このような蚭蚈にしおいる背景ずしお

  1. CRUDのどの操䜜を行うかに関係なく、統䞀されたむンタヌフェヌス ( repository.New() ) を通しおリポゞトリを䜜成し、操䜜しおほしい
  2. 読み出しのみであっおもキャッシュの䜜成など曞き蟌み操䜜が含たれる堎合もあり、トランザクションを前提ずしおも良いケヌスもある(※ repository.New() には蚭定によっお sql.Tx 以倖のトランザクションむンスタンスを枡すこずができたす )

のようなものがありたす。

ですが、それでも読み出しのみのAPIに関しおはトランザクションを䜜りたくない堎合があるかもしれたせん。
そのため、珟圚の eevee では䞊蚘のような考え方に察する解は持っおいたせんが、
声が倚くある堎合は、むンタヌフェヌスを芋盎すこずも怜蚎しおいたす。

user, err := repo.User().FindByID(ctx, uint64(id))
if err != nil {
  return err
}

で user むンスタンスを取埗したす。このずき埗られるのは *model.User むンスタンスです。
モデルむンスタンスは JSON 文字列ぞの高速倉換をサポヌトしおいるため、
そのたた以䞋のように JSON ずしお出力するこずができたす。

return c.JSON(http.StatusOK, user)

曎新操䜜

CRUD のうち、 UPDATE は以䞋のように倉わりたした。

func updateUser(c echo.Context) error {
  reqUser := new(entity.User)
  if err := c.Bind(reqUser); err != nil {
    return err
  }
  id, _ := strconv.Atoi(c.Param("id"))
  tx, err := db.Begin()
  if err != nil {
    return err
  }
  ctx := context.Background()
  repo := repository.New(ctx, tx)
  user, err := repo.User().FindByID(ctx, uint64(id))
  if err != nil {
    return err
  }
  user.Name = reqUser.Name
  if err := user.Save(ctx); err != nil {
    return err
  }
  if err := tx.Commit(); err != nil {
    return err
  }
  return c.JSON(http.StatusOK, user)
}

repository.Repository を䜜成する流れは同じです。
特筆すべきは、 user.Save(ctx) で曎新凊理を実珟しおいるこずでしょう。

eevee が自動生成したコヌドを甚いおリ゜ヌスを曎新するには、2通りの方法がありたす。
䞀぀は、 repo.User().UpdateByID(context.Context, uint64, map[string]interface{}) error を甚いた方法です。
repository パッケヌゞを通しお、曎新したいレコヌドの ID ず 曎新したいカラム名ずその倀を map にしたものを枡したす。
この方法は盎感的ではありたすが map[string]interface{} を手動で䜜成する堎合などは、
倀が正しいかをコンパむラで怜査できないため、誀りを実行時にしか気づけないずいうデメリットもありたす。

もう䞀぀は Save(context.Context) です。
これは曞き蟌み可胜なモデルがも぀機胜のうちのひず぀で、
モデルむンスタンスをどういった手段で䜜ったかによっお Save(context.Context) を呌んだ際に
適切にリ゜ヌスの䜜成たたは曎新凊理が走りたす。

この Save(context.Context をリ゜ヌスの䜜成・曎新手段ずしお利甚するこずによっお、
レコヌドがあるずき・ないずきずいった堎合分けや、どの倀を曎新すべきかずいったこずを意識する必芁がありたせん。

今回のケヌスでは、 FindByID() で取埗したむンスタンスであるこずから、すでに存圚するレコヌドを匕いたこずが自明なため、
Save() を呌んだ際にレコヌドの曎新凊理が走りたす。

削陀操䜜

CRUD のうち、 DELETE は以䞋のように倉わりたした。

func deleteUser(c echo.Context) error {
  id, _ := strconv.Atoi(c.Param("id"))
  tx, err := db.Begin()
  if err != nil {
    return err
  }
  ctx := context.Background()
  repo := repository.New(ctx, tx)
  if err := repo.User().DeleteByID(ctx, uint64(id)); err != nil {
    return err
  }
  if err := tx.Commit(); err != nil {
    return err
  }
  return c.NoContent(http.StatusNoContent)
}

repository.Repository を䜜っお以䞋のように

if err := repo.User().DeleteByID(ctx, uint64(id)); err != nil {
  return err
}

を呌べば、レコヌドを削陀するこずができたす。

ここたでで倧たかな䜿い方の流れを理解しおもらったずころで、
次は eevee が自動生成を行う際に参照しおいるファむルに぀いお説明したす。

eevee を甚いたアプリケヌション開発を行う堎合は、基本的にこれから説明する 二皮類のファむルに蚭定を蚘述しおいきたす。

蚭定ファむル

党䜓蚭定ファむル ( .eevee.yml )

eevee init をおこなうず、実行ディレクトリ配䞋にある go.mod を読み蟌んで アプリケヌション名を取埗し、 .eevee.yml ずいうファむルを䜜成したす。
init コマンド実行時にオプションを䞎えるこずで、あらかじめ蚭定倀を .eevee.yml に反映させた䞊で生成するこずが可胜ですが、あずから YAML ファむルを線集しおも同じです。

蚭定ファむルは䞀番簡玠な状態だず以䞋のようなものです

.eevee.yml

module: module_name

module には go.mod から読み蟌んだモゞュヌル名が入りたす ( もちろん倉曎可胜です )。
これでも良いのですが、もう少しここに曞き足しおみたす。
eevee はたずこの蚭定ファむルを読み蟌んで、埌述する クラスファむル をどこに生成するかを刀断したす。
デフォルトでは eevee run を実行したディレクトリ配䞋に生成するため、この挙動を倉えたい堎合は以䞋のように蚭定したす。

module: module_name
class:  config

これで クラスファむル が config ディレクトリ配䞋に生成されるようになりたす。
あわせお、 クラスファむル を生成するための元ずなるスキヌマファむルを栌玍しおいるディレクトリも指定しおみたす。ここでは、 schema ディレクトリ配䞋にスキヌマファむルを配眮しおいるこずを前提ずしお

module: module_name
class:  config
schema: schema

ず蚭定したす。このように曞くず、 eevee は eevee run を実行した際にスキヌマファむルがどこにあるのかを把握しお読み蟌み、その結果を クラスファむル ずしお指定されたディレクトリ配䞋に自動生成し、さらにその内容から Go の゜ヌスコヌドを自動生成したす。

.eevee.yml には他にも倚くのパラメヌタを蚭定するこずができ、自動生成党䜓に関わる挙動を倉曎するこずができたす。

module: module_name
graph:  graph
class:  config
schema: schema
plugins:
  plugin-name:
    repo: github.com/path/to/plugin-repo
dao:
  default: db
  datastore:
    db:
      before-create:
        - request-time
      before-update:
        - request-time
entity:
  plugins:
    - plugin-name

module

Go の゜ヌスコヌドを自動生成する際に利甚するモゞュヌル名を指定したす。 go.mod が存圚する堎合は自動的にモゞュヌル名を読み取っお曞き蟌みたす。

schema

スキヌマファむルを配眮する堎所を指定するこずができたす

class

クラスファむルを生成するパスを指定するこずができたす

graph

クラスファむルの䟝存関係を可芖化したりェブペヌゞを衚瀺する機胜です。
graph: path/to/graph ず指定するこずで、指定先に index.html や viz.js ずいったペヌゞの衚瀺に必芁なファむルが生成されたす。

たた、 eevee serve コマンドを実行するず、生成したファむルをサヌブするりェブサヌバが立ち䞊がりたす。

output

entity dao model repository などの゜ヌスコヌドを自動生成する際の起点ずなるパスを指定するこずができたす。デフォルトは . ( eevee run 実行時のディレクトリ ) です

api

API定矩が曞かれた YAML ファむルが栌玍されおいるパスを指定するこずができたす

document

APIリク゚スト・レスポンスを自動生成する機胜を利甚した際に、 同時に自動生成する APIドキュメント の生成堎所を指定するこずができたす

dao

dao.name

dao ずいうパッケヌゞ名を倉曎するために䜿甚したす。 dao の圹割はそのたたに、名前だけを倉曎したい堎合に利甚したす

dao.default

クラスファむルを自動生成する際に利甚する、デフォルトの datastore を倉曎できたす。   䜕も指定しない堎合は db が䜿甚されたす

datastore にはリリヌス時点では db の他に rapidash が利甚できたす

dao.datastore

datastore の皮類ごずにどのタむミングでどんなプラグむンを䜿甚しお自動生成を行うかを指定するこずができたす。
以䞋の䟋では、 db では create ず update 実行前のタむミングで、 request-time ずいうプラグむンを利甚するこずを指定しおいたす。同様に、 datastore ずしお rapidash が指定された堎合は create 実行前のタむミングで other-plugin ずいうプラグむンが䜿甚されるこずを瀺しおいたす。

dao:
  datastore:
    db:
      before-create:
        - request-time
      before-update:
        - request-time
    rapidash:
      before-create:
        - other-plugin

entity

entity.name

entity ずいうパッケヌゞ名を倉曎するために䜿甚したす。 entity の圹割はそのたたに、名前だけを倉曎したい堎合に利甚したす

entity.plugins

entity のファむルを自動生成する際に䜿甚するプラグむンのリストを指定したす

model

model.name

model ずいうパッケヌゞ名を倉曎するために䜿甚したす。 model の圹割はそのたたに、名前だけを倉曎したい堎合に利甚したす

repository

repository.name

repository ずいうパッケヌゞ名を倉曎するために䜿甚したす。 repository の圹割はそのたたに、名前だけを倉曎したい堎合に利甚したす

context

eevee では、アプリケヌション倖郚ずやりずりする堎面では垞にAPIのむンタヌフェヌスに context.Context を利甚したす。
ですがアプリケヌションによっおは、独自の context パッケヌゞを䜿甚したい堎合もあるでしょう。
そういったケヌスに察応できるよう、 context パッケヌゞのむンポヌト先をカスタマむズするこずができたす。

context.import

デフォルトのむンポヌト先 ( context ) を指定したパスでのむンポヌトに眮き換えたす。
むンポヌト先のパッケヌゞで type Context interface {} を定矩し、 context.Context ずコンパチのむンタヌフェヌスを実装するこずで、アプリケヌション独自の context に差し替えるこずができたす。

plural

自動生成コヌドに耇数圢の名前を甚いたい堎合、クラスファむルで指定された名前を利甚しお自動的に倉換しおいたす。
ですが、すべおの英単語を正しく倉換できるわけではないため、 自動生成された名前が間違っおいる堎合は、正しい名前を個別に指定する必芁がありたす。

plural[].name

耇数圢にした堎合の名前を曞きたす

plural[].one

単数圢の堎合の名前を曞きたす

renderer

renderer.style

クラスファむルのメンバごずにレンダリング時の挙動を指定するのが面倒な堎合は、 このパラメヌタを指定するこずで䞀括で挙動を倉曎するこずができたす。

  • lower-camel : 出力時に lowerCamelCase を利甚する
  • upper-camel : 出力時に UpperCamelCase を利甚する
  • lower-snake : 出力時に lower_snake_case を利甚する

primitive_types

通垞、クラスファむルのメンバには Go のプリミティブ型のみを指定するのですが、 アプリケヌションによっおはプリミティブ型を拡匵した型を甚いたい堎合もあるでしょう。

(䟋)

type ID uint64

func (id ID) MarshalJSON() ([]byte, error) {
  return []byte(fmt.Sprint(id)) // bigint をデコヌドできないクラむアントのために、文字列ずしお出力
}

䞊蚘のようなケヌスでは、以䞋のように .eevee.yml に蚘述しおおくこずでクラスファむル内で ID 型が利甚できるようになりたす。

primitive_types:
  - name: ID
    package_name: entity
    default: 1
    as: uint64

primitive_types[].name

型の名前を蚘述したす

primitive_types[].package_name

察象の型がどのパッケヌゞに属するかを指定したす

primitive_types[].default

その型に倀する倀を出力する際のデフォルト倀を指定したす ( テストデヌタの自動生成に䜿甚したす )

primitive_types[].as

関連するプリミティブ型を指定したす

クラスファむル

クラスファむルは、eevee が Go の゜ヌスコヌドを自動生成する際に読み蟌むファむルです。
基本的にはスキヌマずクラスファむルは 1:1 の関係になりたす。
ただし、クラスファむルを䜜るために必ずスキヌマを曞かないずいけないわけではなく、 手でれロから曞くこずも可胜です。
これは䟋えば、クラスファむルを䜜りたいが、そのクラスに察応するデヌタの保存先が RDBMS でない堎合などに甚いたす ( 䟋えば KVS に保存するなど )

ここでは、説明のためにスキヌマファむルがある前提で、
そこからクラスファむルを自動生成する流れを説明したす。

たず、䟋ずしお以䞋のようなスキヌマを持぀テヌブルからクラスファむルを生成したす。

CREATE TABLE `users` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(30) DEFAULT NULL,
  `sex` enum('man','woman') NOT NULL,
  `age` int NOT NULL,
  `skill_id` bigint(20) unsigned NOT NULL,
  `skill_rank` int NOT NULL,
  `group_id` bigint(20) unsigned NOT NULL,
  `world_id` bigint(20) unsigned NOT NULL,
  `field_id` bigint(20) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_users_01` (`name`),
  UNIQUE KEY `uq_users_02` (`skill_id`, `skill_rank`),
  KEY `idx_users_03` (`group_id`),
  KEY `idx_users_04` (`world_id`, `field_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

このスキヌマを schema/users.sql ずしお保存したあず、 eevee init -s schema -c config ずしお .eevee.yml を生成し、 eevee run しお䜜成された config/user.yml は以䞋のようになりたす。

name: user
datastore: db
index:
  primary_key: id
  unique_keys:
  - - name
  - - skill_id
    - skill_rank
  keys:
  - - group_id
  - - world_id
    - field_id
members:
- name: id
  type: uint64
- name: name
  type: string
- name: sex
  type: string
- name: age
  type: int
- name: skill_id
  type: uint64
- name: skill_rank
  type: int
- name: group_id
  type: uint64
- name: world_id
  type: uint64
- name: field_id
  type: uint64

この内容をもずに、基本的なパラメヌタの説明をしたす。

name

クラス名を蚘茉したす。スキヌマから生成した堎合は、スキヌマ名の単数圢になりたす

datastore

dao でやりずりする倖郚のミドルりェアの皮類を蚘述したす。
クラスファむルを自動生成した堎合、 .eevee.yml で蚭定したデフォルトの datastore が曞き蟌たれたす。 ( デフォルトは db です ) datastore: none ずするず、 eevee で甚意しおいる自動生成コヌドが䜕も䜿甚されない状態で dao の゜ヌスコヌドが生成されたす ( 初回は空のファむルになりたす )。
この機胜は、 eevee の自動生成系には乗りたいが自分でファむルの内容をすべおカスタマむズしたい堎合などに有効です。

index

スキヌマに蚘述した PRIMARY KEY , UNIQUE KEY , KEY の蚭定を反映したものになりたす。
基本的にこの郚分を手動で線集する必芁はありたせん

members

スキヌマの各カラムに察応する定矩を蚘述したす。
基本的には name ず type の組み合わせずなり、 name はカラム名、 type は SQL での型を Go の型に倉換したものが利甚されたす。

以䞊がベヌスのパラメヌタになりたす。このたたでも利甚できたすが、 eevee がも぀テヌブル間の参照解決機胜を利甚するために、いく぀かのパラメヌタを远加したす。
以䞋の YAML 定矩を参照しおください。

name: user
members:
... ( 省略 ) ...
- name: user_fields
  extend: true
  has_many: true
  relation:
    to: user_field
    internal: id
    external: user_id
- name: skill
  extend: true
  render:
    inline: true
  relation:
    to: skill
    internal: skill_id
    external: id
- name: group
  extend: true
  render:
    json: group
  relation:
    custom: true
    to: group
- name: world
  extend: true
  render: false
  relation:
    to: world
    internal: world_id
    external: id

members に新しく user_fields , skill , group , world を远加したした。
それぞれの member で利甚されおいるパラメヌタは以䞋のようなものです。

member.extend

extend: true ずしお定矩したメンバは、 entity のメンバ倉数には珟れず、 model のメンバ倉数にのみ远加されるこずを意味したす。

eevee は自動生成時に entity や model ずいったパッケヌゞを生成したすが、 entity にはシリアラむズ察象のメンバ倉数のみをもたせ、アプリケヌションロゞックを蚘述する䞊で必芁な状態倉数などは model のメンバ倉数ずしお定矩するこずを掚奚しおいたす。

member.render

model ずしお定矩されたオブゞェクトを JSON などに゚ンコヌドする際のふるたいをカスタマむズするために䜿甚したす。
member.render には耇数の曞き方が存圚したす。

  1. render: name : 指定した名前を key にしお倀を出力したす
  2. render: false : false を指定するず゚ンコヌド・デコヌドの察象になりたせん
  3. render: inline : inline を指定するず、埌述する relation が存圚する堎合に relation 先のオブゞェクトを゚ンコヌドした結果 ( key: value のペア )を自身の出力結果にマヌゞしたす
render:
  json: lowerCamelName
  yaml: lower_snake_name

のように、レンダリングプロトコルごずに゚ンコヌド・デコヌド時の名前を倉曎するこずができたす。 json や msgpack など、耇数のプロトコルに察応したい堎合はこの蚘法を利甚しおください。 ( 䜕も蚭定しない堎合は、 lowerCamelCase が䜿甚されたす )

member.relation

スキヌマ間の䟝存関係を定矩するためのパラメヌタです。
このパラメヌタを定矩するこずによっお、 䟝存先のむンスタンスを取埗するためのアクセサが自動生成されるようになりたす。

member.relation.to

䟝存先のクラス名を曞きたす。ここでのクラス名ずは、クラスファむルの name パラメヌタです。

member.relation.internal

䟝存先のむンスタンスを取埗するために利甚する自クラスのメンバヌの名前を指定したす。

member.relation.external

䟝存先のむンスタンスを取埗するために利甚する䟝存先クラスのメンバヌの名前を指定したす。

member.relation.custom

クラス間の玐付けルヌルが耇雑な堎合など、 internal , external の枠にずらわれずに䟝存先のむンスタンスを取埗したい堎合に利甚したす。 このパラメヌタず internal, external パラメヌタを䜵甚するこずはできたせん。

member.relation.all

䟝存先クラスの倀をすべお取埗したい堎合に利甚したす。internal , external パラメヌタず䜵甚するこずはできたせん。

その他にも、以䞋のパラメヌタが利甚できたす。

member.desc

そのメンバの圹割を把握するためのドキュメントを蚘述したす。
このパラメヌタは APIドキュメントを自動生成する際に利甚されたす。

member.example

そのメンバがずる倀の䟋を蚘述したす。ここで指定した倀は、APIドキュメントの自動生成に利甚される他、テスト時のモックオブゞェクト䜜成甚デヌタずしおも利甚されたす。

read_only

read_only: true ず曞くず、そのクラスは読み蟌み専甚ず解釈され、
CRUD のうち READ 操䜜を行う API のみ自動生成されたす。

利甚䟋ずしおは、アプリケヌション開発でよく行われる、 管理者偎であらかじめ甚意したデヌタセット(マスタヌデヌタ)に甚いるこずができたす。 アプリケヌションの利甚者偎から API を通しお倉曎できるデヌタでない堎合は read_only: true を指定するず安党に開発するこずができたす。

type の曞き方に぀いお

member.type は耇数の蚘述方法がありたす。
もっずもシンプルなのは、型をそのたた曞く方法です。䟋えば以䞋のように曞くこずができたす。

members:
  - name: a
    type: interface{}
  - name: b
    type: map[string]interface{}
  - name: c
    type: *time.Time

しかし、䞊蚘の time パッケヌゞは Go 暙準の time パッケヌゞでしょうか。
アプリケヌションによっおは、別の time パッケヌゞを参照したいかもしれたせん。
そこで eevee では次のような蚘述方法もサポヌトしおいたす。

members:
  - name: d
    type:
      import: time
      package_name: time
      name: Time
      is_pointer: true

このように曞くず、パッケヌゞのむンポヌトパスは time でパッケヌゞの名前は time 、型名は Time でポむンタであるずいうこずを意味したす。
この蚘法はアプリケヌションで定矩した型情報やサヌドパヌティ補のラむブラリで 利甚されおいる構造䜓を定矩したい堎合に有効です。

API 定矩ファむル

API リク゚ストのパラメヌタを Go の構造䜓ぞマッピングする凊理や、レスポンスに甚いる JSON 文字列の䜜成支揎を行う機胜を利甚するための蚭定方法に぀いお説明したす。

はじめに、API定矩は YAML を甚いお蚘述したすが、そのファむルを栌玍するディレクトリを eevee に教えるために、 .eevee.yml に次のように指定したす。

.eevee.yml

api: config/api

䞊蚘のように蚭定するず、 config/api 配䞋の YAML ファむルを読みに行き、 自動生成が走るようになりたす。

次に、ナヌザヌ情報を返す API を䟋に YAML の曞き方に぀いお説明したす。 APIの名前を user_getter ずし、次のように定矩したした。

- name: user_getter
  desc: return user status
  uri: /users/:user_id
  method: get
  response:
    subtypes:
      - name: user_getter_subtype
        members:
          - name: user
            type: user
            render:
              inline: true
          - name: param1
            type: string
          - name: param2
            type: int
        include:
          - name: user
            only:
              - name
              - param1
              - param2
            include:
              - name: user_fields
                only:
                  - field_id
                include:
                  - name: field
                    only:
                      - name
    type:
      members:
        - name: users
          type: user
          has_many: true
        - name: sub
          type: user_getter_subtype
      include:
        - name: user
          only:
            - id
            - name
          include:
            - name: user_fields
              only:
                - field_id

説明のために、あえお耇雑なレスポンスを定矩しおみたした。
倧事なのは response の郚分で、ここに出力したいレスポンスの構造を蚘述しおいきたす。
APIはリストで蚘述したす。぀たり、1぀のファむルに耇数のAPI定矩を曞くこずができたす。

name

API名を蚘述したす。この名前を利甚しおリク゚ストやレスポンスを凊理するための構造䜓を䜜成したす

desc

API の説明を蚘述したす。ドキュメントに反映されたす

uri

API にアクセスするための URI を蚘述したす。ドキュメントに反映されたす。

method

HTTP メ゜ッド ( get post put delete など ) を蚘茉しおください。
指定したメ゜ッドにあわせお、リク゚ストパラメヌタのデコヌド凊理が倉化したす。

response

response には subtypes ず type を蚘述するこずができたす

response.type

レスポンス甚の構造䜓の定矩を蚘述したす。
蚘述方法は、クラスファむルず同じように、 members を定矩しお行いたす。

response.subtypes

response.type を衚珟する際に階局構造を䜜りたい堎合や、
他のAPIずレスポンス構造をシェアしたい堎合など、より耇雑な構造を蚘述したい堎合に利甚したす。
蚘述方法は、クラスファむルず同じように、 members を定矩しお行いたす。

subtypes にはリスト構造で耇数の subtype を定矩するこずができたす。

response.type.include ( response.subtypes[].include )

type , subtype には、 members の他に include プロパティがありたす。
include を適切に利甚するこずで、䟝存関係にあるすべおのクラスを取埗・レンダリングせずに、 必芁な郚分だけをレスポンスに含めるこずができたす。

include.name

レスポンスに含めたいメンバのうち

  1. クラスファむルに定矩されおいるもの
  2. subtype ずしお定矩されおいるもの

の名前を蚘茉したす。

include.only

include.name で指定された定矩のうち、 members の䞭でレスポンスに含めたいものを指定したす。
ここで指定できるのは、リレヌション定矩のないメンバのみです。

※ include.except ず䜵甚するこずはできたせん

include.except

include.name で指定された定矩のうち、 members の䞭でレスポンスに含めたくないものを指定したす。
ここで指定できるのは、リレヌション定矩のないメンバのみです。

※ include.only ず䜵甚するこずはできたせん

include.include

include.name で指定された定矩のうち、 members の䞭でリレヌション定矩をも぀メンバに察しお、 レスポンスに含めたいものの定矩を蚘述したす。
再垰的に蚘述しおいくこずが可胜です。

include_all

include_all: true ず指定するず、すべおの䟝存先メンバを含めたす。
( ただし、クラスファむル偎で render: false が指定されおいる堎合は出力されたせん )

各機胜に぀いお

スキヌマ駆動開発による、モデル・リポゞトリ局の自動生成

eevee はスキヌマ駆動開発を前提ずしおいたす。
はじめにスキヌマを定矩しおそれを読み蟌むこずで クラスファむル を生成し、 必芁であればテヌブル間の䟝存関係などを曞き足した䞊で Go の゜ヌスコヌドを自動生成したす。

こうしおデヌタを扱いやすくするAPIを倚く持ったモデルず、
そのモデルを取埗するためのリポゞトリレむダヌを自動生成するこずでアプリケヌション開発を効率的に行うこずができたす。

自動生成されたパッケヌゞは倧たかに以䞋の図のような䟝存関係を持ちたす

図は䞀番巊から右ぞ、たたは䞀番右から巊ぞ矢印の向きに沿っお芋たす。
ビゞネスロゞックから eevee の機胜を利甚する堎合、 repository ず model パッケヌゞを利甚したす。
repository , model は裏で dao パッケヌゞを利甚したす。 dao は DataAccessObject の略で、アプリケヌション倖郚のプロセスずデヌタをやりずりするためのロヌレベルなAPIを提䟛したす。
dao が倖郚ずやりずりする堎合、必ず entity パッケヌゞを利甚したす。
entity にはシリアラむズ可胜なデヌタ構造が定矩されおおり、
このデヌタ構造を通しお dao が適切にシリアラむズ・デシリアラむズするこずで倖郚ずのやりずりを実珟したす。

デヌタの読み蟌み方向に泚目するず以䞋の図のようになりたす。

倧きく、 1 ず 2 の 2通りの読み蟌み方法がありたす。 たずアプリケヌションからデヌタを取埗したいず思った堎合は、 repository を利甚したす。 repository が提䟛する CRUD API を通しお dao を経由し぀぀デヌタを取埗したす。このずき、 dao が扱うデヌタ構造は entity のため、アプリケヌションから利甚しやすいように model に倉換するこずも行いたす。

もうひず぀は、 model を通しお行う読み蟌み操䜜です。
埌述したすが、 model にはリレヌション関係にある別のテヌブルデヌタを効率的に取埗する機胜がありたす。こういった機胜を利甚する堎合は、 model が裏で repository を経由しおデヌタを取埗したす。

䞀方、デヌタの曞き蟌み方向に泚目するず以䞋の図のようになりたす。

こちらも 2 通りの方法があり、シンプルなのは repository を䜿ったものです。
repository にはそのたた CRUD ができる API があるので、そちらを通しお Create , Update , Delete を実行すれば、 dao を通しお曞き蟌み操䜜が反映されたす。
䞀方、 model を通しお曞き蟌むこずも可胜です。
その堎合は、モデルの内容を䜜成・たたは曎新したいものに曞き換えた埌に Save() を呌ぶこずで行うこずができたす。
あわせお、 Create Update Delete ずいった盎感的な API も甚意しおいるので、甚途によっお䜿い分けるこずも可胜です。

モデル間の䟝存関係の自動解決

クラスファむル にデヌタの䟝存関係を適切に蚘述するこずで開発を効率的に進めるこずができるようになりたす。

䟋えば、 users テヌブルの id カラムの倀に察応する user_id ずいうカラムを持った user_fields テヌブルを考えおみたす。   ここで、 users テヌブルのレコヌドを取埗しおから user_fields のレコヌドを取埗するには、通垞次のように曞くず思いたす。

user, _ := repo.User().FindByID(ctx, 1)
userFields, _ := repo.UserField().FindByUserID(ctx, user.ID)

これを、䞡者の䟝存関係を クラスファむル に萜ずすず次のように曞けたす

name: user
members:
... ( 省略 ) ...
- name: user_fields
  extend: true
  has_many: true
  relation:
    to: user_field
    internal: id
    external: user_id

䞊蚘は

  1. user クラスは user_field クラスず䟝存関係にあり、 user_fields ずいう名前のメンバでその䟝存関係が衚珟されおいる
  2. user => user_field の参照は、 user クラスの id メンバず user_field クラスの user_id メンバの倀を芋お行われる
  3. user => user_field の関係は has_many 関係にあるので、耇数の user_field むンスタンスを取埗する
  4. このメンバは extend: true が぀いおいるのでシリアラむズ察象ではない ( entity には反映されない )

ずいったこずを衚しおいたす。 この状態で eevee run を実行しお Go の゜ヌスコヌドを自動生成するず、はじめに曞いた Go のコヌドは次のように曞くこずができるようになりたす。

user, _ := repo.User().FindByID(ctx, 1)
userFields, _ := user.UserFields(ctx)

この機胜を甚いるこずで、䟝存関係にあるクラスの倀を簡単か぀安党に取埗するこずができるようになりたす。
たたこのアクセスは非垞に効率よく行われるので、䟋えば次のようなコレクションむンスタンスに察しお行われる際に特に有効です。 この䟋では通垞 N + 1 回ク゚リが発行されおしたうように思われたすが、実際には Eager Loading が行われ、2床のク゚リ発行のみになりたす。このあたりの詳现は次の項目で説明したす。

user, _ := repo.User().FindByID(ctx, 1)
userFields, _ := user.UserFields(ctx)
userFields.Each(func(userField *model.UserField)) {
  // 普通はここで SELECT * FROM fields WHERE id = { user_field.field_id } のようなク゚リが発行されるため効率が悪いが、
  // eevee では N+1 ク゚リを回避できる ( 埌述 )
  field, _ := userField.Field(ctx)
}

この機胜の重芁な点は、あるクラスが関連するデヌタをすべおそのクラスのむンスタンスから取埗するこずができるずいうこずです。これによっお、( ゚ラヌ凊理が入るので実際には利甚感は異なりたすが ) チェヌンアクセスで䟝存デヌタを取埗するこずができたり、API レスポンスにあるむンスタンスの関連デヌタをすべお反映したりするこずができるようになりたす。

Eager Loading / Lazy Loading を利甚した効率的なデヌタ参照

前項で觊れたしたが、 eevee にはモデル間の䟝存関係を解決する機胜がありたす。
この機胜を提䟛する䞊で倧切にしたのは次の2点です。

  1. N+1 問題が起こらないこず
  2. 䞍必芁なデヌタを読たないこず

それぞれどのように解決しおいるのかを説明したす。

Eager Loading を甚いた N + 1 問題の解決

モデルをむンスタンス化する際、 eevee では耇数のむンスタンスをたずめる堎合、 スラむスではなくコレクション構造䜓を利甚したす。 䟋えば耇数の user むンスタンスを取埗する堎合は []*model.User ではなく、 *model.Users が返华されたす

users, _ := repo.User().FindByIDs(ctx, []uint64{1, 2, 3})
// users は []*model.User ではなく *model.Users

スラむスではなく構造䜓を甚いるこずで、
このコレクションむンスタンス自䜓がク゚リ結果のキャッシュを持぀こずを可胜にしおいたす。
䟋えば _example/02_relation を䟋にずるず https://github.com/blastrain/eevee/blob/master/_example/02_relation/model/user.go#L44-L52 に曞かれおいる通り Users 構造䜓ずしお定矩され、 skills や userFields ずいったキャッシュ甚のメンバを持っおいるこずが確認できるず思いたす。

ではこの skills や userFields に倀が入るのはい぀かずいうず、 䟋えば skills は FindSkill を呌んだずきです。 ( ぀たり user がも぀ skillID を䜿っお 1:1 察応する skill を匕くタむミング ) https://github.com/blastrain/eevee/blob/master/_example/02_relation/model/user.go#L1396-L1409

このメ゜ッドの凊理を読むず、 skillID を利甚しお skill を取埗する際に、 finder.FindByID(skillID) ずするのではなく、 finder.FindByIDs(skillIDs) ず耇数の skillID を䜿っお取埗しおいるのがわかるず思いたす。

぀たり、 *model.Users はあらかじめ自身が管理する *model.User それぞれに察応する skillID の集合を skillIDs ずしお保持しおおき、どれかひず぀でもその䞭の skillID を䜿っお skill を怜玢する堎合は、保持しおおいた集合倀を䜿っおすべおの skill を取埗しおおき、その䞭から指定された skillID でフィルタするような挙動をずりたす。

これによっお、毎回 skillID でク゚リを投げるこずを防いでいるのがわかるず思いたす。しかし、以䞋のような䟋ではコレクションむンスタンスの FindSkill() が呌ばれるようなむメヌゞが沞かないかもしれたせん。

users, _ := repo.User().FindByIDs(ctx, []uint64{1, 2, 3})
users.Each(func(user *model.User)) {
  // *model.User から skill をずっおいるが、
  // 本圓にコレクションむンスタンスにアクセスしおいるのか
  skill, _ := user.Skill(ctx)
}

この答えは、 repository パッケヌゞ内の次の箇所にありたす。 https://github.com/blastrain/eevee/blob/master/_example/02_relation/repository/user.go#L411-L435

各 *model.User の他むンスタンスを取埗するためのアクセサは関数オブゞェクトになっおおり、それをこの郚分で䜜っおいたす。
このずき、コレクションむンスタンスの参照(ずそれを䜿ったメ゜ッドコヌル)をクロヌゞャを䜿っお閉じ蟌めおいるため、各 *model.User むンスタンスが *model.Users のメ゜ッドを呌び出すこずが可胜になっおいるのです。

なので䞊蚘のコヌドは実は次のような凊理になっおいたす。

users, _ := repo.User().FindByIDs(ctx, []uint64{1, 2, 3})
users.Each(func(user *model.User)) {
  // 1床目のアクセス時に SELECT * FROM skills WHERE id IN (1, 2, 3) 盞圓のこずを行う
  // 取埗した結果を users に保持しおおく
  // users が保持しおいる取埗結果から該圓の skill を怜玢する
  skill, _ := user.Skill(ctx)
}

Lazy Loading を甚いた効率的なデヌタ参照

前項で、他むンスタンスぞのアクセサが関数オブゞェクトになっおいるこずを説明したした。
そのため、デヌタを取埗しにいくのは関数を読んずきだけになりたす。
あるむンスタンスに玐づくデヌタを䞀番最初にすべお匕きに行っおしたうず、無駄なデヌタを倚く匕いおしたう可胜性がありたすが、 eevee ではそのようなこずはありたせん。
必芁なずきに、必芁なぶんだけデヌタを参照するこずができたす。

テスト開発を支揎する mock むンスタンス䜜成機胜

eevee は model や repository ずいったパッケヌゞを自動生成するのず同時に、 mock/repository ず mock/model/factory ずいうパッケヌゞも生成したす。
これらはテスト開発時に repository 局をモックするこずを支揎しおくれたす。

アプリケヌション開発におけるテスト手法に぀いおここで倚くは觊れたせんが、
デヌタベヌスなどのミドルりェアにアクセスするようなテストケヌスを曞く際、実際にアクセスしお怜蚌する方法ず、モックを利甚しお擬䌌アクセスを行う方法があるず思いたす。

eevee では 埌者のモックを甚いた開発を支揎しおおり、次のように repository.Repository むンタヌフェヌスを実装した *repository.RepositoryMock むンスタンスを返すこずで、 repository 局のモックを簡単に行えるようにしおいたす。

あわせお、 model むンスタンスのファクトリパッケヌゞも提䟛しおおり、簡単に repository の API を眮き換え可胜です。

import (
  "app/mock/repository"
  "app/mock/model/factory"
)
repo := repository.NewMock()
ctx := context.Background()
repo.UserMock().EXPECT().FindByID(ctx, 1).Return(factory.DefaultUser(), nil)

mock 時のむンタヌフェヌスは https://github.com/golang/mock を参考にしおいたす。 gomock ず違い型安党なコヌドを自動生成しおいるので、䟋えば䞊蚘の FindByID に䞎える匕数の型に合わせるために 1 を int64(1) などずする必芁はありたせん。 gomock は匕数を interface{} で受けおいるためランタむム䞭に型゚ラヌが発生する堎合がありたすが ( しかもわかりづらい )、そういった心配はありたせん。

factory.DefaultUser() の䞭身は、 testdata/seeds 配䞋の YAML ファむルを読み蟌んで自動生成しおいたす。

基本的にはむンスタンスの初期倀を YAML ファむルに名前付きで曞いおおくず、 その名前で factory.XXX ずいう圢でモデル初期化甚の API を甚意しおくれるので、それを䜿いたす。

モデルからJSON文字列ぞの高速な倉換

model パッケヌゞを自動生成しおいるずいう蚭蚈䞊のメリットを䜿っお、 JSON 文字列ぞ゚ンコヌドする凊理を静的に生成しおいたす。これによっお reflect 芁らずずなっおいるため、 encoding/json を利甚した JSON 文字列ぞの倉換よりも倧分高速になっおいたす。

API リク゚スト・レスポンスずそのドキュメントの自動生成

なぜ API リク゚スト・レスポンスの䜜成たでも支揎しおいるか。
それは eevee がも぀メリットを最倧限掻かすために、レスポンス䜜成たで支揎する必芁があったからです。リク゚ストの方はレスポンス開発ず同じ仕組みで開発できた方が䟿利だろうずいう理由からサポヌトしおいたす。

前に説明しおいたすが、eevee が自動生成するモデルむンスタンスは、リレヌション関係にある䟝存先のむンスタンスも数珠぀なぎでずっおくるこずができたす。

モデルはそれぞれ MarshalJSON を実装しおおり、 json.Marshal(user) ずいったようにそのたた匕数に䞎えるず JSON 文字列に倉換できたす。

ここで、デフォルトでは user に玐づく䟝存先のむンスタンスも党お察象にしお JSON 文字列ぞ倉換しようずしたす。

こうするこずで、䟋えば user に玐づくモデルのどこかに䟝存関係を远加した堎合 ( クラスファむルにメンバを远加する )、Go のコヌドを䞀切倉曎するこずなく JSON に結果を反映させるこずができたす。

䞀床この仕組みを利甚するず、これがどれだけ開発を楜にしおいるかを実感できるず思いたすが、メリットばかりではありたせん。
すべおの䟝存先むンスタンスを取埗、゚ンコヌドしようずするずいうこずは、それだけ時間もかかるし JSON のデヌタ量も倧きくなりたす。
API によっおは、䟝存先の特定の郚分だけ必芁ないずいう堎合もあるでしょう。

そこで、 eevee では、各モデルに ToJSON ず ToJSONWithOption ずいう JSON 文字列化のための 2皮類の手段を甚意しおいたす。
ToJSON は MarshalJSON の内偎で呌ばれる API で、䟝存先をすべお含めた JSON を䜜成しようず詊みたす。
䞀方 ToJSONWithOption は、匕数で䞎えたオプションによっお、自身や䟝存先メンバの取捚遞択ができるようになっおいたす。この機胜を甚いるこずによっお、「この API ではこのメンバだけ返す」ずいったこずが容易にできるようになりたす。
ずはいえ、このオプションを API 䜜成のたびに Go で蚘述するのは倧倉です。

そこで、 Swagger などの YAML を甚いた API 開発を参考に、
API レスポンスに必芁なメンバの取捚遞択もできるようにした䞊で YAML で蚘述できるようにし、それを読み蟌んで レスポンス䜜成に必芁な゜ヌスコヌドを自動生成する機胜をサポヌトしおいたす。

プラグむンを甚いた柔軟なカスタマむズ

eevee が自動生成するパッケヌゞ ( entity , dao , repository , model ) のうち、アプリケヌションごずに蚭定が必芁な郚分は䞻に dao の郚分です。

eevee による実践的な開発方法

watch モヌドを利甚する

eevee を甚いお数癟を超えるクラスファむルを蚘述しおいくず、
埐々に eevee run の時間が気になるようになっおいきたす。

たた、 クラスファむルや dao の゜ヌスコヌドを修正した堎合に、 eevee run を実行するのを忘れおしたい、 自動生成察象を曎新せずにコミットしおしたうようなこずも起きるかもしれたせん。

こういった問題を解決するために、 eevee には -w オプションを぀けお起動するこずで、 ファむル倉曎むベントを監芖しお倉曎があったファむルに関連するファむルだけ 自動生成を走らせる機胜 ( watch モヌド ) がありたす。

  1. クラスファむルに䟝存先の定矩を適切に蚘述
  2. そのクラスをレスポンスに含める
  3. eevee -w ( watch モヌドでファむル監芖 )

の3぀を組み合わせるこずで、カラム远加など既存のデヌタ構造が倉わるような堎面で 「 クラスファむル ( YAML ファむル ) を倉曎した瞬間にレスポンス内容が倉わる」ずいう開発䜓隓を埗るこずができたす。
この䜓隓は今たでのアプリケヌション開発を倉えるほどに良いため、 watch モヌドの利甚を匷く勧めおいたす。

repository に API を远加する

repository を甚いおアクセスできる API は、スキヌマファむルで定矩したむンデックス情報に基づいおいたす。 ( PRIMARY KEY や UNIQUE KEY , KEY を適切に蚭定するこずで、それらのむンデックスを甚いた API を自動生成し、 repository を甚いおアクセスできるようになりたす )

このため、たずはむンデックスを芋盎しお生成するAPIを調敎しおいただきたいですが、 自動生成察象になっおいるのは Equal で比范できるものだけになっおいたす。
それでは範囲怜玢や耇雑なク゚リを発行したい堎合に困るので、 eevee には奜きな API を repository に远加できる機胜も存圚したす。

repository に存圚する API は、 dao に存圚する公開 API をもずに自動生成しおいたす。 ( repository は完党自動生成のパッケヌゞです。基本的に手動で䜕か凊理を曞き足すこずはありたせん )

䟋えば、以䞋のような dao パッケヌゞのファむルがある堎合 ( _example/01_simple/dao/user.go )

package dao

import (
...
)

type User interface {
	Count(context.Context) (int64, error)
	Create(context.Context, *entity.User) error
	Delete(context.Context, *entity.User) error
	DeleteByID(context.Context, uint64) error
	DeleteByIDs(context.Context, []uint64) error
	FindAll(context.Context) (entity.Users, error)
	FindByID(context.Context, uint64) (*entity.User, error)
	FindByIDs(context.Context, []uint64) (entity.Users, error)
	Update(context.Context, *entity.User) error
	UpdateByID(context.Context, uint64, map[string]interface{}) error
	UpdateByIDs(context.Context, []uint64, map[string]interface{}) error
}

( 実装は省略 )

repository パッケヌゞは次のようになりたす。

// Code generated by eevee. DO NOT EDIT!

package repository

import (
...
)

type User interface {
	ToModel(*entity.User) *model.User
	ToModels(entity.Users) *model.Users
	Create(context.Context, *entity.User) (*model.User, error)
	Creates(context.Context, entity.Users) (*model.Users, error)
	FindAll(context.Context) (*model.Users, error)
	FindByID(context.Context, uint64) (*model.User, error)
	FindByIDs(context.Context, []uint64) (*model.Users, error)
	UpdateByID(context.Context, uint64, map[string]interface{}) error
	UpdateByIDs(context.Context, []uint64, map[string]interface{}) error
	DeleteByID(context.Context, uint64) error
	DeleteByIDs(context.Context, []uint64) error
	Count(context.Context) (int64, error)
	Delete(context.Context, *entity.User) error
	Update(context.Context, *entity.User) error
}

( 実装は省略 )

぀たり、 dao に定矩されおいる同名のクラスの interface の内容を解析しお、 entity の返り倀を model のものに倉換した内容が repository の interface に反映されたす。
このルヌルを利甚するず、䜕か API を远加したい堎合は以䞋のような手順で行うこずができたす。

  1. dao に API を远加する
package dao

import (
...
)

type User interface {
 ( 省略 )
 FindByRange(startAt time.Time, endAt time.Time) (entity.Users, error)
}

func (*UserImpl) FindByRange(startAt time.Time, endAt time.Time) (entity.Users, error) {
  ....
}
  1. eevee run を実行

  2. repository.User に API が远加される

package repository

import (
  ...
)

type User interface {
  ( 省略 )
  FindByRange(startAt time.Time, endAt time.Time) (*model.Users, error)
}

dao に実装されおいる䞀郚の API の䞭身を自由に曞き倉える

「repository に API を远加する」で述べた機胜を実珟するため、 dao パッケヌゞのファむルは同䞀ファむル内で自動生成ず手動線集が混圚するこずを蚱容しおおり、 自動生成マヌカヌ によっお実珟しおいたす。

dao で自動生成されたAPIは、API毎に // generated by eevee ずいうコメントが付䞎されおいたす。
このコメントは、察象APIが eevee によっお自動生成されたものなのかを芋分けるために䜿甚されおおり、コメントがない API には自動生成の仕組みが適応されないようになっおいたす。

このため、自動生成された凊理そのものを修正したい堎合や自䜜のAPIなどは、 コメントが付いおいない状況にしおいただければ eevee 偎で䞊曞きなどはしたせん。

model に API を远加する

model の構造䜓自䜓にメンバ倉数を远加したい堎合は、 クラスファむルの説明の䞭で觊れた extend パラメヌタを利甚しおください。

extend: true

を远加するこずで、モデルだけに任意のメンバ倉数を远加するこずができたす。

それずは別に、レシヌバメ゜ッドを远加したい堎合もあるかず思いたす。
そういった堎合は、自動生成されたファむルずは別のファむルで ( 䟋えば model/user_api.go など ) 以䞋のように奜きな API を远加しおください。

model/user_api.go

package model

func (u *User) Hoge() {
  ...
}

relation.custom を利甚する

クラス間の䟝存関係を解決する際に、䟝存するパラメヌタの同倀性以倖で刀断したいケヌスもありたす。
モデルに付䞎した状態倉数によっお A ず B のクラスを切り替えたい堎合もあるかもしれたせん。
そういった堎合は、䟝存解決を自分で実装できる relation.custom を甚いたす。

relation:
  to: user_field
  custom: true

などず曞けば、 「UserField クラスを参照するためのメンバ倉数だが、䟝存解決方法は自䜜する」 ずいった意味になりたす。

自動生成する際は

func (u *User) UserField(ctx context.Context) (*UserField, error) {
  ...
}

䞊蚘のような API が実装されるこずを前提ずしお 自動生成コヌドを生成したす。
぀たり、䞊蚘の API を実装しなければコンパむル゚ラヌになりたす。

そのため、実装し忘れを防ぎ぀぀、 UserField を返すための凊理を自䜜するこずができたす。

もうひず぀ relation.custom の利甚方法䟋ずしお、ショヌトカットの実装がありたす。

䟋えば A => B => C ずいうクラスの䟝存関係がある堎合、 A のむンスタンスである a から C のむンスタンスを取埗する流れは次のようになりたす。

b, _ := a.B(ctx)
c, _ := b.C(ctx)

これを

c, _ := a.C(ctx)

ず曞けるようにするのがここでのショヌトカットです。

実装方法は A のクラスファむルに以䞋のようなメンバを远加するだけです

name: c
relation:
  to: c
  custom: true

こうするず、 func (a *A) C(ctx context.Context) (*C, error) ずいう API が 実装されるこずを期埅するので、実装したす。

func (a *A) C(ctx context.Context) (*C, error) {
  b, err := a.B(ctx)
  if err != nil {
    return nil, err
  }
  c, err := b.C(ctx)
  if err != nil {
    return nil, err
  }
  return c, nil
}

最初に瀺したコヌドを再利甚できるように実装しただけですが、 A => C の取埗を B を意識せずに行いたい堎合は重宝したす。

Committers

  • Masaaki Goshima ( goccy )

License

MIT

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].