46
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ZOZOAdvent Calendar 2023

Day 25

GoのORM決定版 Genをはじめよう

Last updated at Posted at 2023-12-24

はじめに

こんにちは、muff1225です。

2023年も終わりに近づいてきましたね〜
今年もGoたくさん書いて、たくさんリリースできましたか?

私は長らくGoのORMの書き味に悩んでいましたが、今年ようやく一つの答えに辿りつきました。

この記事ではgormのGenの開発者体験が最高だった、ということで感謝の意味を込めてgormのGenを紹介します。

GoのORM

歴史

みなさんはGoのプロジェクトでどんなORMを使ってますか?
GoはORMを使う派 / 使わない派でくっきり分かれている印象です。
これはGoの思想として、標準パッケージを使うことがよしとされている文化的な背景や、まだ他言語に比べ歴史が浅いという背景があり、現時点でデファクトスタンダードなアプローチはありません。
とはいえGoにもORMパッケージはたくさんあり、Active Recordのようなフルパッケージのものであったり、シンプルなクエリを書くための軽量ORMであったりとさまざまです。

GoのORMパッケージ一覧1
image.png

ORMの立ち位置

私はORM使う派で、自分のプロジェクトにはgormxormなどのORMを使ってきました。

私がORMを使う利点は以下です。

  • SQLの値とGoの型との自動マッピング(= タイプセーフにデータを扱える)
  • SQLをGoで扱え、Goの書き味でSQLが書ける(コンテキストスイッチコストの減少)

ただしgormもxormもGoの範囲でできることが限定的で、ORMを使っていてもテーブルのカラム名を書く必要があったり、リレーション構成を覚えておかないと書けない場面が多いんですよね。

gormのサンプルコード

// UserテーブルをStructで定義
type User struct {
	ID            uint `gorm:"primaryKey"`
	Name          string
	PurchaseOrder PurchaseOrder
	CreatedAt     time.Time
	UpdatedAt     time.Time
	DeletedAt     gorm.DeletedAt
}

// PurchaseOrderテーブルをStructで定義(注文情報テーブル)
type PurchaseOrder struct {
	ID        uint `gorm:"primaryKey"`
	UserID    uint
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt
}

func main() {
	// MySQLへ接続
	dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	// Userレコードを定義
	user := User{
		Name: "sample",
	}

	// Insert
	// 生成されるSQL:INSERT INTO users SET name = 'sample'
	db.Create(&user)

	// Select
	// 生成されるSQL:SELECT * FROM users WHERE name = 'sample' LIMIT 1;
	getUser := User{}
	db.Where(&User{Name: "sample"}).First(&getUser) // <---- structを使ってできる(SQL記法がなくてうれしい)

	// Select
	// 生成されるSQL:SELECT * FROM users ORDER BY id DESC;
	getUsers := User{}
	db.Order("age desc").Find(&getUsers) // <---- SQL記法登場

	// PurchaseOrderレコードを定義
	purchaseOrder := PurchaseOrder{
		UserID: user.ID,
	}

 	// Insert
	// 生成されるSQL:INSERT INTO purchase_orders SET user_id = 1;
	db.Create(&purchaseOrder)

	// Select(UserとPurchaseOrderを取得)
	// 生成されるSQL:SELECT * FROM users JOIN purchase_orders ON ON purchase_orders.user_id = users.id WHERE name = 'sample'
	db.Joins("JOIN purchase_orders ON purchase_orders.user_id = users.id").Where(&User{Name: "sample"}).Find(&getUsers) // <---- SQL記法の登場(複雑なSQLは大体こんな感じで文字列書いていくことになる)
}

シンプルなコードでも割とSQLの記法が求められます。
特に複雑なコードになればなるほど、以下のような記述が増えていきます。

db.Joins("JOIN purchaseOrders ON purchaseOrders.user_id = users.id").Where(&User{Name: "sample"}).Find(&getUsers)

データベースのクライアント開きながらカラム名入力したり、リレーション確認したりして、エディタと脳みその切り替えが発生するんですよね。
Goの書き方 / SQLの書き方が混ざってしまってどっちがどっちか忘れてしまったり。
いい感じなんですが、痒いところが残っている感じ。
これを解決しているのがgormのラッパーとして開発されているgormのGenです。

gormのGenの登場

genとは

今回紹介するgenは、以下の思想で開発が行われています。

## Overview
- Idiomatic & Reusable API from Dynamic Raw SQL
- 100% Type-safe DAO API without `interface{}`
- Database To Struct follows GORM conventions
- GORM under the hood, supports all features, plugins, DBMS that GORM supports

最大の特徴は、型安全であること、SQLをGoの文法で扱えること、データベースから必要な定義ファイルを自動生成できることでしょう。
型安全かつほぼGoの文法の中で扱えるので、型保証しつつ補完機能を使いながらSQLを書けます。
gormで抱えていたGoを書いているのにいつの間にかSQLを書いていた、みたいなことが丸っとなくなります。

