Java で使える Persistence フレームワーク

Java で使える Persistence フレームワークは数多くあります。それぞれ開発された時期や目的が異なり一長一短です。実際のプロジェクトで利用したことがあるのは、Torque, iBATIS, Hibernate で、評価したことがあるのは、pBenas, Mr. Persister です。後者は、XML ファイルによる定義を嫌い、Ruby on Rails など Lightweight Language 系の ORM のアプローチに近いものです。Mr. Persister は個人的には面白いと思っています。

最近、ユーザー認証用のウェブサービスを書いた際に iBATIS を使いましたので、その使い方を簡単にまとめておきます。

iBATIS は、生成される SQL を完全に制御下に置けるフレームワークです。生成される SQL 文の品質は、利用者の SQL のスキルに依存しますが、予想できない高度な SQL 文に悩まされることはありません。

1. 準備

iBATIS のプロジェクトページから、iBATIS 2.3.0 をダウンロードします。アーカイブの lib ディレクトリの ibatis-2.3.0.677.jar をプロジェクトに加えます。また、データベースに対応する JDBC ドライバを用意します。ログ出力には commons-logging と log4j を使います。

今回は MySQL を対象としたので、次のライブラリをプロジェクトに加えました。


ibatis-2.3.0.667.jar
mysql-connector-java-5.0.7-bin.jar
commons-logging-1.1.jar
log4j-1.2.14.jar

2. 接続設定

設定ファイルの名前は任意ですが、ここでは sqlMapConfig.xml としました。データベースへの接続設定を記述し、クラスパスの通った場所に置きます。sqlMap 要素は、後述する SQL Mapping を定義するファイルです。


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<settings useStatementNamespaces="true" />
<transactionManager type="JDBC">
<dataSource type="SIMPLE">
<property name="JDBC.Driver" value="com.mysql.jdbc.Driver" />
<property name="JDBC.ConnectionURL" value="jdbc:mysql://localhost/dbname" />
<property name="JDBC.Username" value="dbuser" />
<property name="JDBC.Password" value="dbpasswd" />
</dataSource>
</transactionManager>
<sqlMap resource="sqlMapUser.xml" />
</sqlMapConfig>

com.ibatis.sqlmap.client.SqlMapClient オブジェクトを Singleton pattern で作ります。設定ファイルの名前をリソース名として指定します。


import com.ibatis.common.resources.Resources;
import com.ibatis.sqlmap.client.SqlMapClient;
import com.ibatis.sqlmap.client.SqlMapClientBuilder;

public class SqlConfig {
private static SqlMapClient sqlMap;
static {
try {
String resource = "sqlMapConfig.xml";
Reader reader = Resources.getResourceAsReader(resource);
sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader);
reader.close();
} catch (IOException e) {
logger.fatal(e);
}
}
public static SqlMapClient getSqlMapInstance() {
return sqlMap;
}
}

3. DTO の作成

問い合わせパラメータを格納する、あるいは結果セットを格納する DTO を作成します。

ここでは次のテーブルに対応する User クラスを作ります。


CREATE TABLE user (
id int(10) unsigned NOT NULL auto_increment,
email varchar(255) NOT NULL,
password varchar(255) NOT NULL,
PRIMARY KEY(id)
);


package test;
public class User implements java.io.Serializable {
private Long id;
private String email;
private String password;

// setter/getter...

}

4. SQL Mapping の作成

