こちらの続きです。
今回は GORM を使ってデータベース操作とマイグレーションを行う方法について書きます。

GORM は Golang 用の ORM です。
公式ドキュメントがかなり細かく書かれているので、使い方で詰まってもそこを見れば大体解決できます。

マイグレーション機能についても提供されているものの、カラム削除ができないといった制限もあります。
そのため、他言語やフレームワークでよくあるファイルベースのマイグレーションとして sql-migrate を使う例についても紹介します(実際開発中のプロダクトではこちらを使っています)。

利用するリポジトリ

前回に引き続き以下を利用します。

何をやっているのか

前述のとおり公式ドキュメントを見れば使い方は大体わかるので、ここでは最低限の紹介にとどめます。

GORM は ORM なので、構造体とデータのマッピングや CRUD の実行など、DB 操作は一通りできます。

type userRepository struct{}

// 検索の例
// First にポインタを渡すことでデータをマッピングできる
func (r userRepository) Find(id uint, u *entity.User) error {
    return dbManager.Where(&entity.User{ID: id}).First(u).Error
}

// 挿入の例
func (r userRepository) Create(u *entity.User) error {
    return dbManager.Create(u).Error
}

また、構造体のフィールドとカラムのマッピングには gorm タグを使います。

// ユーザデータを保持するための構造体
type User struct {
    ID          uint       `gorm:"primary_key" json:"id"`
    Name        string     `gorm:"column:name,type:varchar(20);not null"` // column で明示しない場合はフィールド名をスネークケースに変えたものをカラムとして使う
    Tokens      []Token    // 関連データもマッピング可能
}

// テーブル名については、構造体を複数形にしたものをデフォルトで使う
// 構造体に TableName 関数を追加することで上書き可能
func (User) TableName() string {
    return "my_users"
}

マイグレーションについては AutoMigrate 関数をコールすることで実行してくれます。

// app/infrastructure/database/database.go
// コードは抜粋です

// DB に接続
dbManager, err = gorm.Open(mysql.New(mysql.Config{
    DSN: dsn,
}), &gorm.Config{
    Logger: logger.Default.LogMode(logMode),
})

// マイグレーション実行
// 実行したい構造体を列挙していく
if err := dbManager.AutoMigrate(entity.Token{}, entity.User{}); err != nil {
    return err
}

sql-migrate を使う場合、Dockerfile の以下コードのコメントアウトをはずす必要があります。

RUN go install github.com/rubenv/sql-migrate/...@v1.1.1

sql-migrate を使ったマイグレーションについては上記リポジトリにコードが無いので、簡単に使い方を説明しておきます。
基本的なコマンドは以下のとおりです。

$ sql-migrate new {マイグレーション名} # タイムスタンプ+マイグレーション名をファイル名にしてファイルが生成される
$ sql-migrate up # マイグレーション実行
$ sql-migrate down # ロールバック
$ sql-migrate up -env test # 環境を指定して実行(デフォルトは development)

まず、DB への接続情報を定義した yaml ファイルを用意します。
変数も利用可能なので、秘匿情報をべた書きしなくて済みます。

# dbconfig.yml
# 開発用
development:
  dialect: mysql
  dir: db/migrations
  datasource: ${DB_USER}:@tcp(${DB_HOST})/${DB_NAME}?charset=utf8mb4&collation=utf8mb4_general_ci&parseTime=true&loc=Asia%2FTokyo
  table: migrations

# テスト用
test:
  dialect: sqlite3
  datasource: test.db
  dir: db/migrations_sqlite3

# 本番用
production:
  dialect: mysql
  dir: db/migrations
  datasource: ${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST})/${DB_NAME}?charset=utf8mb4&collation=utf8mb4_general_ci&parseTime=true&loc=Asia%2FTokyo
  table: migrations

各環境の設定で対象ディレクトリを指定できるので、(例えば RDBMS の種類が違うなどで)環境ごとにマイグレーションファイルを変えるといったことも可能です。
マイグレーションファイルは SQL ファイルで書き、+migrate up/down というコメントで実行するクエリを判断します。

-- +migrate Up
create table if not exists `users` (
  `id` int unsigned not null auto_increment,
  `created_at` datetime not null default current_timestamp,
  `updated_at` datetime on update current_timestamp,
  `name` varchar(20) not null comment '名前',
  primary key(id)
) comment='ユーザ'; -- セミコロンで区切れば、一度のマイグレーションで複数のクエリを実行可能

-- +migrate Down
drop table if exists `users`;

最後に、前述のコマンドを実行してマイグレーションを実行します。

まとめ

今回はデータベースまわりについてざっくり説明しました。
GORM を最初に使ったのは 2017 年ごろだったと思いますが、当時から色々なことができて感動した記憶があります。
DB 操作は GORM だけで完結できますが、今回紹介したように特定の分野だけ別ツールを使うといったことも可能なので、プロダクトに合った組み合わせを探すのが良いと思います。

次回は CI について書きます。

※2022/5/7追記
こちらにて続編を書きました。