ORM比較(5) - S2JDBC

S2JDBCは、SeasarプロジェクトのS2Containerにextensionとして標準で付属してるORマッピングです。DIコンテナにSeasar2を使う場合には、ORマッピングとして一番最初に使用候補に挙がることになると思います。ただし、JPAアノテーションにはほぼ対応していますが、JPAに準拠してはいません。使い方は独自の方法を取ることになります。
公式ページは http://s2container.seasar.org/2.4/ja/s2jdbc.html になります。

今回使うライブラリは、S2-Framework,S2-Extension,S2-Tigerを使うことになります。実際に必要なライブラリは以下の通り。

  • s2-framework-2.4.35.jar (S2-Framework本体)
  • aopalliance-1.0.jar (S2-Framework依存ライブラリ)
  • commons-logging-1.1.jar (S2-Framework依存ライブラリ)
  • javassist-3.4.ga.jar (S2-Framework依存ライブラリ)
  • ognl-2.6.9-patch-20070908.jar (S2-Framework依存ライブラリ)
  • log4j-1.2.13.jar (commons-logging経由でlog4jを使用)
  • s2-extension-2.4.35.jar (S2-Extension本体)
  • geronimo-jta_1.1_spec-1.0.jar (S2Txを使う場合に必要)
  • s2-tiger-2.4.35.jar (S2-Tiger本体)
  • geronimo-jpa_3.0_spec-1.0.jar (S2JDBCに必要)
  • postgresql-8.3-603.jdbc4.jar (PostgreSQLJDBCドライバ)

それでは実際のソースを。
Entityクラスは、クラスにJPAのjavax.persistence.Entityアノテーションを付ければOKです。対応するテーブル名やカラム名などもJPAアノテーションに従います。ただし、アノテーションで指定しなかった場合のデフォルト名は後ででてくる convention.diconで指定されている、org.seasar.framework.convention.impl.PersistenceNamingConventionImpl でカスタマイズが可能になっています。
JPAと違うとしては、@ManyToOneで指定した列はJPAではエンティティの指定のみになりますが、S2JDBCでは列の値フィールドも必要になります。

ユーザマスタ(テーブル名はHUser)のEntity

@Entity
@Table(name="huser")
public class HUser {
	@Id
	@Column(name="user_id", unique=true)
	public String userId;

	@Column(name="password")
	public String password;

	@Column(name="user_group_id")
	public String userGroupId;

	@ManyToOne()
	@JoinColumn(name="user_group_id")
	public HUserGroup userGroup;
}

ユーザグループ(テーブル名はHUserGroup)のEntity

@Entity
@Table(name="huser_group")
public class HUserGroup {
	@Id
	@Column(name="user_group_id", unique=true)
	public String userGroupId;

	@OneToMany(mappedBy="userGroup")
	public List<HUser> users = new ArrayList<HUser>();
}


コネクションの作成部分は、全てdiconファイルと言われるXMLSeasarの設定ファイルに記述します。convention.dicon,creator.dicon,customizer.dicon,dbsession.diconに関しては、S2-Frameworkのresourcesにあるデフォルトのものをそのまま使いました。
メインとなる、app.dicon

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
	<include path="convention.dicon"/>
	<include path="aop.dicon"/>
	<include path="s2jdbc.dicon"/>
</components>

接続先DBの情報を記述する、s2jdbc.dicon

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
	<include path="jdbc.dicon"/>
	<include path="s2jdbc-internal.dicon"/>
	<component name="jdbcManager" class="org.seasar.extension.jdbc.manager.JdbcManagerImpl">
		<property name="maxRows">0</property>
		<property name="fetchSize">0</property>
		<property name="queryTimeout">0</property>
		<property name="dialect">postgreDialect</property>
	</component>
</components>

jdbc.diconは、デフォルトのファイルがあるのでそれを編集します。この中の URLにDBの接続情報が必要になります。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components namespace="jdbc">
	<include path="jta.dicon"/>
	<include path="jdbc-extension.dicon"
		condition="@org.seasar.framework.util.ResourceUtil@isExist('convention.dicon')"/>

	<component class="org.seasar.extension.jdbc.impl.BasicResultSetFactory"/>
	<component class="org.seasar.extension.jdbc.impl.ConfigurableStatementFactory">
		<arg>
			<component class="org.seasar.extension.jdbc.impl.BasicStatementFactory"/>
			<!--
			<component class="org.seasar.extension.jdbc.impl.BooleanToIntStatementFactory"/>
			-->
		</arg>
		<property name="fetchSize">100</property>
		<!--
		<property name="maxRows">100</property>
		-->
	</component>

	<!-- for PostgreSQL -->
	<component name="xaDataSource"
		class="org.seasar.extension.dbcp.impl.XADataSourceImpl">
		<property name="driverClassName">
			"org.postgresql.Driver"
		</property>
		<property name="URL">
		  "jdbc:postgresql://server:5432/sample_test"
		</property>
		<property name="user">"postgres"</property>
		<property name="password">"postgres"</property>
	</component>

	<component name="connectionPool"
		class="org.seasar.extension.dbcp.impl.ConnectionPoolImpl">
		<property name="timeout">600</property>
		<property name="maxPoolSize">10</property>
		<property name="allowLocalTx">true</property>
		<destroyMethod name="close"/>
	</component>

	<component name="dataSource"
		class="org.seasar.extension.dbcp.impl.DataSourceImpl"
	/>
</components>

上記の設定ができた状態で、以下のようにORMを使うことができます。
まず最初に、SingletonS2ContainerFactory#init を実行すると、クラスパスにある app.diconファイルが設定ファイルとして読み込まれます。あとは、SingletonS2Container.getComponent(JdbcManager.class) を実行すると JdbcManagerが取得できるので、型安全にデータの取得などが行えます。JPAとの相違点としては、@OneToManyが指定してあるEntityを読み込んだ際に innerJoinなどのメソッドを呼ばない限り関連Entityの取得はされません。また、Entityのキャッシュ機能は基本的にはないのでSQLの軽いラッパーとして使えます。

public class Main {
	public static void createTestData() { /*省略*/ }

	public static void main(String[] args) {
		SingletonS2ContainerFactory.init();
		try {
			if (false) createEntities();
		} catch (RuntimeException ex) {
			ex.printStackTrace();
		}

		JdbcManager jdbcManager = SingletonS2Container.getComponent(JdbcManager.class);
		List<HUserGroup> glist = jdbcManager.from(HUserGroup.class).leftOuterJoin("users").getResultList();
		for (HUserGroup group: glist) {
			System.out.println(group.userGroupId + ":");
			for (HUser user: group.users) {
				System.out.println("  " + user.userId + "/" + user.password);
			}
		}
	}
}

データの追加に関しても、保存メソッドがあるわけではなく、JdbcManager#insertもしくはJdbcManager#updateを自分で呼び分ける必要があります。どちらか意識したくない場合には、以下のようにupdateしてみて1行もupdateされなかったらinsertを行うことが推奨されているようです。

if (jdbcManager.update(entity).execute() == 0) {
	jdbcManager.insert(entity).execute()
}

上記のソースは、Subversionにて http://taoe.jpn.ch:8080/svn/trunk/orm_compare/S2JDBC から取得できます。良かったら自分で動かしてみてください*1

*1:PostgreSQLのDBが必要になります