はじめに
こんにちは!今回はGoのSQLライブラリ「sqlc」についての記事を書こうと思います。
(記事作成の動機の話 ... )この夏参加した2週間のインターンで、チーム開発に挑戦しました(後日記事を書きます)。その際、データベースとのやり取りを効率化するために、チームで採用したのが 「sqlc」 です。チームでは sqlc を使って、手書きのSQLを活かしながら型安全なGoコードを自動生成し、機能実装を進めました。(冒頭から若干のネタバレ)
実際に使用してみて感じたのは、その快適さ。SQLの柔軟性を保ちながら、型安全なコードが生成されるという点で、個人的にも「これは今後のプロジェクトでもぜひ使いたい!」と思いました。
とはいえ、ORM/SQLだけでも世の中にはいろんな種類のライブラリが存在しており、エンジニアはこれを適切に選定しなくてはなりません(ということをインターンで学びました)。そこで、今回は他のORM/SQLライブラリについても触れ、比較を行っていきたいと思います。(内容に誤りがあればご指摘ください🙇)
本編では、私自身が経験した sqlc の利便性や、実際の使用感を共有しつつ、どのようにしてこのツールがGoの開発を効率化するのかを中心ご紹介します。開発効率を高めたい方や、SQLの操作をもっと手軽にしたい方は、ぜひ参考にしてみてください!
(ORMについて復習したい方向け)
1. GoにおけるORM・SQLライブラリの比較
Goのアプリケーション開発において、データベースとのやり取りを効率化するために、さまざまなORMやSQLライブラリが存在します。それぞれにメリット・デメリットがありますので、まずは以下のライブラリについて、それぞれの公式チュートリアル等を読みながら、特徴を比較してみました。
ORM・SQLライブラリの例
1. database/sql
-
特徴:
-
メリット:
- ORMと違い、SQLクエリを自由に記述できるため、複雑なクエリやパフォーマンスに敏感なクエリを直接最適化することが可能(カスタマイズの余地が非常に大きい)
- Goの公式標準ライブラリであるため、依存関係が少なく、他のサードパーティライブラリに頼らず使用できる
-
デメリット:
- SQLクエリを手書きで記述する必要がある
- そのため、特に複雑なクエリが多い場合や、アプリケーションが大規模になるにつれて、SQLクエリのメンテナンスが手間になる
- データベースから返ってきた結果をGoの構造体に手動でマッピングする必要がある(特に、大量のフィールドがある場合や複雑なJOINクエリでは手間)
-
参考
2. sqlx
-
特徴:
-
database/sql
パッケージの拡張ライブラリで、SQLクエリの柔軟性を維持しつつ、データベースから取得したデータの自動マッピングを提供する -
database/sql
が持つ低レベルの接続管理やクエリの実行機能に加えて、struct へのデータのバインディングや、複雑なSQLクエリ結果の取り扱いを容易にする機能が追加されている - (所感)クエリは直書きしたいけどマッピングがめんどくさい(最小限の機能を取り回しよく使いたい)場合に使いたい
-
-
メリット:
- 手書きのSQLクエリをサポートし、かつクエリ結果をGoのstructに自動でマッピングできる((面倒臭かったところをまるっとやってくれる))
- 例えば、SQLで取得したカラム名と構造体のフィールド名が一致していれば、手動で
rows.Scan()
などのマッピング処理を記述する必要がなく、structへのデータバインディングが簡単に行える
-
デメリット:
-
database/sql
と同様にSQLクエリを手書きで記述する必要がある
-
-
参考
- 2つ目の記事の中で言及されていたsqlxの実装を観察して「標準ライブラリから具体的にどのような拡張がなされているのか」についてのお話が興味深かったです(リンク)
3. gorm
-
特徴:
- 最も有名なORM
- CRUD(Create, Read, Update, Delete)操作をシンプルに記述できる
- スキーママイグレーションやアソシエーション(リレーション)操作など、多くの便利な機能を提供している
-
メリット:
- 学習コストが低い(← ※基本的な機能については)
- 提供している機能の多さ:非常に拡張性が高く、プラグインを通してさらなる機能を追加できる(たとえば、キャッシュ、ソフトデリート、イベントリスナーなど)
- (ex)
AutoMigrate
を使うことで、構造体に基づいてテーブルの作成や変更を自動化できます。これにより、開発中にデータベースのスキーマを手動で管理する手間が省け、アプリケーションの変更に応じてテーブル構造も容易に変更できます
-
ちなみにRailsのRailに乗って今まで開発を行ってきた私にとっては最もとっつきやすく(下記のように、記述が類似している)、Goで初めてアプリケーションを開発している現在はこちらを採用することで負荷を軽減しています(ありがたや)
example.go
// gorm db.Where("age > ?", 20).Order("name desc").Find(&users)
example.rb// ActiveRecord (Rails) User.where("age > ?", 20).order("name desc")
-
デメリット:
- 低レベルなSQLの最適化が難しく、複雑なクエリや大量のデータを扱う場合にパフォーマンスが低下する可能性がある
- 高度なSQLや特殊なクエリを発行する際、gormでできる範囲は限られており、手書きのSQLに頼らなければならない場合もある(あるいは、
gorm
のAPIが冗長になることも)
4. sqlc
-
特徴:
- SQLクエリから自動的に型安全なGoコードを生成する
- 手書きのSQLクエリをそのまま活用しつつ、Goの型と構造体に基づいたコードを自動生成することで、型安全なデータベース操作が可能になる
- SQLクエリを事前に定義しておくことで、コンパイル時にSQLの構造や結果型を検証できる
-
メリット:
- SQLクエリの自由度を維持しながらも、データベースの問い合わせ結果をGoのコードで安心して扱うことができる
- 動的なORMの代わりに静的なSQLを使いたい開発者にとって、柔軟性と型安全性の両方を実現する選択肢となる
-
デメリット:
- 複雑なクエリを扱う際には多少の学習コストがかかる(SQLは書けろ!と自分に言い聞かせる)
- sqlc自体にはデータベースのスキーママイグレーション機能がないため、データベーススキーマの管理は別のツール(例: goose、migrateなど)を使って行う必要がありる(一定手間)
-
参考
(技術選定の結果、同じようにsqlcを採用している方の記事)
まとめ
ライブラリ | 特徴 | メリット | デメリット |
---|---|---|---|
database/sql |
Go標準のSQLライブラリ。クエリを手書きで記述し、データベース操作を直接行う | - 高い柔軟性:SQLを自由に記述可能 - 標準ライブラリで依存関係が少ない |
- SQLの手書きが必要 - マッピングを手動で行う必要がある - 複雑なクエリのメンテナンスが手間 |
sqlx |
database/sql の拡張ライブラリ。クエリは手書きながらも、自動マッピングを提供 |
- SQLクエリの手書きサポート - 構造体への自動マッピングが可能 |
- SQLクエリの手書きが必要 - 複雑なクエリを記述する場合の負担は大きい |
gorm |
有名なORM。CRUD操作の簡略化、スキーママイグレーション、リレーションなど、便利な機能を提供 | - 学習コストが低い - 自動マッピングとCRUD操作が簡単 - スキーママイグレーションのサポート |
- 複雑なクエリや大量データでパフォーマンスが低下することがある - 高度なクエリの記述には限界があり、手書きSQLが必要 |
sqlc |
- SQLからGoコードを自動生成 - 型安全なデータベース操作が可能で、コンパイル時にクエリの型を検証 |
- 型安全なコード生成 - SQLの自由度を保ちながらも、Goの型に基づいた安全な操作が可能 |
- スキーママイグレーション機能がない - 複雑なクエリを扱う際に学習コストがかかる |
補足
- こちらのZennの記事では、GitHub-APIを使用して各種ライブラリの近年の流行を可視化しています(すごい!)
- こちらの記事ではmigration/transaction用のライブラリについても言及されており、ぜひ参考にさせていただきたい
2. 型安全 / 非型安全
型安全なライブラリ(例: sqlc)は、SQLを直接書きつつ型の整合性をコンパイル時に検出できるため、堅牢で保守性の高いコードが作成できます。また、SQLの自由度を維持しつつ型安全なデータ操作が可能です。一方、型安全でないライブラリ(例: gormやsqlx)は、ORMによる簡略化や自動マッピングの恩恵があり、開発スピードが速いですが、ランタイムエラーのリスクやSQLクエリの自由度が低下することがあります。
どちらが「正しい」選択かはプロジェクトの要件やチームのスキル、データベースの複雑さに依存すると考えます。(実際今回参加したインターンでは、開発速度や学習コストと将来的なプロダクトの規模(つまり必要な堅牢性)とを天秤にかけて(厳密には他にも選定理由がありますが)、sqlcのような型安全なライブラリを採用しました。)
以下のように、先ほど列挙したものを含むライブラリを型安全なものと型安全でないものに分けてみました。
型安全なライブラリ
-
sqlc
(先述) -
ent
(https://entgo.io/ja/ , https://zenn.dev/tkb/articles/d1e6e3b7d62051)
型安全でないライブラリ
-
database/sql
- 手動で
rows.Scan
やQuery
メソッドを使用するため、型の不一致や変換エラーが発生するリスクがある
- 手動で
-
sqlx
-
struct
への自動マッピング機能が追加されている(先述) - ただし、クエリの実行時に型のチェックが厳密ではなく、クエリの実行結果とGoの型が一致しない場合にエラーが発生しやすいため、厳密な型安全性は提供されていない
-
-
gorm
-
gorm
はGoの構造体にデータを自動的にマッピングするが、内部的にはリフレクション(reflection)を使ってデータベースの値をGoの型に変換しているため、完全に型安全ではない(特に複雑なクエリやカスタムSQLを使う場合)
-
3. 実際にsqlcを使ってみる
若干前置きが長くなってしまいましたが、いよいよ実際にsqlcを使って、簡単なサンプルコードを生成してみましょう。
前提
インストールが必要な場合、下記のsqlcをインストールします。(brewとかもインストールできます)
$ go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
次に、sqlc.yaml
ファイルを作成し、設定を行います。
version: "2"
sql:
- engine: "mysql"
queries: "query.sql" # SQLクエリを記述したファイルを指定
schema: "schema.sql" # データベースのスキーマを定義したファイルを指定
gen:
go:
package: "db"
out: "db"
schemaファイルの作成
CREATE TABLE authors (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name TEXT NOT NULL,
bio TEXT
);
SQLファイルの作成
-- name: GetAuthor :one
SELECT * FROM authors
WHERE id = ? LIMIT 1;
-- name: ListAuthors :many
SELECT * FROM authors
ORDER BY name;
-- name: CreateAuthor :exec
INSERT INTO authors (
name, bio
) VALUES (
?, ?
);
-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = ?;
コードの生成
次に、以下のコマンドを実行して、Goコードを生成します。
$ sqlc generate
# 確認
$ tree
.
├── db
│ ├── db.go
│ ├── models.go
│ └── query.sql.go
├── query.sql
├── schema.sql
└── sqlc.yml
これにより、SQLクエリに基づいた型安全なGoコードが生成されます。
参考
4. コードの生成結果を観察
前章までで生成したファイルについて記載内容を観察してみましょう。(一部コメントによる解説がなされていますが、それは私が生成後に直接編集したものです)
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package db
import (
"context"
"database/sql"
)
// DBTX はデータベーストランザクションとDBインターフェースの抽象化
// sql.DBやsql.Txなど、クエリ実行時にどちらを使うかを柔軟に対応できる
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
// Newは、DBTXインターフェースを受け取り、Queries構造体を初期化する
// Queries構造体はDB操作メソッドを持つ
func New(db DBTX) *Queries {
return &Queries{db: db}
}
// Queriesは、すべてのクエリをまとめる構造体
// この構造体にはクエリ実行メソッドが含まれる
type Queries struct {
db DBTX
}
// WithTxはトランザクションでクエリを実行するためのメソッド
// sql.TxをDBTXとして受け取り、トランザクションでクエリを実行するQueriesを返す
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package db
import (
"database/sql"
)
// Authorはauthorsテーブルの各行を表す構造体
// SQLのデータ型に対応してGoの型が設定されている
type Author struct {
ID int64
Name string
Bio sql.NullString
}
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: query.sql
package db
import (
"context"
"database/sql"
)
// CreateAuthorクエリ: authorsテーブルに新しい著者を挿入
const createAuthor = `-- name: CreateAuthor :exec
INSERT INTO authors (
name, bio
) VALUES (
?, ?
)
`
type CreateAuthorParams struct {
Name string
Bio sql.NullString
}
func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) error {
_, err := q.db.ExecContext(ctx, createAuthor, arg.Name, arg.Bio)
return err
}
const deleteAuthor = `-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = ?
`
func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteAuthor, id)
return err
}
const getAuthor = `-- name: GetAuthor :one
SELECT id, name, bio FROM authors
WHERE id = ? LIMIT 1
`
func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) {
row := q.db.QueryRowContext(ctx, getAuthor, id)
var i Author
err := row.Scan(&i.ID, &i.Name, &i.Bio)
return i, err
}
const listAuthors = `-- name: ListAuthors :many
SELECT id, name, bio FROM authors
ORDER BY name
`
func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) {
rows, err := q.db.QueryContext(ctx, listAuthors)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Author
for rows.Next() {
var i Author
if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
sqlcを使うと、手書きのSQLクエリから簡単にDB操作のメソッドを作成できるので、開発がすごくスムーズに進みました!今回の記事では基本的なCRUD操作をメインに試しましたが、他にも少し変則的なクエリや複雑な操作にも対応していけるので、今後も色々試していきたいと思います。特に、型安全なコードが自動生成されるのは大きなメリットなので、SQLに慣れている方にはかなりおすすめです。
5. まとめ
今回の記事では、sqlcについて、他のライブラリと比較しながら、その特徴や使用方法について解説しました。sqlcは、手書きのSQLクエリを活かしながら型安全なGoコードを自動生成できる点で非常に強力で、SQLの柔軟性を重視する開発者には特に魅力的なツールです。
実際に使用してみた感想として、簡単にDB操作のメソッドが作成でき、開発の効率が非常に上がったことを実感しています。今回取り上げた基本的な操作だけでなく、今後はもっと複雑なクエリや変則的な操作にも挑戦し、機会があればその経験を記事としてまとめたいと思います。
今夏に参加した複数社でのインターンで得た学びの1つですが、今後の開発では、プロダクトの価値やメンテナンス性を高めるために、開発ツールをどのように活用するか、また、柔軟さと堅牢さ等の様々なトレードオフのどこを取るかといった的確な判断、技術選定が非常に重要だと感じています。プロジェクトの要件に合った最適なツールを選び、スピードと品質を両立させながら、プロダクト全体の成長に貢献していきたいというモチベーションが高まりました。
今後も実践を積みながら、学んだことを共有していきたいと思います。
最後まで読んでいただきありがとうございました。