
目次
- はじめに
- SQLBoilerのコード生成フローをおさらい
- 調査のきっかけになったAIレビュー
- columnsWithoutDefault が示すもの
- 実際の挙動を確かめる
- まとめ — AIレビューとの付き合い方
はじめに
こんにちは。開発本部開発1部デリッシュキッチンMS2に所属している惟高です。
私が現在関わっているプロジェクトでは、SQLBoiler という ORM を使って既存の MySQL スキーマから Go のモデルコードを自動生成しています。
普段は SQLBoiler が生成してくれるモデルを中身を覗かず「ブラックボックス」として使っていますが、ある日の AI コードレビューからのコメントをきっかけに、その挙動を丁寧に追うことにしました。
この記事では、SQLBoiler がテーブル定義をどのように読み取りモデルを生成するのかをたどりながら、普段は意識していなかった内部構造を改めて整理していきます。
Note: SQLBoiler は現在メンテナンスモードのため、将来的に別 ORM への切り替えを検討する可能性があります。この記事は現行運用の振り返りとしてご覧ください。
SQLBoilerのコード生成フローをおさらい
SQLBoiler の CLI は、Cobra ベースのコマンドからドライバーとテンプレートを組み合わせてモデルを出力します。
実装では boilingcore.New が呼ばれ、設定を元にスキーマを読み取りテンプレートを準備します。
大まかな流れは以下のとおりです。
スキーマ情報の取得: DB ドライバーが
drivers.Tableとdrivers.Column構造体にメタデータを詰めます。 ここで列ごとのDefaultやNullableの情報が集約されます。列のカテゴリー分け: 取得したカラムは
FilterColumnsByDefaultやFilterColumnsByAutoなどの関数で種類ごとに分類されます。 たとえばDefault文字列が空なら「デフォルトなし」、AutoGeneratedがtrueなら「DB が自動で値を生成してくれる列」といった具合にルール化されます。テンプレート生成: 上記の結果がテンプレートに渡され、
modelsパッケージのコードとして出力されます。 テンプレートコード の先頭でColumnsWithoutDefaultやColumnsWithDefaultの配列が生成されるのがその一例です。
調査のきっかけになったAIレビュー
あるテーブルに nullable な group_id カラムを追加したところ、自動生成コードにおける columnsWithoutDefault にもその列が追加され、レビューボットから「ここに載っているなら必須では?」という指摘を受けました。
今回のマイグレーション
ALTER TABLE schema_version_cv_invalid_conditions ADD COLUMN group_id INT NULL DEFAULT NULL;

SQLBoiler が出力した差分は次のようなものでした。
var schemaVersionCVInvalidConditionColumnsWithoutDefault = []string{ "schema_version_id", "target_column", "condition_type", "group_id" // 今回追加された箇所 }
「NULL を許す設計だったはずなのに必須扱い?」と違和感を覚え、SQLBoiler が生成したモデルコードを確認してみました。
columnsWithoutDefault が示すもの
今回は AI レビューで指摘があった columnsWithoutDefault について調査した結果をまとめていきます。
結論から言うと、ColumnsWithoutDefault は「DB が自動補完しない列の一覧」であり、必須かどうかを判定する仕組みではありませんでした。
テンプレートを辿ると、FilterColumnsByDefault(false, columns) の結果がそのまま ColumnsWithoutDefault として出力されていることが分かります。実装は Column.Default の文字列が空かどうかを確認しているだけです。
Note: SQLBoiler の MySQL ドライバーでは
column_defaultが SQL の NULL のとき、*stringにスキャンした値がnilになるためColumn.Defaultは空文字のままです。DEFAULT NULLを宣言した列もこのパターンに当たるので、ColumnsWithoutDefault側に分類されます。(該当コード)
columnsWithoutDefault の使用用途は以下があります。
- INSERT クエリの生成
wl, _ := columns.InsertColumnSet(
{{$alias.DownSingular}}AllColumns,
{{$alias.DownSingular}}ColumnsWithDefault,
{{$alias.DownSingular}}ColumnsWithoutDefault,
nzDefaults,
)
ここでは ColumnsWithoutDefault をベースに、モデル側で値が設定された ColumnsWithDefault を足し合わせたうえで INSERT に載せる列(wl)を決めています。
randomize.Struct(seed, &a, {{$ltable.DownSingular}}DBTypes, false,
strmangle.SetComplement({{$ltable.DownSingular}}PrimaryKeyColumns,
{{$ltable.DownSingular}}ColumnsWithoutDefault)...)
テストコードでも ColumnsWithoutDefault を補助配列として利用できます。
例えば「親レコードに子レコードをひも付けるテスト」(SetChild などの関連付け処理)では、DB が補ってくれない列だけを取り出してランダムなダミーデータで埋め、その状態で関連付けメソッドが意図どおり機能するかを確認する用途に使っています。
実際の挙動を確かめる
生成されたモデル構造体では、該当する列は null.Int のような nullable 型で表現されます。
type SchemaVersionCVInvalidCondition struct { // 今回追加された部分のみを表示 GroupID null.Int `boil:"group_id" json:"group_id,omitempty" toml:"group_id" yaml:"group_id,omitempty"` }
null.Int{Valid:false} のまま Insert() するとクエリには列が含まれず、MySQL 側では NULL が保存されます。
まとめ — AIレビューとの付き合い方
今回、AI コードレビューの「必須では?」という指摘をきっかけに、SQLBoiler の内部構造について学ぶことができ、特にcolumnsWithoutDefault がどのように生成・利用されているかを改めて確認できました。
あわせて、columnsWithoutDefault のような自動生成コードは仕様を知らないと誤検知を招きやすいので、AI レビューではレビュー対象外にするなど運用でノイズを抑える工夫も必要だと感じました。
これから AI レビューを使う機会は増えていくはずです。 だからこそ、指摘を鵜呑みにせず該当コードや生成ロジックを辿って本当に正しいかどうか確かめる姿勢を持っていたいと思います。
少しでも参考になれば幸いです。最後まで読んでいただき、ありがとうございました。