SpringBootでMyBatisを使用している際につまづいた点と、その解決策として作成したプラグインの一例を紹介します。
1. Selectiveがつくメソッドとつかないメソッドの違い
MyBatisの自動生成で作成したMapperには、insert、update、などのメソッドが用意されています。
これらのメソッドには、Selectiveがつくものとつかないものがあります。
例:「insert」「insertSelective」「updateByPrimaryKey」「updateByPrimaryKeySelective」
これらの違いは、「null」の扱いにあります。
Selectiveが付かないメソッド
- すべてのカラムに対して登録・更新処理を行う
- Entityに何も値を設定しない場合、そのカラムには「null」がセットされる
Selectiveが付くメソッド
- 値がnull以外のカラムに対して登録・更新処理を行う
- Entityに何も値を設定しない場合、登録・更新対象のカラムに含まれない
これらを意識せずに使用していると、想定外のエラーや登録値の原因になるため、注意が必要です。
具体的なシチュエーションと実際に流れるSQLを確認してみます。
なお、本記事でサンプルとして使用しているテーブルレイアウトは以下の通りです。
CREATE TABLE `sample_table` (
`id` char(1) NOT NULL,
`column1` varchar(50),
`default_check` char(1) NOT NULL default 0,
`memo1` TEXT ,
PRIMARY KEY (`id`)
);
必須カラムがあるテーブルにINSERTする場合
insertメソッドを使用すると、Entityで値を設定していないカラムに対しては、「null」を登録するSQLが実行されます。
たとえテーブル定義でデフォルト値が設定されていたとしても、明示的に「null」を登録しようとするSQLになってしまうため、エラーが発生します。
insertメソッドを使用する場合は、デフォルト値が設定されているカラムに対しても、明示的に値を設定してあげる必要があります。
よくあるのが、既存のテーブルに新規カラムを追加した際、デフォルト値が設定されているからソースは直さなくてもいいだろうと考えてしまうケースです。
上記の通り、insertメソッドを使用する場合は、デフォルト値が設定されているカラムに対しても、値を設定してあげる必要があります。
NGパターン
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("A");
// sampleEntity.setDefaultCheck("0"); // 記述しないとエラーになる
sampleEntity.setMemo1("insert");
sampleRepository.insert(sampleEntity);
insert into sample_table (id, column1, default_check, memo1) values ("1", "A", null, "insert");
OKパターン
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("A");
sampleEntity.setDefaultCheck("0"); // 記述しないとエラーになる
sampleEntity.setMemo1("insert");
sampleRepository.insert(sampleEntity);
insert into sample_table (id, column1, default_check, memo1) values ("1", "A", "0", "insert");
あるいは、insertSelectiveメソッドを使用します。
insertSelectiveメソッドを使用すると、値を設定していないカラムは登録対象外になるため、テーブル定義としてデフォルト値を設定していれば、そのままデフォルト値が登録されます。
OKパターン
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("A");
//sampleEntity.setDefaultCheck("0"); // 記述しなくてもエラーにならない
sampleEntity.setMemo1("insert");
sampleRepository.insertSelective(sampleEntity);
insert into sample_table (id, column1, memo1) values ("1", "A", "insert");
nullで更新したくない(or 更新したい)カラムがある場合
更新時も考え方は同様です。
updateメソッドを使用すると、Entityで値を設定していないカラムに対しては、「null」で更新するSQLが実行されます。
NGパターン
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("B");
// sampleEntity.setDefaultCheck("0"); // 記述しないとエラーになる
sampleEntity.setMemo1("update");
sampleRepository.updateByPrimaryKey(sampleEntity);
update sample_table set column1 = "B", default_check = null, memo1 = "update" where id = "1";
OKパターン
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("B");
sampleEntity.setDefaultCheck("0"); // 記述しないとエラーになる
sampleEntity.setMemo1("update");
sampleRepository.updateByPrimaryKey(sampleEntity);
update sample_table set column1 = "B", default_check = "0", memo1 = "update" where id = "1";
updateSelectiveであれば、値を設定していないカラムは更新対象外になります。
OKパターン
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("B");
//sampleEntity.setDefaultCheck("0"); // 記述しなくてもエラーにならない
sampleEntity.setMemo1("update");
sampleRepository.updateByPrimaryKeySelective(sampleEntity);
update sample_table set column1 = "B", memo1 = "update" where id = "1";
また、updateSelectiveを使用する場合は、nullで更新することはできないため注意が必要です。
nullで更新したいカラムがある場合は、updateを使用する必要があります。
NGパターン
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("B");
sampleEntity.setDefaultCheck("0");
sampleEntity.setMemo1(null); // nullで更新できない
sampleRepository.updateByPrimaryKeySelective(sampleEntity);
update sample_table set column1 = "B", default_check = "0" where id = "1";
OKパターン
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("8");
sampleEntity.setColumn1("B");
sampleEntity.setDefaultCheck("0");
sampleEntity.setMemo1(null);
sampleRepository.updateByPrimaryKey(sampleEntity);
update sample_table set column1 = "B", default_check = "0", memo1 = null where id = "8";
ただし、updateを使用する場合は、明示的にnullを設定していなくても、何も値を設定しない場合は、「null」として更新されることにも注意が必要です。
OKパターン
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("8");
sampleEntity.setColumn1("B");
sampleEntity.setDefaultCheck("0");
// sampleEntity.setMemo1(null); // 記述しなくてもnullで更新される
sampleRepository.updateByPrimaryKey(sampleEntity);
update sample_table set column1 = "B", default_check = "0", memo1 = null where id = "8";
2. DBの型が「TEXT」「BLOB」の場合に、Selectメソッドが分離される
つまづいた点の2つ目として、DBの型が「TEXT」「BLOB」の場合に、SelectByExampleメソッドが分離されることがあります。
型が「TEXT」のカラムは通常のselectByExamplメソッドで取得できず、selectByExampleWithBLOBsメソッドを使用する必要がありました。
Mapper.xmlは以下のようになっています。
<sql id="Base_Column_List">
id, column1, default_check
</sql>
<sql id="Blob_Column_List">
<!-- TEXT型のカラムだけ分離されている -->
memo1
</sql>
<!-- こちらのメソッドを使用しないと、memo1カラムは取得できない -->
<select id="selectByExampleWithBLOBs" parameterType="jp.co.demo.sample.entity.generated.SampleTableExample" resultMap="ResultMapWithBLOBs">
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List" />
,
<include refid="Blob_Column_List" />
from sample_table
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null">
order by ${orderByClause}
</if>
</select>
<select id="selectByExample" parameterType="jp.co.demo.sample.entity.generated.SampleTableExample" resultMap="BaseResultMap">
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List" />
from sample_table
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null">
order by ${orderByClause}
</if>
</select>
targetRuntimeに「MyBatis3」を使用しているためなのか、回避策として見かけたmodelTypeを「flat」にするという方法でも回避できなかったため、力技でプラグインを作成して解決しました。
以下、プラグインの書き方の一例です。
BLOB型のカラムをBase_Column_Listに追加し、selectByExampleメソッドで取得できるようにしています。
また、BLOB型のカラムに対して、条件を指定するメソッドを追加しています。
@Override
public boolean modelBaseRecordClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
List<IntrospectedColumn> blobColumns = introspectedTable.getBLOBColumns();
for (IntrospectedColumn column : blobColumns) {
introspectedTable.getBaseColumns().add(column);
}
introspectedTable.getBLOBColumns().clear();
return super.modelBaseRecordClassGenerated(topLevelClass, introspectedTable);
}
@Override
public boolean modelExampleClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
for (IntrospectedColumn column : introspectedTable.getAllColumns()) {
if (column.isBLOBColumn()) {
addTextColumnMethods(topLevelClass, column);
}
}
return true;
}
private void addTextColumnMethods(TopLevelClass topLevelClass, IntrospectedColumn column) {
// GeneratedCriteriaクラスを探す
InnerClass generatedCriteriaClass = null;
for (InnerClass innerClass : topLevelClass.getInnerClasses()) {
if (innerClass.getType().getShortName().equals("GeneratedCriteria")) {
generatedCriteriaClass = innerClass;
break;
}
}
if (generatedCriteriaClass == null) {
return;
}
String camelCaseName = column.getJavaProperty();
String capitalizedName = camelCaseName.substring(0, 1).toUpperCase() + camelCaseName.substring(1);
String columnName = column.getActualColumnName();
addMethod(generatedCriteriaClass, "and" + capitalizedName + "IsNull", columnName + " is null");
addMethod(generatedCriteriaClass, "and" + capitalizedName + "IsNotNull", columnName + " is not null");
addMethod(generatedCriteriaClass, "and" + capitalizedName + "EqualTo", columnName + " =",
column.getFullyQualifiedJavaType());
addMethod(generatedCriteriaClass, "and" + capitalizedName + "NotEqualTo", columnName + " <>",
column.getFullyQualifiedJavaType());
addMethod(generatedCriteriaClass, "and" + capitalizedName + "GreaterThan", columnName + " >",
column.getFullyQualifiedJavaType());
addMethod(generatedCriteriaClass, "and" + capitalizedName + "GreaterThanOrEqualTo", columnName + " >=",
column.getFullyQualifiedJavaType());
addMethod(generatedCriteriaClass, "and" + capitalizedName + "LessThan", columnName + " <",
column.getFullyQualifiedJavaType());
addMethod(generatedCriteriaClass, "and" + capitalizedName + "LessThanOrEqualTo", columnName + " <=",
column.getFullyQualifiedJavaType());
addMethod(generatedCriteriaClass, "and" + capitalizedName + "Like", columnName + " like",
column.getFullyQualifiedJavaType());
addMethod(generatedCriteriaClass, "and" + capitalizedName + "NotLike", columnName + " not like",
column.getFullyQualifiedJavaType());
addMethod(generatedCriteriaClass, "and" + capitalizedName + "In", columnName + " in",
new FullyQualifiedJavaType(
"java.util.List<" + column.getFullyQualifiedJavaType().getShortName() + ">"));
addMethod(generatedCriteriaClass, "and" + capitalizedName + "NotIn", columnName + " not in",
new FullyQualifiedJavaType(
"java.util.List<" + column.getFullyQualifiedJavaType().getShortName() + ">"));
addMethod(generatedCriteriaClass, "and" + capitalizedName + "Between", columnName + " between",
column.getFullyQualifiedJavaType(), column.getFullyQualifiedJavaType());
addMethod(generatedCriteriaClass, "and" + capitalizedName + "NotBetween", columnName + " not between",
column.getFullyQualifiedJavaType(), column.getFullyQualifiedJavaType());
}
private void addMethod(InnerClass innerClass, String methodName, String condition) {
Method method = new Method(methodName);
method.setVisibility(org.mybatis.generator.api.dom.java.JavaVisibility.PUBLIC);
method.setReturnType(new FullyQualifiedJavaType("Criteria"));
method.addBodyLine("addCriterion(\"" + condition + "\");");
method.addBodyLine("return (Criteria) this;");
innerClass.addMethod(method);
}
private void addMethod(InnerClass innerClass, String methodName, String condition,
FullyQualifiedJavaType paramType) {
Method method = new Method(methodName);
method.setVisibility(org.mybatis.generator.api.dom.java.JavaVisibility.PUBLIC);
method.setReturnType(new FullyQualifiedJavaType("Criteria"));
method.addParameter(new org.mybatis.generator.api.dom.java.Parameter(paramType, "value"));
method.addBodyLine("addCriterion(\"" + condition + "\", value, \"" + paramType.getShortName() + "\");");
method.addBodyLine("return (Criteria) this;");
innerClass.addMethod(method);
}
private void addMethod(InnerClass innerClass, String methodName, String condition,
FullyQualifiedJavaType paramType1, FullyQualifiedJavaType paramType2) {
Method method = new Method(methodName);
method.setVisibility(org.mybatis.generator.api.dom.java.JavaVisibility.PUBLIC);
method.setReturnType(new FullyQualifiedJavaType("Criteria"));
method.addParameter(new org.mybatis.generator.api.dom.java.Parameter(paramType1, "value1"));
method.addParameter(new org.mybatis.generator.api.dom.java.Parameter(paramType2, "value2"));
method.addBodyLine(
"addCriterion(\"" + condition + "\", value1, value2, \"" + paramType1.getShortName() + "\");");
method.addBodyLine("return (Criteria) this;");
innerClass.addMethod(method);
}
プラグインを適用して生成されたMapper.xmlは次のようになりました。
<sql id="Base_Column_List">
<!-- Base_Column_ListにTEXT型の「memo1」も含まれている -->
id, column1, default_check, memo1
</sql>
createCriteriaで条件を指定することも可能になります。
SampleTableExample sampleExample = new SampleTableExample();
// TEXT型のカラムもcreateCriteriaで条件指定が可能
sampleExample.createCriteria().andMemo1Like("%Hoge%");
List<SampleTableEntity> sampleEntityList = sampleRepository.selectByExample(sampleExample);