SQL Mapping の定義ファイルを作ります。sqlMapConfig.xml の sqlMap 要素で指定した sqlMapUser.xml は、DTO の User クラスに対応します。ここでは、select, selectByEmail, insert, updateEmail, delete を定義しています。sqlMapConfig.xml名前空間を有効にしていますので、実際には user.select, user.selectByEmail, ... となります。


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap
PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlMap namespace="user">
<select id="select" resultClass="test.User">
SELECT * FROM user WHERE id=#value#
</select>
<select id="selectByEmail" resultClass="test.User">
SELECT * FROM user WHERE email=#value#
</select>
<insert id="insert" parameterClass="test.User">
INSERT INTO user (email,password) VALUES (#email#,#password#)
<selectKey keyProperty="id" type="post" resultClass="Long">
SELECT LAST_INSERT_ID() AS value
</selectKey>
</insert>
<update id="updateEmail" parameterClass="test.User">
UPDATE user SET email=#email# WHERE id=#id#
</update>
<delete id="delete">
DELETE FROM user WHERE id=#value#
</delete>
</sqlMap>

# で囲まれたパラメータがプレースホルダになっており、結果セットを受け取る resultClass, パラメータを渡す parameterClass を指定しています。パラメータが 1 つの場合は #value# が対応します。

insert した時に発行されたキー値を参照したいことは良くありますが、insert 要素の下に selectKey 要素を記述して実現しています。

5. DAO の作成

select, selectByEmail, insert, updateEmail, delete に対応する DAO のメソッドを記述します。SQL Mapping で定義した名前は、SqlMapClient の queryForObject, insert, update, delete メソッドで呼び出します。

トランザクション制御は、SqlMapClient の startTransaction, endTransaction, commitTransaction メソッドとなります。commitTransaction しないで endTransaction を呼ぶと変更は反映されません。

iBATIS 2.3.0 では Java Generics に対応していないので、SqlMapClient#queryForObject ではキャストが必要です (次に期待)。

テーブルを加える場合は、SQL Mapping を SqlMapConfig に追加し、DTO/DAO を追加することになります。


public class UserDao {

public User getUser(Long id) throws DaoException {
User user = new User();
SqlMapClient sqlMap = SqlConfig.getSqlMapInstance();
try {
user = (User) sqlMap.queryForObject("user.select", id);
} catch (SQLException e) {
logger.fatal(e);
throw new DaoException(e, "SystemError");
}
return user;
}

public User getUserByEmail(String email) throws DaoException {
User user = new User();
SqlMapClient sqlMap = SqlConfig.getSqlMapInstance();
try {
user = (User) sqlMap.queryForObject("user.selectByEmail", email);
} catch (SQLException e) {
logger.fatal(e);
throw new DaoException(e, "SystemError");
}
return user;
}

public Long addUser(User user) throws DaoException {
Long id = new Long(0);
SqlMapClient sqlMap = SqlConfig.getSqlMapInstance();
try {
sqlMap.startTransaction();
id = (Long) sqlMap.insert("user.insert", user);
sqlMap.commitTransaction();
} catch (SQLException e) {
logger.fatal(e);
throw new DaoException(e, "SystemError");
} finally {
try {
sqlMap.endTransaction();
} catch (SQLException e) {
logger.fatal(e);
throw new DaoException(e, "SystemError");
}
}
return id;
}

public void updateEmail(User user) throws DaoException {
SqlMapClient sqlMap = SqlConfig.getSqlMapInstance();
try {
sqlMap.startTransaction();
sqlMap.update("user.updateEmail", user);
sqlMap.commitTransaction();
} catch (SQLException e) {
logger.fatal(e);
throw new DaoException(e, "SystemError");
} finally {
try {
sqlMap.endTransaction();
} catch (SQLException e) {
logger.fatal(e);
throw new DaoException(e, "SystemError");
}
}
}

public void deleteUserPerfectly(Long id) throws DaoException {
SqlMapClient sqlMap = SqlConfig.getSqlMapInstance();
try {
sqlMap.startTransaction();
sqlMap.delete("user.delete", id);
sqlMap.commitTransaction();
} catch (SQLException e) {
logger.fatal(e);
throw new DaoException(e, "SystemError");
} finally {
try {
sqlMap.endTransaction();
} catch (SQLException e) {
logger.fatal(e);
throw new DaoException(e, "SystemError");
}
}
}

}

6. ロギング

必要なログを出力します。


# SqlMap logging configuration...
#log4j.logger.com.ibatis=DEBUG
#log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=DEBUG
#log4j.logger.com.ibatis.sqlmap.engine.cache.CacheModel=DEBUG
#log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientImpl=DEBUG
#log4j.logger.com.ibatis.sqlmap.engine.builder.xml.SqlMapParser=DEBUG
#log4j.logger.com.ibatis.common.util.StopWatch=DEBUG
#log4j.logger.java.sql.Connection=DEBUG
#log4j.logger.java.sql.Statement=DEBUG
#log4j.logger.java.sql.PreparedStatement=DEBUG
#log4j.logger.java.sql.ResultSet=DEBUG

7. まとめ

この手のフレームワークは、その利用目的に合致していれば、さほど面倒なトラブルは起こらないものですが、利用目的を少し離れたところに適用しようとすると、チーム内にエキスパートが居ないとつらい事になります (ミスマッチな Hibernate の利用でそのような話を耳にします。)。

iBATIS については、それ自体は SQL Mapping の部分に徹しており (以前のバージョンは DAO も含んでいた) 見通しの良いフレームワークだと思います。

森田