Movatterモバイル変換


[0]ホーム

URL:


Future Tech Blog
フューチャー技術ブログ
Programmingカテゴリ

SQLファイルから型安全なコードを生成するsqlc

TIGの辻です。GoのORマッパー連載8日目です。本記事ではsqlc を紹介します。早速ですが、結論から行きましょう。

sqlc まとめ

  • SQLファイルからデータベースにアクセスできる型安全なGoのコードを生成するライブラリ
    • 構造体のモデルの手書き実装不要
    • 複数テーブルをJOINしたときのマッパー実装不要
    • 生成されるコードは不要なリフレクションなし

SQLをがんがん書きたい、でも面倒なマッパー構造体は書きたくない、という開発者にとっては大きな味方になります。

sqlc の紹介

sqlc はSQLファイルからGoのアプリケーションコードを生成するライブラリです。2020/2にv1.0.0 をリリースし、着々とスターを伸ばしています。2021/08現在はv1.8.0 をリリースしています。本資料で生成しているコードもv1.8.0 を用いています。

https://star-history.t9t.io/#kyleconroy/sqlc

2021/08現在ではMySQLとPostgreSQLの2つのデータベースをサポートしています。

データベースのパーサを適用してクエリを解析している点が設計上の大きな特徴です。解析エンジンがPostgreSQLの場合、実際のPostgreSQLサーバーのソースをcgo を経由して、Goから呼び出せるようになっています。PostgreSQLのクエリ解析エンジン本体はpganalyze/pg_query_go が提供しています。

ひとたび以下のようなSQLを実装すれば、sqlc generate コマンドを実行することで、型安全なGoのアプリケーションコードが生成できます。SQLファイルは複数に分割することもできます。ユースケースごとにSQLファイルを分ける、といった使い方ができるでしょう。

-- name: GetAuthor :one
SELECT*FROM author
WHERE id= $1 LIMIT1;

-- name: ListAuthors :many
SELECT*FROM author
ORDERBY id;

-- name: CreateAuthor :one
INSERTINTO author (id, name)VALUES ($1, $2) RETURNING*;

-- name: DeleteAuthor :exec
DELETEFROM author
WHERE id= $1;

-- name: ListBookOverPrice :many
SELECT
b.title
,a.name
,b.price
FROM
book b
LEFTJOIN
author a
ON1=1
AND b.author_id= a.id
WHERE
price> $1
ORDERBY
b.title
;

※データベースのスキーマ例

本記事ではデータベースはPostgreSQLとします。

createtable author
(
idintegerPRIMARY KEY,
namevarchar(99)notnull,
created_attimestampnotnulldefault now()
);

createtable book
(
idintegerPRIMARY KEY,
titlevarchar(99)notnull,
priceintegernotnull,
author_idintegernotnull,
created_attimestampnotnulldefault now()
);

altertable bookaddforeign key (author_id)references author (id);

sqlc の作者が書いている記事Introducing sqlc - Compile SQL queries to type-safe Go の中にあるHow to use sqlc in 3 steps という謳い文句に嘘はないです。とてもシンプル。

  • SQLのクエリを書く
  • sqlc コマンドを実行して、クエリに対する型安全性の高いインタフェースを提供するGoのコードを生成する
  • sqlc で生成したメソッドを呼び出すアプリケーションコードを書く

実際に上のSQLファイルに対してsqlc generate コマンドを実行すると以下のようなGoのコードが生成されます。

生成されたSQLファイル

  • db.go
  • models.go
  • query.sql.go
db.go
// Code generated by sqlc. DO NOT EDIT.

package db

import (
"context"
"database/sql"
)

type DBTXinterface {
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
}

funcNew(db DBTX) *Queries {
return &Queries{db: db}
}

type Queriesstruct {
db DBTX
}

func(q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}
models.go
// Code generated by sqlc. DO NOT EDIT.

package db

import (
"time"
)

type Authorstruct {
IDint32
Namestring
CreatedAt time.Time
}

type Bookstruct {
IDint32
Titlestring
Priceint32
AuthorIDint32
CreatedAt time.Time
}
query.sql.go
// Code generated by sqlc. DO NOT EDIT.
// source: query.sql

package db

import (
"context"
)

const createAuthor =`-- name: CreateAuthor :one
INSERT INTO author (id, name) VALUES ($1, $2) RETURNING id, name, created_at
`

type CreateAuthorParamsstruct {
IDint32
Namestring
}

func(q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author,error) {
row := q.db.QueryRowContext(ctx, createAuthor, arg.ID, arg.Name)
var i Author
err := row.Scan(&i.ID, &i.Name, &i.CreatedAt)
return i, err
}

const deleteAuthor =`-- name: DeleteAuthor :exec
DELETE FROM author
WHERE id = $1
`

func(q *Queries) DeleteAuthor(ctx context.Context, idint32)error {
_, err := q.db.ExecContext(ctx, deleteAuthor, id)
return err
}

const getAuthor =`-- name: GetAuthor :one
SELECT id, name, created_at FROM author
WHERE id = $1 LIMIT 1
`

