【golang】sqlcコマンドで「SQLクエリから型安全なGoコードを生成」し、生産性を上げたい

前書き:sqlcとは

本記事は、kyleconroy/sqlcの基本的な情報を紹介します。

sqlcは、DBスキーマ(DBテーブル定義)、SQLクエリ定義、設定ファイルの3点をインプットとして、型安全なCRUDコード + DBテーブルに対応したモデル(構造体)を自動生成します。ここでのモデルの自動生成には、複数テーブルをJOINしたクエリ用の構造体も含まれます。

個人的な視点では、sqlcは「SQLクエリを検証してから、そのクエリを実行するGolangコードを書いて、クエリ結果を受け取るための構造体を書くのが大変」という課題を解決するツールです。独自のDSL(Domain Specific Language)は殆ど登場しないので、SQLをゴリゴリ書ける開発者には使いやすいツールです。

 

sqlcに着目した理由

sqlcに着目した理由は、標準パッケージ主体の開発スタイル(ORM “Object-Relational Mapping”を避けた開発スタイル)の中で、少しでも実装量を減らして開発速度を向上させたいからです。

ORM(例:go-gorm/gorment/ent)を避ける理由は、「ブラックボックス化されるSQLクエリ」「実行速度の低下」「複雑なクエリを書きづらい」などが挙げられます。それらしい理由を書いていますが、これらは世間一般で語られる理由です。

私がORMを使わない理由は、単純に現職のサーバーサイドチームの意向に従っているだけです(私はYESマン)。私は前職が組み込みエンジニアだった関係でDBド素人であり、ORMのメリット/デメリットを考慮した技術選定ができません。そのため、会社で経験値の高い方法(標準パッケージ主体の開発スタイル)に従った方が安全でした。

(そもそも技術選定できる立場じゃないしな!)

 

が、半年も開発していると、「これは改善したほうが良いのでは…?」という部分が出てきました。例えば、テストコードのためだけにCRUDコードを実装する時間が無駄に感じられました。DBスキーマ(DBテーブル定義)はkayac/ddl-makerを用いてGolang構造体からSQLファイルを自動生成していますが、ddl-makerはCRUDコードを自動生成しません。

そんな中でsqlcを採用すれば、SQLクエリさえ書けばCRUDコードを自動生成できます。現職の課題を解決(※)するには、ピッタリです

 

※ 「ddl-maker用の構造体」と「sqlcが自動生成する構造体」が同じ役割を持つ、という新たな課題が発生します。しかし、この問題は弊社固有のものなので、皆さんは気にされなくて大丈夫です。

         

DBおよびプログラミング言語のサポート状況

sqlcは、GolangでMySQL/PostgreSQLをサポートしています。他の言語サポートは、開発段階のようです。今後は、サポートDBにSQLiteを追加し、サポート言語にC#, TypeScriptを追加するようです。

Language

MySQL

PostgreSQL

Go Stable Stable
Kotlin Beta Beta
Python Beta Beta

      

sqlcのインストール方法

公式のインストール手順は、公式ドキュメントを参照してください。Golangユーザーであれば、定番の方法でインストールできます。

Macユーザーであれば”$ brew install sqlc”、Ubuntuユーザーであれば”$ sudo snap install sqlc”でインストールする方法もあります。

       

sqlcのインプットファイル一覧

前提として、sqlcによる自動生成には、以下のファイルを準備する必要があります。

  • DBテーブル定義(schema.sql)
  • SQLクエリ定義(query.sql)
  • sqlc設定ファイル(sqlc.yaml or sqlc.json)

これらのサンプル(書き方)を順番に説明します。言語はGolang、DBはMySQLとします。

       

DBテーブル定義(schema.sql)

DBテーブル定義として、MySQLかPostgeSQL用の”CREATE TABLE〜”文を記載したファイルを準備します。sqlc独自の文法やDSLは、登場しません。

今回はサンプルとして、ユーザー、部署、ユーザーと部署の中間テーブルを作成するschema.sqlを用意します。

        

SQLクエリ定義(query.sql)

SQLクエリ定義として、sqlcに自動生成して欲しいCRUDコード用のSQLクエリを実装します。ここでの定義では、sqlcの独自のDSL(正確には、”Query annotations“)を用いて「生成するCRUDコードの関数名」と「返り値」を指定する必要があります。

   

Query annotationsの文法は-- name: $関数名 $返り値の種類であり、SQLクエリの上に記載します。下表に、Query annotationsの一覧を示します。

Query annotation 役割
:exec ExecContext のの実行結果(errorインターフェース)を返す
:execresult ExecContext の実行結果である「sql.Result」、「errorインターフェース」を返す
:execrows ExecContext の実行結果である「影響を与えた行の数」、「errorインターフェース」を返す
:execlastid ExecContext の実行結果である「最終ID(last ID)」、「errorインターフェース」を返す
:many QueryContextの実行結果である「取得した複数レコード」、「errorインターフェース」を返す
:one QueryRowContextの実行結果である「取得したレコード」、「errorインターフェース」を返す
:batchexec Batchオブジェクト(このオブジェクトはExec、Closeメソッドを持つ)を返す。PostgreSQLのみ使用可能
:batchmany Batchオブジェクト(このオブジェクトはQuery、Closeメソッドを持つ)を返す。PostgreSQLのみ使用可能
:batchone Batchオブジェクト(このオブジェクトはQueryRow、Closeメソッドを持つ)を返す。PostgreSQLのみ使用可能

           

