【超初心者向け】MyBatis超入門

# はじめに
とても久しぶりの超入門シリーズです。
何年も前にMyBatisの勉強をしたのですが、まとめていませんでした。
MyBatisは文献が豊富で、わざわざ自力でまとめなくてもよかったのですが、超入門シリーズの別の記事の準備をしていて、その中でMyBatisを使い、結構手間取ったので、まずはMyBatisの記事を執筆することにしました。
では、さっそくはじめます。

# 環境構築
しばらく開発をしていなかったので、開発環境を新しく構築しました。
今回使用した開発環境は次の通りです。

■開発環境一覧
|種別|製品名|バージョン|特記事項|
|:-------------- |:--------------------------|:---------|-------|
|実行環境|Java|Java SE 21|Eclipse付属のJRE|
|統合開発環境|Pleiades All in one Eclipse|2024-06||
|DBMS|Oracle|19c|Oracle社が配布してくれている構築済みVMを使いました。<br>Oracle様、ありがとうございます。|
|SQLクライアント|MyBatis|3.5.16|当記事の本丸|
|JDBCドライバ|ojdbc|10||
|便利機能|lombok|1.18.34|初めて使いましたが、すごく便利ですね。|
|ロガー|logback|1.5.6||
|ロガーIF|slf4j|2.0.15||

# 実装内容概略
今回のサンプルで実装する内容を箇条書きで紹介します。
* 単一レコードのINSERT
* 単一レコードのSELECT
* 複数レコードのSELECT

# サンプルの実装
はりきってサンプルの実装です。

## ツリー
プロジェクトのツリーです。
ちょっと失敗して段がおかしいのですが、中身に影響はないので許してください。

```:ツリー
プロジェクトルート
├─lib
│      logback-classic-1.5.6.jar
│      logback-core-1.5.6.jar
│      lombok-1.18.34.jar
│      mybatis-3.5.16.jar
│      ojdbc10.jar
│      slf4j-api-2.0.15.jar
│
├─resources
│  │  logback.xml
│  │  mybatis-config.xml
│  │
│  └─sql
│          sqlMap.xml
│
└─src
    └─jp
        └─t
            └─mybatis
                ├─entity
                │      UserInfo.java
                │
                ├─mapper
                │      UserMapper.java
                │
                └─sample
                    ├─bl
                    │      BusinessLogic.java
                    │
                    └─main
                            MyBatisSampleMain.java

```

## DBの準備
今回もサンプルにはOracleを使用します。
サンプルで実装するテーブルのDDLです。
```SQL:USER_INFO.sql
CREATE TABLE USER_INFO
( 
    UNUMBER    NUMBER
  , UNAME      VARCHAR2(24)
  , AGE        NUMBER
  , BDATE      DATE
);
```

## MyBatisの準備
MyBatisの準備をします。
まずはDBの環境他MyBatisの動作設定を行うます。
設定にはXMLを使用します。
MyBatisの設定には、接続先の環境ほか、クエリのキャッシュや更新処理の実行モード、SQLMapperなどがあります。
SQLMapperについての詳細は後述します。

```XML:mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org/DTO Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<!-- MyBatis本体の設定 -->
	<settings>
		<!-- 更新処理のデフォルトををバッチ更新に設定 -->
		<setting name="defaultExecutorType" value="BATCH" />
		<!--
			キャッシュのタイプをSTATEMENTに設定する。
			これをやらないと、同じクエリを発行するときにDBに問い合わせず、
			メモリに持っている結果がそのまま返ってくることになる。
		 -->
		<setting name="localCacheScope" value="STATEMENT" />
	</settings>

	<!-- 接続環境設定 -->
	<environments default="ORCL">
		<environment id="ORCL">
			<transactionManager type="JDBC" />
			<dataSource type="UNPOOLED">
				<property name="driver" value="oracle.jdbc.driver.OracleDriver" />
				<property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521/ORCL" />
				<property name="username" value="USER01" />
				<property name="password" value="ORACLE" />
			</dataSource>
		</environment>
	</environments>

	<!-- SQLMapperたち -->
	<mappers>
		<mapper resource="sql/sqlMap.xml" />
	</mappers>
</configuration>
```

## Javaの実装
まずはMain処理です。
大したことはしません。