func(q *Queries) GetAuthor(ctx context.Context, idint32) (Author,error) {
row := q.db.QueryRowContext(ctx, getAuthor, id)
var i Author
err := row.Scan(&i.ID, &i.Name, &i.CreatedAt)
return i, err
}

const listAuthors =`-- name: ListAuthors :many
SELECT id, name, created_at FROM author
ORDER BY id
`

func(q *Queries) ListAuthors(ctx context.Context) ([]Author,error) {
rows, err := q.db.QueryContext(ctx, listAuthors)
if err !=nil {
returnnil, err
}
defer rows.Close()
var items []Author
for rows.Next() {
var i Author
if err := rows.Scan(&i.ID, &i.Name, &i.CreatedAt); err !=nil {
returnnil, err
}
items =append(items, i)
}
if err := rows.Close(); err !=nil {
returnnil, err
}
if err := rows.Err(); err !=nil {
returnnil, err
}
return items,nil
}

const listBookOverPrice =`-- name: ListBookOverPrice :many
SELECT
b.title
,a.name
,b.price
FROM
book b
LEFT JOIN
author a
ON1 = 1
AND b.author_id = a.id
WHERE
price > $1
ORDER BY
b.title
`

type ListBookOverPriceRowstruct {
Titlestring
Namestring
Priceint32
}

func(q *Queries) ListBookOverPrice(ctx context.Context, priceint32) ([]ListBookOverPriceRow,error) {
rows, err := q.db.QueryContext(ctx, listBookOverPrice, price)
if err !=nil {
returnnil, err
}
defer rows.Close()
var items []ListBookOverPriceRow
for rows.Next() {
var i ListBookOverPriceRow
if err := rows.Scan(&i.Title, &i.Name, &i.Price); err !=nil {
returnnil, err
}
items =append(items, i)
}
if err := rows.Close(); err !=nil {
returnnil, err
}
if err := rows.Err(); err !=nil {
returnnil, err
}
return items,nil
}

上記ではdb パッケージとして生成されました。パッケージ名はsqlc設定ファイルで調整できます。

アプリケーション実装例

sqlc が生成したコードを使うアプリケーションの実装例は以下のような感じです。

main.go
package main

import (
"context"
"database/sql"
"fmt"

"github.com/d-tsuji/go-sandbox/sqlc/db"
_"github.com/jackc/pgx/v4/stdlib"
)

funcmain() {
pgx, err := sql.Open("pgx","postgres://booktest:pass@localhost:15432/testdb?sslmode=disable")
if err !=nil {
panic(err)
}
ctx := context.Background()
q := db.New(pgx)

// -----------------------------------------------------------
// create user
param := db.CreateAuthorParams{
ID:104,
Name:"Daishiro Tsuji",
}
u, err := q.CreateAuthor(ctx, param)
if err !=nil {
panic(err)
}
fmt.Println(u)
// {104 Daishiro Tsuji 2021-08-02 08:53:51.40108 +0000 UTC}

// get user
u, err = q.GetAuthor(ctx,101)
if err !=nil {
panic(err)
}
fmt.Println(u)
// {101 Mat Ryer 2021-08-02 08:53:44.580572 +0000 UTC}

// delete user
if err := q.DeleteAuthor(ctx,104); err !=nil {
panic(err)
}

// list user
ls, err := q.ListBookOverPrice(ctx,3500)
if err !=nil {
panic(err)
}
for _, l :=range ls {
fmt.Println(l)
}
// {Go言語でつくるインタプリタ Thorsten Ball 3740}
// {Go言語によるWebアプリケーション開発 Mat Ryer 3520}
}

個人的に特に嬉しいポイント

  • クエリベースでコード生成可能

データベースに対して発行するSQLのSELECT文は、経験上、複数のテーブルをJOINすることが多く、また、複雑になりがちです。またデータベースクライアントでデータベースに接続し、実際にクエリを発行し、実行計画を確認しながらクエリの性能をチェックすることが多いです。

SQLを書いてしまうことが多く、記述したSQLをもとに型安全なGoのアプリケーションコードを生成できるのはかなり嬉しいポイントです。

  • 自作のマッパー構造体不要

また、他のO/Rマッパを使った場合、モデルのコードがテーブルベースであることが多く、生のSQLをO/Rマッパに実装したとしても、結果を取得するマッパーのモデルはクエリ個別に作ることが必要になることもあります。こうしたSELECT文におけるマッパーが不要な点もsqlc を使う嬉しいポイントと言えます。

  • O/Rマッパライブラリ不要

生成されたコードを用いることで直接クエリの結果を取得できます。すなわち、database/sql パッケージを直接用いることでO/Rマッパライブラリは不要となります。

参考

目次

  1. sqlc まとめ
  2. sqlc の紹介
    1. 生成されたSQLファイル
    2. アプリケーション実装例
  3. 個人的に特に嬉しいポイント
  4. 参考

カテゴリー


[8]ページ先頭

©2009-2025 Movatter.jp