iBATIS3

概要

iBatis3を導入する目的で評価用のサンプルコードを書いてみました。

参考にしたサイト

記載は書かれた日時が新しい順です。

Oboe吹きプログラマの黙示録

「iBATIS3 と Google guice」(2010/04/10の記事)
http://blogari.zaq.ne.jp/oboe2uran/
SqlSessionFactoryをシングルトンで使用することの問題についての考察があります。

「iBATIS3入門」(2010/03/14の記事)

http://sourceforge.jp/projects/artery/releases/46311
http://sourceforge.jp/projects/artery/downloads/46311/ibatis3.pdf/
ArteryというORマッパープロジェクト内にある、iBATIS3に関する日本語で書かれた入門書です。
分かりやすい内容でおすすめです。
xmlのにSQLのIN句を指定する方法
xmlSQLを書かずにアノテーションで指定する方法
等々についても説明があります。

公式サイトhttp://ibatis.apache.org/index.html

「User Guide (English)」(2010/02/15づけ)
http://svn.apache.org/repos/asf/ibatis/java/ibatis-3/trunk/doc/en/iBATIS-3-User-Guide.pdf
本家サイトの英語で書かれたガイドです。
最新の内容と思われますが、サンプルコードが不親切です。

CodeZine

iBATISを使ったO/RマッピングによるDBアクセスの実例」(2007/06/07の記事)
http://codezine.jp/article/detail/1289
バージョン2.3に対しての説明です。


サンプルの内容

MySQLのUSERSテーブルに対しCRUDを行う。

DROP TABLE IF EXISTS users;
CREATE TABLE users (
  user_id INT NOT NULL AUTO_INCREMENT,
  user_name VARCHAR(10),
  version INT,
  PRIMARY KEY (user_id)
) ENGINE=InnoDB;

・idは自動採番
・更新時にはvarsionで楽観的排他制御をおこなう
・varsionはSQLで自動インクリメンタル(Javaでは意識しない)

pom.xml

下記を追加

<dependency>
  <groupId>org.apache.ibatis</groupId>
  <artifactId>ibatis-sqlmap</artifactId>
  <version>3.0-beta-10</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.10</version>
</dependency>

サンプルソース構成

src/test/ibatis
    /configuration
        /AppSqlSessionFactory.java
        /iBatisConfiguration.xml

    /entity
        /User.java
        /UserId.java
        /UserMapper.xml

サンプルソース

AppSqlSessionFactory.java

package test.ibatis.configuration;

import java.io.IOException;
import java.io.Reader;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class AppSqlSessionFactory {
	private final static String XML_PATH = "test/ibatis/configuration/iBatisConfiguration.xml";

	public static SqlSessionFactory get() {
		try {
			Reader reader = Resources.getResourceAsReader(XML_PATH);
			return new SqlSessionFactoryBuilder().build(reader);
		} catch (IOException e) {
			throw new IllegalStateException(e);
		}
	}
}

iBatisConfiguration.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//ibatis.apache.org//DTD Config 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-config.dtd">
<configuration>
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<property name="driver" value="com.mysql.jdbc.Driver" />
				<property name="url" value="jdbc:mysql://localhost/iBatisTest" />
				<property name="username" value="root" />
				<property name="password" value="ponyo" />
			</dataSource>
		</environment>
	</environments>
	<mappers>
		<mapper resource="test/ibatis/entity/UserMapper.xml" />
	</mappers>
</configuration>

User.java

package test.ibatis.entity;

public class User {
	private long id;
	private String name;
	private long version;

	// 新規追加の場合
	public User(String name) {
		this(-1L, -1L);
	}

	// iBatisにより生成される場合
	public User(Long id, Long version) {
		this.id = id;
		this.version = version;
	}

	public long getId() {
		return id;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public long getVersion() {
		return version;
	}

}

UserId.java

package test.ibatis.entity;

public class UserId {
	private long id;

	public long get() {
		return id;
	}
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="USERS">