```java:MyBatisSampleMain.java
package jp.t.mybatis.sample.main;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jp.t.mybatis.sample.bl.BusinessLogic;

/**
 * <h1>[MyBatis超入門サンプルのMain]</h1><br>
 *<br>
 * Main処理を持つ。
 */
public class MyBatisSampleMain {
	/**
	 * ロガー。
	 * slf4jは1.8からServiceProviderなるものが必要になったようです。
	 */
	private static Logger logger = LoggerFactory.getLogger(MyBatisSampleMain.class);

	/**
	 * <h2>[Main処理]</h2><br>
	 *<br>
	 * 各業務処理を呼び出す。
	 * @param args コマンド引数
	 */
	public static void main(String[] args) {
		logger.info("---------- アプリケーション起動 ----------");

		// --------------------------------------------------------------------
		// 順番に業務処理を呼び出していく
		// --------------------------------------------------------------------
		BusinessLogic bl = new BusinessLogic();

		// --------------------------------------------------------------------
		// セッション準備他初期処理を行う
		// --------------------------------------------------------------------
		try {
			bl.init();
		} catch (Exception e) {
			logger.error("初期処理でエラー", e);
		}

		// --------------------------------------------------------------------
		// 単一レコードのINSERT
		// --------------------------------------------------------------------
		bl.insertOne();

		// --------------------------------------------------------------------
		// 単一レコードのSELECT
		// --------------------------------------------------------------------
		bl.selectOne();

		// --------------------------------------------------------------------
		// 複数レコードのSELECT
		// --------------------------------------------------------------------
		bl.selectMulti();

		// --------------------------------------------------------------------
		// セッション破棄他後処理を行う
		// --------------------------------------------------------------------
		bl.destruct();

		logger.info("---------- アプリケーション終了 ----------");
	}
}
```

続いて業務処理です。
業務処理はすべてここに集約させます。

```java:BusinessLogic.java
package jp.t.mybatis.sample.bl;

import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jp.t.mybatis.entity.UserInfo;
import jp.t.mybatis.mapper.UserMapper;

/**
 * <h1>[業務処理]</h1><br>
 *<br>
 * 業務処理は全てここに実装する。
 */
public class BusinessLogic {
	/** ロガー */
	private static Logger logger = LoggerFactory.getLogger(BusinessLogic.class);

	/** DBへのセッション */
	private SqlSession session;

	/**
	 * <h2>[初期処理]</h2><br>
	 *<br>
	 * 業務処理に必要な初期処理を行う。
	 * @throws Exception 例外
	 */
	public void init() throws Exception {
		// --------------------------------------------------------------------
		// DBに接続
		// 今回はMyBatisが主役なので、Spring連携などはせず、自力で接続する。
		// --------------------------------------------------------------------
		try(InputStream in = getClass().getResourceAsStream("/mybatis-config.xml")) {
			SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in, "ORCL");
			this.session = factory.openSession();
		} catch(Exception e) {
			logger.error("DBへの接続に失敗しました。", e);
		}
	}

	/**
	 * <h2>[INSERT]</h2><br>
	 *<br>
	 * 単一レコードをINSERTする。
	 */
	public void insertOne() {
		// --------------------------------------------------------------------
		// INSERTするオブジェクト生成
		// --------------------------------------------------------------------
		UserInfo record = new UserInfo();
		// NOはSQLで発行するので設定しない
		record.setUName("新生児");
		record.setAge(100);

		try {
			// --------------------------------------------------------------------
			// DBに登録するDATE(java.sql.Date)は作るのにひと手間必要である。
			// だたし、フォーマットが「-」区切りの"yyyy-MM-dd"形式の場合のみ、
			// 直接変換が可能である。
			// java.sql.Date sqlDate = java.sql.Date.valueOf("2024-08-20");
			// --------------------------------------------------------------------
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
			Date utilDate = sdf.parse("2024/08/20");
			record.setBDate(new java.sql.Date(utilDate.getTime()));

			// --------------------------------------------------------------------
			// INSERT
			// --------------------------------------------------------------------
			UserMapper um = this.session.getMapper(UserMapper.class);
			int ret = um.insertOne(record);

			// 単一レコードのSELECT
			session.commit();
			logger.info("INSERTの戻り値=[" + ret + "]");
		} catch (ParseException e) {
			// エラー発生時はロールバック
			session.rollback();
			logger.error("何かのエラーです。", e);
		}
	}

	/**
	 * <h2>[SELECT]</h2><br>
	 *<br>
	 * 単一レコードをSELECTする。
	 */
	public void selectOne() {
		UserMapper um = this.session.getMapper(UserMapper.class);
		// --------------------------------------------------------------------
		// SELECT
		// --------------------------------------------------------------------
		UserInfo record = um.selectOne(1, "1人目");

		// 結果がNULLでなければ表示する
		if(record != null) {
			logger.info("----- SELECT結果 -----");
			logger.info("個人番号[" + record.getUNumber() + "]");
			logger.info("氏名[" + record.getUName() + "]");
			logger.info("年齢[" + record.getAge() + "]");
			logger.info("誕生日[" + record.getBDate() + "]");
			logger.info("----- SELECT結果 -----");
		} else {
			logger.info("検索結果がNULLです。");
		}
	}

	/**
	 * <h2>[複数レコードのSELECT]</h2><br>
	 *<br>
	 * 複数レコードをSELECTする。
	 */
	public void selectMulti() {
		UserMapper um = this.session.getMapper(UserMapper.class);
		List<String> params = new ArrayList<>(){
			{
				add("1");
				add("2");
				add("5");
			}
		};
		List<UserInfo> li = um.selectMulti(params);

		li.forEach(record -> {
			logger.info("----- SELECT結果 -----");
			logger.info("個人番号[" + record.getUNumber() + "]");
			logger.info("氏名[" + record.getUName() + "]");
			logger.info("年齢[" + record.getAge() + "]");
			logger.info("誕生日[" + record.getBDate() + "]");
			logger.info("----- SELECT結果 -----");
		});
	}

	/**
	 * <h2>[終了処理]</h2><br>
	 *<br>
	 * 終了する前に実行すべきことを行う。
	 */
	public void destruct() {
		this.session.close();
	}
}
```

