Mybatis Generatorで生成されるSQLに排他取得用FOR UPDATE句を追加する方法

# はじめに
[こちら](https://qiita.com/norippe3/items/ed597c4a9a112fe8c4bc)の記事でMybatis Generatorのpluginを作成して、特定カラムをデフォルト値で更新する方法を紹介しましたが、同じようにpluginを作成して、``selectByPrimaryKeyForUpdate``メソッドを追加し、PKでレコード取得時に``FOR UPDATE``句を追加する方法を紹介します。

# 準備
プラグインの作成方法は以下で説明してるのでプラグイン作成のための準備事項やプラグインの実行方法はこちらを参照してください。

https://qiita.com/norippe3/items/ed597c4a9a112fe8c4bc

# やり方

## PluginAdapterのサブクラスの実装

```java:PluginAdapterのサブクラス

package com.example.test;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.mybatis.generator.api.FullyQualifiedTable;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.Interface;
import org.mybatis.generator.api.dom.java.Method;
import org.mybatis.generator.api.dom.xml.Attribute;
import org.mybatis.generator.api.dom.xml.Document;
import org.mybatis.generator.api.dom.xml.TextElement;
import org.mybatis.generator.api.dom.xml.XmlElement;

/**
 * selectByPrimaryKeyに、排他取得用の"FOR UPDATE"句を追加するためのプラグイン
 */
public class AddForUpdatePlugin extends PluginAdapter {

	private final Map<FullyQualifiedTable, List<XmlElement>> elementsToAdd = new HashMap<>();

	@Override
	public boolean validate(List<String> warnings) {
		return true;
	}

	@Override
	public boolean sqlMapSelectByPrimaryKeyElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {

		// ForUpdate用の新しいタグを生成
		XmlElement newElement = new XmlElement(element);

		Iterator<Attribute> iterator = newElement.getAttributes().iterator();
		while (iterator.hasNext()) {
			Attribute attribute = iterator.next();
			if ("id".equals(attribute.getName())) {
				iterator.remove();
				Attribute newAttribute = new Attribute("id", attribute.getValue() + "ForUpdate");
				newElement.addAttribute(newAttribute);
				// "for update"を追加
				newElement.addElement(new TextElement("for update"));
				break;
			}
		}

		// 新しく生成したタグをローカル変数に保存。後ほどsqlMapDocumentGeneratedメソッドにてRootDocument配下に追加
		List<XmlElement> elements = elementsToAdd.computeIfAbsent(introspectedTable.getFullyQualifiedTable(),
				k -> new ArrayList<>());
		elements.add(newElement);

		return true;
	}

	@Override
	public boolean clientSelectByPrimaryKeyMethodGenerated(Method method, Interface interfaze,
			IntrospectedTable introspectedTable) {

		// 新しいメソッドを追加
		Method newMethod = new Method(method);
		newMethod.setName(method.getName() + "ForUpdate");
		interfaze.addMethod(newMethod);

		return true;
	}

	@Override
	public boolean sqlMapDocumentGenerated(Document document,
			IntrospectedTable introspectedTable) {
		
		List<XmlElement> elements = elementsToAdd.get(introspectedTable.getFullyQualifiedTable());
		if (elements != null) {
			for (XmlElement element : elements) {
				document.getRootElement().addElement(element);
			}
		}

		return true;
	}
}
```

``sqlMapSelectByPrimaryKeyElementGenerated()``メソッドをオーバーライドしてSQL定義用XMLファイルの``selectByPrimaryKey``をベースに``selectByPrimaryKeyForUpdate``用のDOMを新たに生成、生成したDOMに``for update``句を追加します。

``sqlMapDocumentGenerated()``メソッドをオーバーライドして、``sqlMapSelectByPrimaryKeyElementGenerated()``メソッドにて生成したDOMをXMLに追加します。

さらに、``clientSelectByPrimaryKeyMethodGenerated()``メソッドオーバーライドして、Mapperクラスの``selectByPrimaryKey``メソッドをベースに``selectByPrimaryKeyForUpdate``メソッドを作成します。

## generatorConfig.xmlに作成したプラグインを設定

```xml:generatorConfig.xml
<generatorConfiguration>
  <context id=...>
	<!-- 排他取得用の"FOR UPDATE"句を追加するためのプラグイン -->
	<plugin type="com.example.test.AddForUpdatePlugin"/>
    ...


```
generatorConfig.xmlに上記で作成したクラスを``plugin``タグに設定します。

# 生成されたファイルのサンプル

```java:Mapperクラス
...

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table public.user
     *
     * @mbg.generated Mon May 12 14:43:50 JST 2025
     */
    User selectByPrimaryKeyForUpdate(String userId);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table public.user
     *
     * @mbg.generated Mon May 12 14:43:50 JST 2025
     */
    User selectByPrimaryKey(String userId);

...
```
上記の通り、``selectByPrimaryKeyForUpdate()``メソッドが追加で生成されます。

```xml:SQL定義用xml
...
  <select id="selectByPrimaryKeyForUpdate" parameterType="java.lang.String" resultMap="BaseResultMap">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Mon May 12 14:43:50 JST 2025.
    -->
    select 
    <include refid="Base_Column_List" />
    from public.user
    where user_id = #{userId,jdbcType=VARCHAR}
    for update
  </select>
...
```
上記の通り、``selectByPrimaryKeyForUpdate``が生成され、最後に``forupdate``が出力されていま。

# 参考サイト

https://github.com/mybatis/generator/blob/master/core/mybatis-generator-core/src/main/java/org/mybatis/generator/plugins/RowBoundsPlugin.java