さらにデータベースから自動でstructを作ってくれるのでSQLとstructの定義ずれも排除できます。
gormなどが抱えていた痒いところを解決してくれていてめちゃくちゃ書き味が良くなりました。

## 使い方
使い方はシンプルで、自動生成したコードを呼び出すだけです。
サンプルコードを用意しているので動かしながらやってみてください。

自動生成

Genはデータベースからリバースして必要な定義を自動生成してくれます。
Genではバイナリから実行するパターンと、goファイルに定義して実行するパターンがあるのですが、私のプロジェクトではgoファイルに必要な情報を定義しています。

データベースからModelの生成

テーブル名、フィールド名、型、インデックス情報など必要な情報はまるっとGenで生成できます。
ただし一点だけ手動対応が必要なところがありまして、テーブルの親子関係をmodelに持たせる場合、自動生成用にコードの仕込みが必要です。
私のプロジェクトでは、基本的な定義を生成するコマンドと、テーブルの親子関係を設定するコマンドの2つを用意して動かしています。

以下、生成用コマンドのファイルです。

基本的な定義を生成するコマンド
package main

import (
	"os"

	"github.com/muff1225/gorm-gen-sample/app/interface/db"
	"gorm.io/gen"
)

func main() {
	g := gen.NewGenerator(gen.Config{
		OutPath:           "./interface/db/query", // 出力パス
		Mode:              gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface,
		FieldWithIndexTag: true,
		FieldWithTypeTag:  true,
		FieldNullable:     true,
	})

	sqlHandler, err := db.NewSqlHandler(os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_PORT"), os.Getenv("MYSQL_DATABASE"))
	if err != nil {
		panic(err)
	}

	g.UseDB(sqlHandler.Conn)
	all := g.GenerateAllTable() // database to table model.

	g.ApplyBasic(all...)

	// Generate the code
	g.Execute()
}
テーブルの親子関係を設定するコマンド
package main

import (
	"os"

	"github.com/muff1225/gorm-gen-sample/app/interface/db"
	"github.com/muff1225/gorm-gen-sample/app/interface/db/model"
	"gorm.io/gen"
	"gorm.io/gen/field"
)

func main() {
	g := gen.NewGenerator(gen.Config{
		OutPath:           "./interface/db/query", // 出力パス
		Mode:              gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface,
		FieldWithIndexTag: true,
		FieldWithTypeTag:  true,
		FieldNullable:     true,
	})

	sqlHandler, err := db.NewSqlHandler(os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_PORT"), os.Getenv("MYSQL_DATABASE"))
	if err != nil {
		panic(err)
	}

	g.UseDB(sqlHandler.Conn)

	// Generate the code
	g.Execute()

	// 生成したModelにRelation情報を手動追加(これだけは手動対応が必要)
	allModels := []interface{}{
		g.GenerateModel(
			model.TableNameUser,
			gen.FieldRelateModel(field.BelongsTo, "PurchaseOrder", model.PurchaseOrder{}, nil),
		),
	}
	g.ApplyBasic(allModels...)

	// Generate the code
	g.Execute()
}

この2種類のコマンドで、modelとqueryという2種類のファイルが自動生成されます。

自動生成されるmodel

modelは名の通り、テーブル定義をsturctに落とし込んだものです。

User
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.

package model

import (
	"time"

	"gorm.io/gorm"
)

const TableNameUser = "users"

// User mapped from table <users>
type User struct {
	ID            int32          `gorm:"column:id;type:int unsigned;primaryKey;autoIncrement:true" json:"id"`
	Name          string         `gorm:"column:name;type:varchar(255);not null" json:"name"`
	CreatedAt     time.Time      `gorm:"column:created_at;type:datetime(3);not null;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
	UpdatedAt     time.Time      `gorm:"column:updated_at;type:datetime(3);not null;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
	DeletedAt     gorm.DeletedAt `gorm:"column:deleted_at;type:datetime(3)" json:"deleted_at"`
	PurchaseOrder PurchaseOrder  `json:"purchase_order"`
}

// TableName User's table name
func (*User) TableName() string {
	return TableNameUser
}

PurchaseOrder
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.

package model

import (
	"time"

	"gorm.io/gorm"
)

const TableNamePurchaseOrder = "purchase_orders"

// PurchaseOrder mapped from table <purchase_orders>
type PurchaseOrder struct {
	ID        int32          `gorm:"column:id;type:int unsigned;primaryKey;autoIncrement:true" json:"id"`
	UserID    int32          `gorm:"column:user_id;type:int unsigned;not null;index:fk_purchase_orders_user_id,priority:1" json:"user_id"`
	CreatedAt time.Time      `gorm:"column:created_at;type:datetime(3);not null;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
	UpdatedAt time.Time      `gorm:"column:updated_at;type:datetime(3);not null;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:datetime(3)" json:"deleted_at"`
}

// TableName PurchaseOrder's table name
func (*PurchaseOrder) TableName() string {
	return TableNamePurchaseOrder
}

自動生成されるquery

queryは自動補完や関数の操作感を高めるため、テーブルごとに独立したinterfaceを提供しています。
Goの中でSQLを書く場合、このqueryに定義されているinterfaceを呼び出してSQLを構築します。
生成されるコードが長いので割愛します。

Userのquery

Genを使ってSQLを実行する

自動生成されたmodelとqueryを使ってSQLを実行します。
前述のサンプルコードをGenに書き換えました。

Genのサンプルコード

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/muff1225/gorm-gen-sample/app/interface/db"
	"github.com/muff1225/gorm-gen-sample/app/interface/db/model"
	"github.com/muff1225/gorm-gen-sample/app/interface/db/query"
)

func main() {
    // MySQLへ接続
	sqlHandler, err := db.NewSqlHandler(os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_PORT"), os.Getenv("MYSQL_DATABASE"))
	if err != nil {
		fmt.Println(err.Error())
	}

	qu := query.Use(sqlHandler.Conn)

	ctx := context.Background()
	userQuery := qu.User
	purchaseOrderQuery := qu.PurchaseOrder

	// Insert
	// 生成されるSQL:INSERT INTO users SET name = 'sample'
	user := &model.User{
		Name: "sample",
	}
	err = qu.Transaction(func(tx *query.Query) error {
		if err := userQuery.WithContext(ctx).Create(user); err != nil {
			return err
		}

		return nil
	})
	if err != nil {
		fmt.Println(err)
		return
	}

	// Select
	// 生成されるSQL:SELECT * FROM users WHERE name = 'sample' LIMIT 1;
	userResult, err := userQuery.WithContext(ctx).Where(userQuery.Name.Eq("sample")).First()
	if err != nil {
		fmt.Println(err)
		return
	}

	// Select
	// 生成されるSQL:SELECT * FROM users ORDER BY id DESC;
	userResults, err := userQuery.WithContext(ctx).Order(userQuery.ID.Desc()).Find()
	if err != nil {
		fmt.Println(err)
		return
	}

	// PurchaseOrderレコードを定義
	purchaseOrder := &model.PurchaseOrder{
		UserID: userResult.ID,
	}

	// Insert
	// 生成されるSQL:INSERT INTO purchase_orders SET user_id = 1;
	err = qu.Transaction(func(tx *query.Query) error {
		if err := purchaseOrderQuery.WithContext(ctx).Create(purchaseOrder); err != nil {
			return err
		}

		return nil
	})
	if err != nil {
		fmt.Println(err)
		return
	}

	// Select(UserとPurchaseOrderを取得)
	// 生成されるSQL:SELECT * FROM users JOIN purchase_orders ON purchase_orders.user_id = users.id WHERE name = 'sample'
	purcahseOrderResults, err := purchaseOrderQuery.WithContext(ctx).Join(user, userQuery.ID.EqCol(purchaseOrderQuery.UserID)).Where(userQuery.Name.Eq("sample")).Find()
	if err != nil {
		fmt.Println(err)
		return
	}
}

構文の比較

GenではQueryで定義されている関数をチェインしていくことで表現が可能です。
それぞれ比較で並べてみました。

SELECTの違い

gorm
db.Where(&User{Name: "sample"}).First(&getUser)
Gen
userQuery.WithContext(ctx).Where(userQuery.Name.Eq("sample")).First()

INSERTの違い

gorm
user := User{
    Name: "sample",
}
db.Create(&user)
Gen
user := &model.User{
    Name: "sample",
}
err = qu.Transaction(func(tx *query.Query) error {
    if err := userQuery.WithContext(ctx).Create(user); err != nil {
        return err
    }

    return nil
})

JOINの違い

gorm
db
.Joins("JOIN purchase_orders ON purchase_orders.user_id = users.id")
.Where(&User{Name: "sample"})
.Find(&getUsers)
Gen
purchaseOrderQuery.WithContext(ctx)
.Join(user, userQuery.ID.EqCol(purchaseOrderQuery.UserID))
.Where(userQuery.Name.Eq("sample"))
.Find()

その他の機能

Genはサブクエリやバッチインサートなども当然同じ書き味で使えます。

var users = []*User{{Name: "modi_1"}, ...., {Name: "modi_10000"}}

// batch size 100
query.User.WithContext(ctx).CreateInBatches(users, 100)

その他、Genの書き方はドキュメントで網羅されているので、こちらもチェック!
Genのドキュメント

サンプルコード

今回使ったサンプルコードをGithubに置きました。
Dockerで動くようにしているのですぐに試せると思います。
ぜひGenのドキュメントを片手に、2024年はGenにトライしてみてください〜

さいごに

もしこの記事で読んで、参考になった!と思ったらぜひ いいね をお願いいたします :pray:
みなさん良いお年を〜!

  1. awesome-go-ormsより抜粋。d-tsujiさん、ありがとうございます!

46
28
1

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
46
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?