	<resultMap id="UserMapping" type="test.ibatis.entity.User">
		<constructor>
			<idArg column="user_id" javaType="long" />
			<idArg column="version" javaType="long" />
		</constructor>
		<result property="name" column="user_name" />
	</resultMap>

	<select id="findById" parameterType="long" resultType="test.ibatis.entity.User" resultMap="UserMapping" >
		SELECT user_id, user_name, version
		FROM users
		WHERE user_id = #{value}
	</select>

	<insert id="insert" parameterType="test.ibatis.entity.User">
		INSERT INTO users (user_name, version)
		VALUES (#{name},1)
	</insert>

	<select id="getInsertedId" resultType="test.ibatis.entity.UserId" >
		SELECT LAST_INSERT_ID() id;
	</select>

	<update id="updateById" parameterType="test.ibatis.entity.User">
		UPDATE users
		SET
			user_name = #{name},
			version = version + 1
		WHERE
			user_id = #{id} AND
			version = #{version}
	</update>

	<delete id="deleteById" parameterType="long">
		DELETE FROM users
		WHERE user_id = #{id}
	</delete>
</mapper>

テスト

検証のためさらにUnitTestを追加します。
pom.xmlに追加

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.8.1</version>
  <scope>test</scope>
</dependency>

UserTest.java

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.Test;

import test.ibatis.configuration.AppSqlSessionFactory;
import test.ibatis.entity.User;
import test.ibatis.entity.UserId;

public class UserTest {

	@Test
	public void test() throws Exception {
		SqlSessionFactory factory = AppSqlSessionFactory.get();
		SqlSession session = factory.openSession();

		try {

			// 新規追加
			User user = new User("ユーザ名");
			user.setName("ユーザ名");
			long count = session.insert("USERS.insert", user);
			assertEquals(1, count);// 追加件数が1件であることを確認

			// 採番されたidを取得
			UserId userId = (UserId) session.selectOne("USERS.getInsertedId");
			long id = userId.get();

			// 新規追加したUserを取得
			user = (User) session.selectOne("USERS.findById", id);
			assertEquals(id, user.getId());
			assertEquals("ユーザ名", user.getName());
			assertEquals(1, user.getVersion());

			// 更新
			user.setName("ユーザ名改");
			count = session.update("USERS.updateById", user);
			assertEquals(1, count);// 更新件数が1件であることを確認

			// 更新結果確認
			user = (User) session.selectOne("USERS.findById", id);
			assertEquals(id, user.getId());
			assertEquals("ユーザ名改", user.getName());
			assertEquals(2, user.getVersion());//バージョンがインクリメントされたことを確認

			session.commit();

		} catch (Exception e) {
			e.printStackTrace();
			session.rollback();
			fail(e.getMessage());
		} finally {
			session.close();
		}
	}

}

結果

テストが全て通過しました。うまく動いているようです。

追加説明

SqlSessionFactoryBuilder

公式サイトのユーザーズガイド(後述)には、SqlSessionFactoryBuilderについて下記のように書かれています。

This class can be instantiated, used and thrown away. There is no need to keep it around once you've
created your SqlSessionFactory. Therefore the best scope for instances of SqlSessionFactoryBuilder is
method scope (i.e. a local method variable). You can reuse the SqlSessionFactoryBuilder to build
multiple SqlSessionFactory instances, but it's still best not to keep it around to ensure that all of the XML
parsing resources are freed up for more important things.

SqlSessionFactoryBuilderのベストスコープはメソッドスコープだとありますが、ビジネスロジック(=1トランザクション)毎にnewという解釈でいいのでしょうか?
今回のサンプルでは毎回newしていますが、SqlSessionFactoryをシングルトンで管理できるならそちらの方がよさそうです。

UserId.java

usersテーブルの自動採番されたidを取得するだけのためにあるクラスです。マッパーXMLのresultMapは、テーブルのカラム と JavaBeanのフィールド が1対1で対応している場合は省略できるため、UserIdに対するresultMapは記述していません。SELECT結果をJavaBeanにマッピングする際はクラスのフィールドに直接挿入されるようで、setterは無くても大丈夫でした。