
Domaの自動生成が 近似する名前のテーブル のカラムを混ぜてくる問題を生成時に自動で解消する
以前、 DomaCodeGenでテーブル名が近似するテーブルが存在すると、異なるテーブルのカラムが生成される という記事を書きましたが、解消方法までは記事に含めていませんでした。
今回はAIによる調査コストの大幅減で改めて原因の調査と自動での解消が可能か試してみました
現象
以下例の通り、
_ で一文字違いのテーブル名が存在するスキーマに対してDomaodeGenを利用すると、この現象が発生します。_ を含むテーブルのEntityに、もう片方の類似テーブルにのみ含まれるカラムが追加され、実際のテーブルの内容とEntityの内容が整合しなくなります。CREATE TABLE sample_item (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
price INTEGER
);
CREATE TABLE sample2item (
id BIGINT PRIMARY KEY,
parent_id BIGINT,
child_id BIGINT,
extra_col VARCHAR(50)
);
@Entity(listener = SampleItemListener::class, metamodel = Metamodel())
@Table(name = "sample_item")
class SampleItem : AbstractSampleItem() {
@Id @Column(name = "id") var id: Long = -1L
@Column(name = "parent_id") var parentId: Long? = -1L // ← sample2item由来(混入)
@Column(name = "child_id") var childId: Long? = -1L // ← 混入
@Column(name = "extra_col") var extraCol: String? = null // ← 混入
@Column(name = "name") var name: String? = null
@Column(name = "price") var price: Int? = -1
}原因
DomaCodeGenでは、対象テーブルのカラムを取得する際のテーブル名をエスケープせずそのまま指定し、完全一致ではなくパターン検索する実装となっています。
このため、
_ を含むテーブルが指定されると、 _ はLIKEと同様に「任意1文字」として扱われ、 _ の位置が別の文字になっているテーブルも一致してしまいます。加えて、取得した結果を「テーブル名が一致する行だけ」に絞り込んでもいないため、誤って一致した隣のテーブルのカラムもそのまま採用されてしまいます。
結果、「パターン検索で類似名称のテーブルにも一致する」→「取得結果をテーブル名で絞らない」いう2段構えで類似名称のテーブルのカラムがすり抜け、Entityに本来は無いカラムが一緒に生成される、という動きになります。
パターン "sample_item" の '_' は「任意の1文字」
sample_item → _ が文字 '_' に一致(本命)
sample2item → _ が文字 '2' に一致(誤マッチしてsample_itemに無いカラムが取得される)以下はプラグイン本体で原因となっている該当箇所(doma-codegen-plugin TableMetaReader)
// getColumns の第3引数へテーブル名を“そのまま”渡している(エスケープなし)
ResultSet rs = metaData.getColumns(
tableMeta.getCatalogName(),
tableMeta.getSchemaName(),
tableMeta.getName(), // "sample_item" が LIKE パターンとして解釈される
null);
while (rs.next()) {
// 戻り行を TABLE_NAME で絞っていない → 両テーブルのカラムをすべて採用してしまう
ColumnMeta columnMeta = new ColumnMeta();
columnMeta.setName(rs.getString("COLUMN_NAME"));
...
}解決方法
カラム取得を担う
TableMetaReader を自作版に差し替え、取得した結果を対象テーブルのカラムだけに絞ることで解消する方法です。(「取得結果をテーブル名で絞らない」挙動を修正することで解消する方針)
手作業や生成後の後処理は不要で、再生成しても混入は発生しません。
(1) 修正版リーダー(buildSrc/src/main/java/domafix/FixedTableMetaReader.java)
getDbColumnMetas を override し、取得結果を TABLE_NAME 完全一致で絞る(原因 で引用したメソッドの修正版)public class FixedTableMetaReader extends TableMetaReader {
public FixedTableMetaReader(
CodeGenDialect dialect, DataSource dataSource, String catalogName, String schemaName,
String tableNamePattern, String ignoredTableNamePattern, List<String> tableTypes) {
super(dialect, dataSource, catalogName, schemaName,
tableNamePattern, ignoredTableNamePattern, tableTypes);
}
@Override
protected List<ColumnMeta> getDbColumnMetas(DatabaseMetaData metaData, TableMeta tableMeta)
throws SQLException {
List<ColumnMeta> results = new ArrayList<>();
try (ResultSet rs = metaData.getColumns(
tableMeta.getCatalogName(), tableMeta.getSchemaName(), tableMeta.getName(), null)) {
while (rs.next()) {
// ★核心: '_' ワイルドカード誤マッチで混ざる“隣のテーブル”のカラムを排除
if (!tableMeta.getName().equals(rs.getString("TABLE_NAME"))) {
continue;
}
ColumnMeta columnMeta = new ColumnMeta();
columnMeta.setName(rs.getString("COLUMN_NAME"));
columnMeta.setSqlType(rs.getInt("DATA_TYPE"));
columnMeta.setTypeName(rs.getString("TYPE_NAME").toLowerCase());
columnMeta.setLength(rs.getInt("COLUMN_SIZE"));
columnMeta.setScale(rs.getInt("DECIMAL_DIGITS"));
columnMeta.setNullable(rs.getBoolean("NULLABLE"));
columnMeta.setDefaultValue(rs.getString("COLUMN_DEF"));
columnMeta.setComment(rs.getString("REMARKS"));
results.add(columnMeta);
}
}
return results;
}
}(2) 修正版リーダーの差し替えファクトリ(buildSrc/src/main/java/domafix/FixedGlobalFactory.java)
GlobalFactory#createTableMetaReader を override し、(1) 修正版リーダー を返すようにするpublic class FixedGlobalFactory extends GlobalFactory {
@Override
public TableMetaReader createTableMetaReader(
CodeGenDialect dialect, DataSource dataSource, String catalogName, String schemaName,
String tableNamePattern, String ignoredTableNamePattern, List<String> tableTypes) {
return new FixedTableMetaReader(
dialect, dataSource, catalogName, schemaName,
tableNamePattern, ignoredTableNamePattern, tableTypes);
}
}(3) buildSrc の依存(buildSrc/build.gradle.kts)
(1)(2) を buildSrc にプラグイン本体(+JDBCドライバ)を依存として与え、コンパイルできるようにする
plugins { java }
repositories {
gradlePluginPortal() // Doma codegen プラグイン本体はここに公開されている
mavenCentral()
}
dependencies {
implementation("org.domaframework.doma:codegen:3.0.0")
// プラグインが buildSrc 側 classloader から読まれるため、JDBCドライバも同じ classloader に載せる
implementation("org.postgresql:postgresql:42.7.7")
}(4) 配線(build.gradle.kts)
生成設定に globalFactory.set(…) を追加して (2) を使わせ、修正を有効化する
plugins {
// buildSrc がクラスパス提供するためバージョンは書かない
id("org.domaframework.doma.codegen")
}
domaCodeGen {
register("dev") {
// …url/user/password/entity/dao 設定…
globalFactory.set(domafix.FixedGlobalFactory()) // ← これだけで以後の生成はクリーン
}
}
ここまでの修正を入れて生成し直すと、元々混入していた SampleItem は次のようになり、Sample2item 由来のカラム(patient_id / child_id/ extra_col )が生成されなくなります。
@Entity(listener = SampleItemListener::class, metamodel = Metamodel())
@Table(name = "sample_item")
class SampleItem : AbstractSampleItem() {
@Id @Column(name = "id") var id: Long = -1L
@Column(name = "name") var name: String? = null
@Column(name = "price") var price: Int? = -1
}
まとめ
前回の記事では、事象の確認と共有、そして「プラグイン自体に手を入れるしかなさそう(当面はテーブル名の変更で回避)」という見立てまでで止まっていたものを、今回はAIの活用で調査コストが大きく下がったこともあり、プラグインには手を入れず、差し替える形で解消する方法まで紹介できました。
この不具合は Doma 固有のものではなく、DatabaseMetaData を使ってスキーマを読む自動生成ツール全般が踏みうる罠なので、生成物を鵜呑みにせずDBの実体と突き合わせておくと安全そうです。そして、従来は「事象の共有」で止まりがちだった原因究明・根治まで踏み込めたことが、前回からの一番の変化でした。