ここからはMyBatisでDB対する操作を実装します。
MyBatisでは、Mapperと呼ばれるIFにSELECTやINSERTのメソッドを定義し、クエリはXMLに実装します。
IFを実装したクラスは実装する必要はなく、MyBatisがそれに応じたインスタンスを暗黙的に生成して処理を呼び出してくれます。
INSERTする対象やSELECTの結果は、Entityと呼ばれるデータを持つフィールドのみのオブジェクト（POJO）を使用して受け渡しを行います。
まずは値の受け渡しを行うEntityを実装します。
今回はlombokを持っているので、lombokの機能を使いましょう。
lombokとは、フィールドに持つ変数のGetter、Setterを自動生成してくれるライブラリです。
ソースコード上見た目はそのままですが、内部的にはしっかりと存在している状態なので、問題なく呼び出しができます。
lombokの機能を使用するにはクラス宣言に@Dataを付与します。
今回初めて使ったのですが、すごく便利ですね。
大規模プロジェクトだと、Getterの参照検索ができないとかデメリットもあるのでしょうが。
```java:UserInfo.java
package jp.t.mybatis.entity;

import java.sql.Date;

import lombok.Data;

/**
 * <h1>[ユーザー情報のEntity]</h1><br>
 *<br>
 * 手抜きではないがフィールドの取得設定はLombokを使用する。
 */
@Data
public class UserInfo {
	/** 番号 */
	private long uNumber;

	/** 氏名 */
	private String uName;

	/** 年齢 */
	private long age;

	/** 誕生日 */
	private Date bDate;
}
```

続いてMapperの実装です。
前述のとおり、IFを定義します。
クエリで使用するためのパラメータはこのIF上で定義します。
```java:UserMapper.java
package jp.t.mybatis.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Param;

import jp.t.mybatis.entity.UserInfo;

/**
 * <h1>[USER_INFOテーブルのMapper]</h1><br>
 *<br>
 * MyBatisはSQLを実行するとき、Mapperと呼ばれるインターフェースを介して実行する。<br>
 * これは、USER_INFOテーブルに対する操作を行うときに使用するMapperである。<br>
 * しかし、テーブルとMapperが1対1である必要はなく、複数のテーブルを結合して行うような操作を持たせることもできる。
 */
public interface UserMapper {
	/**
	 * <h2>[単一レコードのINSERT処理]</h2><br>
	 *<br>
	 * 1件のレコードをUSER_INFOテーブルにINSERTする。<br>
	 * オブジェクト1件をパラメータとし、SQL上もフィールド名をそのまま変数名として扱う場合、@ Paramでの修飾名は不要。
	 * @param  ui INSERTするレコードのパラメータに使用するオブジェクト
	 * @return INSERT件数
	 */
	public int insertOne(UserInfo ui);

	/**
	 * <h2>[単一レコードのSELECT処理]</h2><br>
	 *<br>
	 * 1件のレコードをUSER_INFOテーブルからSELECTする。<br>
	 * SQLで使用するパラメータはMyBatisのアノテーションで指定することもできるので、ここではその方法で指定する。
	 * @param one 一つ目の条件
	 * @param two 2つ目の条件
	 * @return SELECTしたレコード
	 */
	public UserInfo selectOne(@Param("whereOen") long one, @Param("whereTwo") String two);

	/**
	 * <h2>[複数レコードのSELECT処理]</h2><br>
	 *<br>
	 * IN句を使って複数のレコードをSELECTする。<br>
	 * IN内で使用する値は、MyBatisの構文でループを使用する。<br>
	 * そのループで走査する値たちはListで指定する。
	 * @param from INに指定する1つ目
	 * @param to   INに指定する2つ目
	 * @return SELECTしたレコードのリスト
	 */
	public List<UserInfo> selectMulti(@Param("items") List<String> items);
}
```