以下に、SQLクエリ定義(query.sql)の例を示します。

userテーブルに対して、「SELECT、INSERT、UPDATE、UPSERT、DELETEするクエリ」および「 user , departmentuser_department_relation をJOINして、ユーザー名と部署名を表示するクエリ」を実装しています。

                      

上記の例で、少し工夫している部分は、取得するカラムに対して意図的に別名を付けている箇所です。具体的には、JOINを用いたクエリです。

以下に示すクエリでは、userテーブルとdepartmentテーブルは、どちらもカラム名がnameです。sqlcがこのクエリから構造体を自動生成した場合、構造体のフィールド名が”Name”, “Name_2″となり、少し残念な感じになります。

そこで、意図的にSELECT u.name AS user_name, d.name AS department_name 〜と書くこと(AS句でカラムに別名をつけること)によって、sqlcが人間に優しい構造体フィールド名(”UserName“、”DepartmentName”)を命名するようにしています。

       

sqlc設定ファイル(sqlc.yaml or sqlc.json)

sqlc設定ファイルは、yaml形式もしくはjson形式がサポートされています。設定ファイルにはVersion 1とVersion 2があり、今回の例ではVersion 2を採用します。

sqlc設定ファイルでは、大別して以下の3点を設定します。

  • インプットファイルのパス情報
  • 使用するDB情報
  • アウトプットするファイルの情報

 

今回の例では、プロジェクトルートディレクトリ直下にsqlcディレクトリを作り、この中にsqlcに関するファイルを集約します。sqlcが自動生成するファイルは<PROJECT_ROOT>/sqlc_output_dir以下に集約し、それらのパッケージ名は”query”とします。

以下、ディレクトリツリーとsqlc設定ファイルの例です。

注意点は、以下の通りです

  • sqlcインプットファイル(schema、queries)は、sqlc設定ファイル(sqlc.yaml or sqlc.json)を基点とした相対パスを指定
  • engineは、postgresqlかmysqlのいずれかが入る
  • 自動生成先ディレクトリ(out)は、sqlc設定ファイル(sqlc.yaml or sqlc.json)を基点とした相対パスを指定

             

自動生成の例

sqlcは、generateサブコマンドでCRUDコードを自動生成します。

sqlcは、デフォルトではカレントディレクトリ以下にあるsqlc設定ファイル(sqlc.yaml or sqlc.json)を探します。今回の例では、カレントディレクトリにはsqlc設定ファイルがありません。

このような場合は、–file(-f)オプションを用いて、明示的にsqlc設定ファイルパスを指定する必要があります。

    

自動生成完了後は、以下のようなディレクトリ構成になります。自動生成されたファイルは、db.go、models.go、query.sql.goの3点です。

自動生成されたコードは、以下の通りです。

  • db.go:SQLクエリの実行時に使用するQueries構造体を定義したファイル
  • model.go:各DBテーブルに対応するモデルを定義したファイル
  • query.sql.go:各SQLクエリを実行するメソッドを定義したファイル

         

クエリを実行するには、

  1. db.goに定義されたNew()に対して、sql.DBもしくはsql.Txを引数渡し
  2. New()の返り値から、Queries構造体を取得
  3. Queries構造体のメソッド(CRUDコード)を実行

という流れで、非常に使いやすく、シンプルです(参考ドキュメント

       

sqlcが持つサブコマンド、オプション

generateサブコマンド以外で良く使う可能性があるのは、以下の2つと思われます。

  • compile:SQLシンタックスおよび型エラーのチェック
  • init:sqlc.yamlの生成(あまり記載が埋まってなくて不満……)

           

最後に

sqlcを知った当初は、「SQL書きたくないのにSQLクエリから各種ファイルを自動生成するのか。微妙だなー」という感想でした。しかし、sqlcのインプットとなるSQLクエリは、普段のCRUDコード実装で手書きしている部分です。

そのため、sqlcの導入はあまり負担が増えません。むしろ、Golangコードの実装に使っていた時間が自動生成によってゼロになります。生産性が大きく変わります。

 

製品コードに使えないまでも、テストコードにsqlcを導入できないかなと考えています(このコメントは、社内の人向けに書いています)

    

おすすめ

1件の返信

  1. 2023年5月20日

    […] この煩わしさを解決しようとすると、SQLファイルからGolangコードを生成するkyleconroy/sqlcが輝いて見えます。sqlcに関しては過去に別記事でまとめましたが、sqlcは型安全なCRUDコード + DBテーブルに対応したモデル(構造体)を自動生成します。 […]