every Tech Blog

株式会社エブリーのTech Blogです。

トモニテ相談室での API サーバーの開発環境で sqlboiler と oapi-codegen を導入してみた

はじめに

こんにちは トモニテ でバックエンド周りの開発を行っている rymiyamoto です。

今回はトモニテの新規事業として、2023 年 11 月 30 日にローンチした家族・家庭や恋愛に対する悩みをプロのカウンセラーと相談出来る新サービス トモニテ相談室 の開発に関わる中で API サーバーの開発環境での工夫について紹介していきます。

トモニテ相談室の通話周りの基盤の話は別の記事で紹介されていますので、こちらも合わせてご覧いただけると幸いです。

tech.every.tv

技術選定

新規でサービスを立ち上げるに当たっての技術選定は 2 点を意識して選びました

  1. できるだけ既存の技術をを使う
  2. 面倒なところは楽できるようにする

できるだけ既存の技術をを使う

こちらに関しては基本的にはトモニテの技術スタックを踏襲することにしました。

  • 使用言語: Go
  • フレームワーク: echo
  • インフラ: AWS ECS

どちらも現在のトモニテだけに関わらず社内での技術スタックとして定着しているものを選びました。

面倒なところは楽できるようにする

トモニテ本体だとリリースしてから大きく内部のツールの変更はしていないままで、以下のようなアジリティが悪いところがありました。

  • テーブル定義後のモデル定義が手動で、dbr だと eager loading が面倒
  • エンドポイントのドキュメント定義がソースからの定義で乖離しやすく扱いづらい(echo-swagger)

そのため、今回はトモニテ本体にも後々展開もしやすくある程度ある活発なパッケージを選んでいます。

開発(ローカル)環境

ローカル環境は docker に固めて開発を行っており、go のイメージに対して、以下のようなツールを入れています。

ツール 用途
air ホットリロード
sql-migrate マイグレーション
sqlboiler ORM
oapi-codegen ドキュメント駆動開発
mockgen モック生成
gofumpt フォーマット
golangci-lint 静的解析
ARG GO_VERSION=1.21.4

FROM golang:${GO_VERSION} AS dev

RUN go install github.com/cosmtrek/air@latest
RUN go install github.com/rubenv/sql-migrate/...@latest
RUN go install github.com/volatiletech/sqlboiler/v4@v4.15.0
RUN go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-mysql@v4.15.0
RUN go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.13.4
RUN go install go.uber.org/mock/mockgen@v0.2.0
RUN go install mvdan.cc/gofumpt@latest
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2
COPY .air.toml /
CMD ["air", "-c", ".air.toml"]

モデル生成(sqlboiler)の活用

sqlboiler はスキーマファーストなので、一般的な ORM とは異なりソースからモデルを生成するのではなく、テーブルからモデルを生成を行うことができます。

そのため前もって DB にマイグレーションをかけておくことでモデルを生成することができます。
(今回は トモニテでも運用している sql-migrate でマイグレーションを管理することにしました)

sqlboiler を使用するにあたって DB 情報や出力先を config.toml に記載しておきます

# 生成コマンド実行時に既存のディレクトリを削除するかどうか
wipe     = true
output   = "internal/models"
no-tests = true
templates = [
  "/go/pkg/mod/github.com/volatiletech/sqlboiler/v4@v4.15.0/templates/main"
]

[mysql]
  dbname  = "example"
  host    = "db"
  port    = 3306
  user    = "root"
  pass    = "password"
  sslmode = "false"
  blacklist = ["gorp_migrations"]

ここまで準備できてコマンドを実行するとモデルが生成されます。 (そのままコマンドを実行すると毎回 docker compose exec app XXX と打たないといけないので make コマンドを用意しています。)

# model生成
.PHONY: model
model: model
    docker compose exec app sqlboiler mysql --config ./asset/sqlboiler/config.toml --add-soft-deletes
$ make model

こうすることで、internal/models 配下に DB の制約に応じたリレーションを考慮したモデルが生成されて利用ができるようになります。

ORM としての使い方は公式ドキュメントを参照してください。

ドキュメント駆動開発(oapi-codegen)の活用

oapi-codegen は OpenAPI からコードを生成するツールです。

OpenAPI の定義を api/app.yaml に記載しておきます。 (記述は一般的な OpenAPI の構文が使えます)

openapi: 3.1.0
info:
  title: Example API
  version: 1.0.0
servers:
  - url: http://localhost:1323/1.0/app
paths:
  /notifications:
    get:
      tags:
        - notifications
      summary: お知らせ一覧を取得
      operationId: getNotifications
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Notification"
components:
  schemas:
    Notification:
      type: object
      required:
        - id
        - title
        - body
      properties:
        id:
          type: integer
        title:
          type: string
        body:
          type: string

ここまで準備できてコマンドを実行するとコード (エンドポイントの定義) が生成されます。

# openapiからのコード生成
.PHONY: openapi
openapi: openapi
    docker compose exec app oapi-codegen -generate server -package api -o ./internal/app/api/openapi_api.gen.go api/app.yml
    docker compose exec app oapi-codegen -generate types -package api -o ./internal/app/api/openapi_types.gen.go api/app.yml
$ make openapi

こうすることで、internal/app/api 配下に OpenAPI に従ったエンドポイントの定義が生成されます。 その後 RegisterHandlersWithBaseURL(自動作成される関数)を main.go で呼び出してエンドポイントの登録を行うことで、エンドポイントの定義が完了するので、実際の処理を handler に書くだけで実装が完了します。

エンドポイントへの登録

internal/app/server.go

package app

import (
    "github.com/rymiyamoto/server/internal/app/api"
    "github.com/rymiyamoto/server/internal/app/handler"
    "github.com/labstack/echo/v4"
)

type server struct {
    *handler.Handler
}

func newServer() *server {
    return &server{
        handler.NewHandler(), // 内部での依存解決は handler に任せる
    }
}

func RegisterHandlers(e *echo.Echo, basePath string) {
    server := newServer()
    api.RegisterHandlersWithBaseURL(e, server, basePath)
}

main.go

package main

import (
    "github.com/rymiyamoto/server/internal/app"
    "github.com/labstack/echo/v4"
)

func main() {
    e := echo.New()
    // ...
    app.RegisterHandlers(e, "/1.0/app")
    e.Start(":1323")
}

まとめ

基本的な大枠(言語やフレームワーク)はトモニテの技術スタックを踏襲しつつ、面倒なところは楽できるようにすることで開発効率を上げることができました。

導入当初はいくらツールの歴史があるといえどキャッチアップに時間がかかることもありますが、今後の開発効率向上には必要な投資だと考えています。
また、今回はローカル環境での開発を docker に固めて行っていますが、これにより開発環境の差異をなくすことができ、開発者の環境構築にかかる時間を削減することができました。

今後もトモニテ相談室の開発においては、技術選定や開発環境の構築についても工夫を重ねていきたいと考えています。