ここまでで、Javaの実装は完了です。
次はクエリを実装します。

## クエリの実装
さて、DBに対するクエリを実装しましょう。
前述のとおり、MyBatisではXMLにクエリを実装します。
クエリを実装したXMLはSQLMapperと呼ばれ、アプリケーションで使用するSQLMapperは、mybatis-config.xmlに定義しています。
では、実装です。
どうでもいいことですが、ハイライトをXMLにするかSQLにするかは悩みどころですね。
```XML:sqlMap.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="jp.t.mybatis.mapper.UserMapper">
	<!-- SQLの定義 -->
	<!-- 単一レコードのINSERT -->
	<insert id="insertOne" parameterType="jp.t.mybatis.entity.UserInfo">
		<!--
			単一レコードをINSERTする場合は、パラメータのタイプにMapperのクラス名を完全限定名で指定する。
			また一部の例外を除き、XML内のSQLはCDATAで囲ってしまうのがよい。
			">"、"<"とかエスケープし忘れの元となってしまう。
		-->
		<![CDATA[
			INSERT INTO
			    USER_INFO (
			        UNUMBER
			      , UNAME
			      , AGE
			      , BDATE
			    ) VALUES (
			        (
			            /* ******************************************************* *
			             * 番号は欠番含む最小番号を使用する。
			             * MyBatisには関係ないが、このSQLを使うと、
			             * 連番の中で穴あきになっている番号（欠番）を見つけられる。
			             * ******************************************************* */
			            SELECT
			                -- レコードがない(NULL)の場合は1とする
                            NVL(MIN(UNUMBER + 1), 1) AS UNUMBER
			            FROM
			                USER_INFO
			            WHERE
			                (UNUMBER + 1) NOT IN (
			                    SELECT
			                        UNUMBER
			                    FROM
			                        USER_INFO
			                )
			        )
			      -- 変数を使う場合はパラメータとするクラスのフィールド名を指定する
			      , #{uName}
			      , #{age}
			      , #{bDate}
			    )
		]]>
	</insert>

	<!-- 単一レコードのSELECT  -->
	<select id="selectOne" resultType="jp.t.mybatis.entity.UserInfo" parameterType="List">
		<!--
			パラメータの引数を2つ以上にする場合、なぜかタイプはListを使用する。
			また、SQL内で使用する変数名はアノテーションの引数に指定した名前を使用する。
		-->
		<![CDATA[
			SELECT
			    *
			FROM
			    USER_INFO
			WHERE
			    UNUMBER = #{whereOen}
			and UNAME   = #{whereTwo}
		]]>
	</select>

	<!-- 複数レコードのSELECT -->
	<select id="selectMulti" resultType="jp.t.mybatis.entity.UserInfo" parameterType="List">
		<!--
			複数レコードを返却するとき、resultTypeはオブジェクトを指定しておけば勝手にListになる？
		-->
		<![CDATA[
			SELECT
			    *
			FROM
			    USER_INFO
			WHERE
			    UNUMBER IN
		]]>
				<!--
					INを使うときはXMLのforeachを使用する。
					なぜかSQLにIN(#{item1}, #{item2}のような書き方をしても効かない。
					また、あくまでもXMLの構文なので、<![CDATA[]]>の外に書かなければならない。
				-->
				<foreach item="item" collection="items" open="(" separator="," close=")">
					#{item}
				</foreach>
	</select>
</mapper>
```

