はじめに
Goでデータベースにアクセスする場合、様々なやり方(ORMを使う、クエリを直接書いて実行する等)がありますが、それぞれ良さと辛みがあると思います。
個人的には、RailsのActiveRecordのようなORMはあまり好きではなく、SQLを書いて実行する事が多かったのですが、それはそれで構造体へのマッピングが面倒といった課題もありました。
今回は、そんな面倒なマッピングなどをいい感じにやってくれるsqlcというライブラリについて、紹介したいと思います。
sqlcとは
sqlcは、SQLファイルからGoの型安全なコードを生成するライブラリです。
SQLからGoのファイルを生成するライブラリはいくつかありますが、それらは、構造体タグを使ったり、手作業によるマッピングが必要だったりしますが、sqlcではこれらの作業が必要ありません。
sqlcの使い方は非常に簡単で、
- なんらかの処理をするSQLを書く
-
sqlc generate
コマンドを実行して、Goのコードを生成する - 生成されたコードを使ってデータベースにアクセスする
たったこれだけです。
コード解析
sqlcでは、コードの生成中に全てクエリとDDLを解析し、テーブル内の全ての列とクエリ内の全ての式の名前と型を把握します。これにより、これらのいずれかが一致しない場合はコードの生成に失敗するので、クエリの実行時までエラーがわからないような状態を防ぐ床ができます。
サポートする言語とデータベース
公式によると、sqlcでは以下の言語とデータベースをサポートしています。
言語 | Plugin | MySQL | PostgreSQL | SQLite |
Go | (built-in) | Stable | Stable | Stable |
Kotlin | sqlc-gen-kotlin | Beta | Beta | Not implemented |
Python | sqlc-gen-python | Beta | Beta | Not implemented |
また、コミュニティにより、F#に対応したプラグインもあるようです。
sqlcでコードを生成する
それでは、実際にsqlcを使ってデータベースにアクセスするコードを生成してみます。
準備
まず初めに、sqlcでコードを生成するための準備として、sqlcのインストールし、設定ファイルを作成します。
インストールの方法はいくつかあるので、詳しくは公式のInstalling sqlcをご覧ください。
設定ファイルは
sqlc.yaml
という名前でproject rootに置いておきます。
1 2 3 4 5 6 7 8 9 | version: "2" sql: - engine: "mysql" queries: "query.sql" schema: "schema.sql" gen: go: package: "bookshelf" out: "bookshelf" |
これらのプロパティの内容は以下のとおりです。
engine | データベースの種類 |
queries | クエリファイルのパス |
schema | DDLファイルのパス |
package | 生成するコードのパッケージ名 |
path | 生成するコードの出力先 |
DDLを書く
スキーマ定義を
schema.sql
に書きます。
1 2 3 4 5 6 7 8 9 10 11 | -- schema.sql CREATE TABLE authors ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL ); CREATE TABLE books ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, author_id INT NOT NULL REFERENCES authors(id) ); |
クエリを書く
それぞれのCRUD処理のクエリを
query.sql
に書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | -- query.sql -- name: GetAuthor :one SELECT * FROM authors WHERE id = ? LIMIT 1; -- name: ListAuthors :many SELECT * FROM authors ORDER BY name; -- name: CreateAuthor :execresult INSERT INTO authors (name) VALUES (?); -- name: UpdateAuthor :exec UPDATE authors SET name = ? WHERE id = ?; -- name: DeleteAuthor :exec DELETE FROM authors WHERE id = ?; -- name: GetBook :one SELECT * FROM books WHERE id = ?; -- name: ListBooks :many SELECT * FROM books ORDER BY title; -- name: CreateBook :execresult INSERT INTO books (title, author_id) VALUES (?, ?); -- name: UpdateBook :exec UPDATE books SET title = ?, author_id = ? WHERE id = ?; -- name: DeleteBook :exec DELETE FROM books WHERE id = ?; |
sqlcでコードを生成するために、各クエリに名前とコマンドを示すコメントを付ける必要があります。
-- name: DeleteBook :exec
これらのannotationについては、詳しくはこちらをご覧ください。
コードを生成する
sqlc generate
コマンドを実行してGoのコードを生成します。
このコマンドを実行すると以下の様にファイルが生成が生成されます。
1 2 3 4 5 6 7 8 9 10 | % tree . . ├── bookshelf │ ├── db.go │ ├── models.go │ └── query.sql.go ├── go.mod ├── query.sql ├── schema.sql └── sqlc.yaml |
生成されたコードの中身は以下のようになっています。
db.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.24.0 package bookshelf import ( "context" "database/sql" ) 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 } func New(db DBTX) *Queries { return &Queries{db: db} } type Queries struct { db DBTX } func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ db: tx, } } |
models.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.24.0 package bookshelf import () type Author struct { ID uint64 Name string } type Book struct { ID uint64 Title string AuthorID uint64 } |
query.sql.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.24.0 // source: query.sql package bookshelf import ( "context" "database/sql" ) const createAuthor = `-- name: CreateAuthor :execresult INSERT INTO authors (name) VALUES (?) ` func (q *Queries) CreateAuthor(ctx context.Context, name string) (sql.Result, error) { return q.db.ExecContext(ctx, createAuthor, name) } const createBook = `-- name: CreateBook :execresult INSERT INTO books (title, author_id) VALUES (?, ?) ` type CreateBookParams struct { Title string AuthorID uint64 } func (q *Queries) CreateBook(ctx context.Context, arg CreateBookParams) (sql.Result, error) { return q.db.ExecContext(ctx, createBook, arg.Title, arg.AuthorID) } const deleteAuthor = `-- name: DeleteAuthor :exec DELETE FROM authors WHERE id = ? ` func (q *Queries) DeleteAuthor(ctx context.Context, id uint64) error { _, err := q.db.ExecContext(ctx, deleteAuthor, id) return err } const deleteBook = `-- name: DeleteBook :exec DELETE FROM books WHERE id = ? ` func (q *Queries) DeleteBook(ctx context.Context, id uint64) error { _, err := q.db.ExecContext(ctx, deleteBook, id) return err } const getAuthor = `-- name: GetAuthor :one SELECT id, name FROM authors WHERE id = ? LIMIT 1 ` // query.sql func (q *Queries) GetAuthor(ctx context.Context, id uint64) (Author, error) { row := q.db.QueryRowContext(ctx, getAuthor, id) var i Author err := row.Scan(&i.ID, &i.Name) return i, err } const getBook = `-- name: GetBook :one SELECT id, title, author_id FROM books WHERE id = ? ` func (q *Queries) GetBook(ctx context.Context, id uint64) (Book, error) { row := q.db.QueryRowContext(ctx, getBook, id) var i Book err := row.Scan(&i.ID, &i.Title, &i.AuthorID) return i, err } const listAuthors = `-- name: ListAuthors :many SELECT id, name 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); 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 } const listBooks = `-- name: ListBooks :many SELECT id, title, author_id FROM books ORDER BY title ` func (q *Queries) ListBooks(ctx context.Context) ([]Book, error) { rows, err := q.db.QueryContext(ctx, listBooks) if err != nil { return nil, err } defer rows.Close() var items []Book for rows.Next() { var i Book if err := rows.Scan(&i.ID, &i.Title, &i.AuthorID); 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 } const updateAuthor = `-- name: UpdateAuthor :exec UPDATE authors SET name = ? WHERE id = ? ` type UpdateAuthorParams struct { Name string ID uint64 } func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) error { _, err := q.db.ExecContext(ctx, updateAuthor, arg.Name, arg.ID) return err } const updateBook = `-- name: UpdateBook :exec UPDATE books SET title = ?, author_id = ? WHERE id = ? ` type UpdateBookParams struct { Title string AuthorID uint64 ID uint64 } func (q *Queries) UpdateBook(ctx context.Context, arg UpdateBookParams) error { _, err := q.db.ExecContext(ctx, updateBook, arg.Title, arg.AuthorID, arg.ID) return err } |
生成されたコードを使う
生成されたコードを使って、データベースにauthorとbookのCRUD処理を書いてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | package main import ( "context" "database/sql" "log" "os" "sqlc-sample/bookshelf" ) func main() { ctx := context.Background() db, err := sql.Open("mysql", "user:password@dbname?parseTime=true") if err != nil { os.Exit(1) } queries := bookshelf.New(db) // create an author _, err = queries.CreateAuthor(ctx, "William Shakespeare") if err != nil { os.Exit(1) } // list all authors authors, err := queries.ListAuthors(ctx) if err != nil { os.Exit(1) } log.Printf("authors: %+v\n", authors) if len(authors) == 0 { os.Exit(1) } // create a book author := authors[0] _, err = queries.CreateBook(ctx, bookshelf.CreateBookParams{ Title: "Romeo and Juliet", AuthorID: author.ID, }) if err != nil { os.Exit(1) } // list all books books, err := queries.ListBooks(ctx) if err != nil { os.Exit(1) } // update a book book := books[0] err = queries.UpdateBook(ctx, bookshelf.UpdateBookParams{ Title: "Othello", AuthorID: author.ID, ID: book.ID, }) if err != nil { os.Exit(1) } // delete a book err = queries.DeleteBook(ctx, book.ID) if err != nil { os.Exit(1) } } |
このように、非常に簡単かつ楽にデータベースにアクセスする処理を書くことができます。
sqlcでの構造体の生成をカスタマイズ
sqlcでコードを生成する際に、生成される型をカスタマイズする方法を紹介します。
手順は以下のとおりです。
- (任意)置き換える際に使用する型を定義する
- (任意)
schema.sql
を修正する -
sqlc.yaml
を修正する(overridesを追記する) -
sqlc generate
で生成する
例として、booksにJSON型のカラムを追加し、それを別途定義したBookData型を使って構造体が生成されるようにします。
置き換えに使用する型の定義
dtoディレクトリを作成し、book_data.goを作成します。ファイルの内容は以下のとおりです。
1 2 3 4 5 6 7 | package dto type BookData struct { Genres []string `json:"genres"` Title string `json:"title"` Published bool `json:"published"` } |
schema.sqlを修正する
booksテーブルにjson型のdataカラムを追加します。
1 2 3 4 5 6 | CREATE TABLE books ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, author_id BIGINT UNSIGNED NOT NULL REFERENCES authors(id), data json NOT NULL # 追加 ); |
sqlc.yamlを修正する
overridesを追加して、books.dataがBookDataとして構造体が生成されるように設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | version: "2" sql: - engine: "mysql" queries: "query.sql" schema: "schema.sql" gen: go: package: "bookshelf" out: "bookshelf" overrides: # 以下を追加 - column: "books.data" go_type: import: "sqlc-sample/dto" package: "dto" type: "BookData" |
sqlc generateで生成する
最後に
sqlc generate
コマンドでコードを生成すると、
models.go
が以下のように生成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.24.0 package bookshelf import ( dto "sqlc-sample/dto" ) type Author struct { ID uint64 Name string } type Book struct { ID uint64 Title string AuthorID uint64 Data dto.BookData } |
Book.Data
の型が
dto.BookData型
になっています。
さいごに
sqlcを使うと、データベースにアクセスするコードを簡単かつ楽に書くことができ、コードを書くのが楽しくなるのではと思います。
今回紹介した内容以外にも、その他の設定ファイルの項目についてや、便利なマクロなどもありますので、これらを活用してsqlcをもっと便利に使うことができます。