0
0

MyBatisを使用した際につまづいた点とプラグインの一例

Last updated at Posted at 2024-07-14

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パターン

sample.java
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("A");
// sampleEntity.setDefaultCheck("0");  // 記述しないとエラーになる
sampleEntity.setMemo1("insert");
sampleRepository.insert(sampleEntity);
実際に流れるSQL.sql
insert into sample_table (id, column1, default_check, memo1) values ("1", "A", null, "insert");

OKパターン

sample.java
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("A");
sampleEntity.setDefaultCheck("0");  // 記述しないとエラーになる
sampleEntity.setMemo1("insert");
sampleRepository.insert(sampleEntity);
実際に流れるSQL.sql
insert into sample_table (id, column1, default_check, memo1) values ("1", "A", "0", "insert");

あるいは、insertSelectiveメソッドを使用します。
insertSelectiveメソッドを使用すると、値を設定していないカラムは登録対象外になるため、テーブル定義としてデフォルト値を設定していれば、そのままデフォルト値が登録されます。

OKパターン

sample.java
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("A");
//sampleEntity.setDefaultCheck("0");  // 記述しなくてもエラーにならない
sampleEntity.setMemo1("insert");
sampleRepository.insertSelective(sampleEntity);
実際に流れるSQL.sql
insert into sample_table (id, column1, memo1) values ("1", "A", "insert");

nullで更新したくない(or 更新したい)カラムがある場合

更新時も考え方は同様です。
updateメソッドを使用すると、Entityで値を設定していないカラムに対しては、「null」で更新するSQLが実行されます。

NGパターン

sample.java
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("B");
// sampleEntity.setDefaultCheck("0"); // 記述しないとエラーになる
sampleEntity.setMemo1("update");
sampleRepository.updateByPrimaryKey(sampleEntity);
実際に流れるSQL.sql
update sample_table set column1 = "B", default_check = null, memo1 = "update" where id = "1";

OKパターン

sample.java
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("B");
sampleEntity.setDefaultCheck("0"); // 記述しないとエラーになる
sampleEntity.setMemo1("update");
sampleRepository.updateByPrimaryKey(sampleEntity);
実際に流れるSQL.sql
update sample_table set column1 = "B", default_check = "0", memo1 = "update" where id = "1";

updateSelectiveであれば、値を設定していないカラムは更新対象外になります。

OKパターン

sample.java
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("B");
//sampleEntity.setDefaultCheck("0"); // 記述しなくてもエラーにならない
sampleEntity.setMemo1("update");
sampleRepository.updateByPrimaryKeySelective(sampleEntity);
実際に流れるSQL.sql
update sample_table set column1 = "B", memo1 = "update" where id = "1";

また、updateSelectiveを使用する場合は、nullで更新することはできないため注意が必要です。
nullで更新したいカラムがある場合は、updateを使用する必要があります。

NGパターン

sample.java
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("1");
sampleEntity.setColumn1("B");
sampleEntity.setDefaultCheck("0");
sampleEntity.setMemo1(null); // nullで更新できない
sampleRepository.updateByPrimaryKeySelective(sampleEntity);
実際に流れるSQL.sql
update sample_table set column1 = "B", default_check = "0" where id = "1";

OKパターン

sample.java
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("8");
sampleEntity.setColumn1("B");
sampleEntity.setDefaultCheck("0");
sampleEntity.setMemo1(null);
sampleRepository.updateByPrimaryKey(sampleEntity);
実際に流れるSQL.sql
update sample_table set column1 = "B", default_check = "0", memo1 = null where id = "8";

ただし、updateを使用する場合は、明示的にnullを設定していなくても、何も値を設定しない場合は、「null」として更新されることにも注意が必要です。

OKパターン

sample.java
SampleTableEntity sampleEntity = new SampleTableEntity();
sampleEntity.setId("8");
sampleEntity.setColumn1("B");
sampleEntity.setDefaultCheck("0");
// sampleEntity.setMemo1(null); // 記述しなくてもnullで更新される
sampleRepository.updateByPrimaryKey(sampleEntity);
実際に流れるSQL.sql
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は以下のようになっています。

自動生成された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型のカラムに対して、条件を指定するメソッドを追加しています。

MyBatisPlugin.java
	@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は次のようになりました。

自動生成されたMapper.xml
  <sql id="Base_Column_List">
    <!-- Base_Column_ListにTEXT型の「memo1」も含まれている -->
    id, column1, default_check, memo1
  </sql>

createCriteriaで条件を指定することも可能になります。

Sample.java
SampleTableExample sampleExample = new SampleTableExample();
// TEXT型のカラムもcreateCriteriaで条件指定が可能
sampleExample.createCriteria().andMemo1Like("%Hoge%");
List<SampleTableEntity> sampleEntityList = sampleRepository.selectByExample(sampleExample);
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0