## おまけの実装
おまけで、logbackの設定を載せておきます。
サンプルではコンソールしか使っていません。
```XML:logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<property name="LOGDIR" value="./log" />
	<property name="LOGROLLDIR" value="./log/hist" />

	<!-- Appender -->
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<!-- ログレベルフィルター -->
		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
			<level>INFO</level>
		</filter>

		<Target>System.out</Target>

		<encoder>
			<charset>UTF-8</charset>
			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %msg%n</pattern>
		</encoder>
	</appender>

	<!-- 汎用ロガー -->
	<logger name="jp.t.mybatis.sample" level="DEBUG">
		<appender-ref ref="STDOUT" />
	</logger>

	<!-- rootロガーは使用しない -->
	<root level="INFO" />
</configuration>
```

ここまでで、サンプルの実装は完了です。
張り切って実行してみましょう。

# 実行
こちらの実行結果は、いくつか事前にデータを投入してありますが、無事に実行できました。
```:実行結果
SLF4J(I): Connected with provider of type [ch.qos.logback.classic.spi.LogbackServiceProvider]
2024-08-31 16:11:08.762 [main] INFO  ---------- アプリケーション起動 ----------
2024-08-31 16:13:10.150 [main] INFO  INSERTの戻り値=[-2147482646]
2024-08-31 16:13:11.554 [main] INFO  ----- SELECT結果 -----
2024-08-31 16:13:11.557 [main] INFO  個人番号[1]
2024-08-31 16:13:11.557 [main] INFO  氏名[1人目]
2024-08-31 16:13:11.558 [main] INFO  年齢[34]
2024-08-31 16:13:11.559 [main] INFO  誕生日[2000-01-01]
2024-08-31 16:13:11.560 [main] INFO  ----- SELECT結果 -----
2024-08-31 16:13:12.567 [main] INFO  ----- SELECT結果 -----
2024-08-31 16:13:12.568 [main] INFO  個人番号[1]
2024-08-31 16:13:12.568 [main] INFO  氏名[1人目]
2024-08-31 16:13:12.569 [main] INFO  年齢[34]
2024-08-31 16:13:12.570 [main] INFO  誕生日[2000-01-01]
2024-08-31 16:13:12.571 [main] INFO  ----- SELECT結果 -----
2024-08-31 16:13:12.571 [main] INFO  ----- SELECT結果 -----
2024-08-31 16:13:12.571 [main] INFO  個人番号[2]
2024-08-31 16:13:12.571 [main] INFO  氏名[2人目]
2024-08-31 16:13:12.571 [main] INFO  年齢[34]
2024-08-31 16:13:12.571 [main] INFO  誕生日[2000-01-01]
2024-08-31 16:13:12.571 [main] INFO  ----- SELECT結果 -----
2024-08-31 16:13:12.571 [main] INFO  ----- SELECT結果 -----
2024-08-31 16:13:12.571 [main] INFO  個人番号[5]
2024-08-31 16:13:12.571 [main] INFO  氏名[5人目]
2024-08-31 16:13:12.572 [main] INFO  年齢[34]
2024-08-31 16:13:12.572 [main] INFO  誕生日[2000-01-01]
2024-08-31 16:13:12.572 [main] INFO  ----- SELECT結果 -----
2024-08-31 16:13:12.576 [main] INFO  ---------- アプリケーション終了 ----------
```

# おわりに
今回はMyBatisを紹介しました。
個人的には[別の記事](https://qiita.com/tarosa0001/items/8b500ff605fe9246bd28)で紹介している、MiageSQLが軽量で好みなのですが、いかんせん参考文献が少なく、新しいフレームワークとかと連携するときに考えないといけないので、いろんな要素を使う大規模なプロジェクトにはちょっと不向きです。
その点、MyBatisはSpring bootなどとの相性も良く、動作の設定も割と細かくできるので、活躍する場面は多いでしょう。
当記事が、開発者の皆さまの一助になれば幸いです。

# 参考文献
今回参考にした文献を紹介しておきます。
すみません、なにぶん何年も前にした勉強なので、当時助けてくださった文献が思い出せませんでした。
執筆してくださった筆者の方には本当に申し訳ないのですが、こちらでは、思い出せた分のみの紹介とさせていただきます。
* https://qiita.com/opengl-8080/items/a9b5d4038f19d4813ee6