JavaEE5サーバ管理のEntityManagerをSeasar2から使う

(2008/03/28追記)この日記で書いてる方法だと、どうやらEntityManagerがcloseされないみたいです。あくまでEJB3のDIで取得出来るEntityManagerと同等のものをJNDIで取得出来るというだけみたい。


http://d.hatena.ne.jp/shin/20080110/p4

S2TopLinkを使えばSeasar2TopLinkが単純に連携して使えるものだと勘違いしていたようだ。
どうも、JavaEE環境ではなくTomcatなどのDB方面がJavaSEな環境で連携して使うものっぽい?

S2TopLink-JPASeasar2JPAのPersistenceUnitを作成・管理します。よって、JavaEE5サーバ管理のJPAとは連携しません。
ただ・・・そういえば、JavaEE5サーバ管理のPersistenceUnit、PersistenceContextはJNDI経由で取得することが出来るんでした。
JNDIを使えば、サーバ管理のEntityManagerをSeasar2管理のコンポーネント内で利用することも出来そうな気がします。
・・・というわけで、実験してみました。サーバはGlassFishV2、EJB3は使わず、Webアプリケーションで試しました。Seasar2はS2・S2-Tigerのみ利用。JPAサポート機能・S2TopLink等は利用していません。

persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
	<persistence-unit name="jpatest" transaction-type="JTA">
	<jta-data-source>jdbc/depot_development</jta-data-source>
	</persistence-unit>
</persistence>

データソース名でバレバレですが、railsの練習用に使っていたMySQLのテーブルをそのまま使ってます。GlassFishの管理画面からDataSourceの設定を行い、この名前で登録しています。

Users.java

package test.entity;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Users implements Serializable {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;

	@Column(name="hashed_password")
	private String hashedPassword;

	private String name;

	private String salt;

	private static final long serialVersionUID = 1L;

	public Users() {
		super();
	}

	public int getId() {
		return this.id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getHashedPassword() {
		return this.hashedPassword;
	}

	public void setHashedPassword(String hashedPassword) {
		this.hashedPassword = hashedPassword;
	}

	public String getName() {
		return this.name;
	}

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

	public String getSalt() {
		return this.salt;
	}

	public void setSalt(String salt) {
		this.salt = salt;
	}

}

これはEclipseJPAツールでテーブルから自動作成したものです。AUTO_INCREMENT設定だけ手書きで追加しました。

TestService.java

package test.service;

import java.util.List;

import test.entity.Users;

public interface TestService {

	List<Users> getUsers();
	
	Users insertUsers(String name, String password);
}

TestServiceImpl.java

package test.service.impl;

import java.util.List;

import javax.persistence.EntityManager;

import test.entity.Users;
import test.service.TestService;

public class TestServiceImpl implements TestService {
	
	private EntityManager em;

	@SuppressWarnings("unchecked")
	public List<Users> getUsers() {
		System.out.println(em);
		return (List<Users>) em.createQuery("SELECT u FROM Users u").getResultList();
	}

	public void setEm(EntityManager em) {
		this.em = em;
	}

	public Users insertUsers(String name, String password) {
		Users u = new Users();
		u.setName(name);
		u.setHashedPassword(password);
		em.persist(u);
		return u;
	}

}

EntityManagerを利用するServiceクラスです。

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="j2ee.dicon"/>
	<include path="aop.dicon"/>
	
	<component class="test.service.impl.TestServiceImpl" instance="prototype">
	<aspect>aop.traceInterceptor</aspect>
	<aspect>j2ee.requiredTx</aspect>
	</component>
	
	<component class="javax.persistence.EntityManager" instance="prototype">
	@org.seasar.extension.j2ee.JndiResourceLocator@lookup("java:comp/env/persistence-context/depot_development")
	</component>
	
</components>

EntityManagerはJNDIから取得、そのEntityManagerを使うServiceオブジェクトにトランザクションAOPを設定します。
EntityManagerはスレッドセーフではないので、EntityManager・Serviceオブジェクト共にprototypeを指定。利用するタイミングで都度新しいオブジェクトを取得するような使い方をしなくてはいけません。

s2container.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>
<component class="org.seasar.framework.container.factory.SimplePathResolver">
  <initMethod name="addRealPath">
    <arg>"jta.dicon"</arg>
    <arg>"jta-11.dicon"</arg>
  </initMethod>
</component>
</components>

jta-sun9.diconで試してみたら、TransactionManagerを取得できないというエラーが発生したので、TransactionManagerをJNDIで取得しないjta-11.diconを利用しました。この指定によって、トランザクションGlassFish側で管理されるようになります。

jdbc.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 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.OracleResultSetFactory"/>
	-->
	<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>

	<!-- from JNDI -->
	<component name="dataSource"
		class="javax.sql.DataSource">
		@org.seasar.extension.j2ee.JndiResourceLocator@lookup("java:comp/env/jdbc/depot_development")
	</component>
	
	<!--
	<component name="dataSource"
		class="org.seasar.extension.datasource.impl.SelectableDataSourceProxy"/>
	-->
</components>

JNDI用の設定を適用。

TestServlet.java

package test.servlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.seasar.framework.container.SingletonS2Container;

import test.service.TestService;

/**
 * Servlet implementation class for Servlet: TestServvlet
 * 
 */
public class TestServlet extends javax.servlet.http.HttpServlet {
	/**
	 * 
	 */
	private static final long serialVersionUID = -537911867951842664L;

	/*
	 * (non-Java-doc)
	 * 
	 * @see javax.servlet.http.HttpServlet#doGet(HttpServletRequest request,
	 *      HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		
		TestService service = SingletonS2Container.getComponent(TestService.class);
		Object list = service.getUsers();
		request.setAttribute("users", list);
		request.getRequestDispatcher("/WEB-INF/jsp/list.jsp").forward(request, response);
	}

	/*
	 * (non-Java-doc)
	 * 
	 * @see javax.servlet.http.HttpServlet#doPost(HttpServletRequest request,
	 *      HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		TestService service = SingletonS2Container.getComponent(TestService.class);
		request.setAttribute("user", service.insertUsers(request.getParameter("name"), request.getParameter("password")));
		request.getRequestDispatcher("/WEB-INF/jsp/test.jsp").forward(request, response);
	}
}

適当です。

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	id="WebApp_ID" version="2.5">
	<display-name>jpatest</display-name>
	
	<filter>
        <filter-name>s2filter</filter-name>
        <filter-class>org.seasar.framework.container.filter.S2ContainerFilter</filter-class>
    </filter>

    <filter>
        <filter-name>hotdeployfilter</filter-name>
        <filter-class>org.seasar.framework.container.hotdeploy.HotdeployFilter</filter-class>
    </filter>

    <filter>
        <filter-name>encodingfilter</filter-name>
        <filter-class>org.seasar.extension.filter.EncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>s2filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter-mapping>
        <filter-name>hotdeployfilter</filter-name>
        <url-pattern>/*</url-pattern>
        <!-- for Servlet2.4
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
        -->
    </filter-mapping>

    <filter-mapping>
        <filter-name>encodingfilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
	
    <servlet>
        <servlet-name>s2servlet</servlet-name>
        <servlet-class>org.seasar.framework.container.servlet.S2ContainerServlet</servlet-class>
        <init-param>
        	<param-name>configPath</param-name>
        	<param-value>app.dicon</param-value>
    	</init-param>
    	<init-param>
        	<param-name>debug</param-name>
        	<param-value>false</param-value>
    	</init-param>
    	<load-on-startup>1</load-on-startup>
    </servlet>
    
	<servlet>
		<description></description>
		<display-name>TestServlet</display-name>
		<servlet-name>TestServlet</servlet-name>
		<servlet-class>test.servlet.TestServlet</servlet-class>
	</servlet>

    <servlet-mapping>
        <servlet-name>s2servlet</servlet-name>
        <url-pattern>/s2servlet</url-pattern>
    </servlet-mapping>

	<servlet-mapping>
		<servlet-name>TestServlet</servlet-name>
		<url-pattern>/TestServlet</url-pattern>
	</servlet-mapping>

	<resource-ref>
		<res-ref-name>jdbc/depot_development</res-ref-name>
		<res-type>javax.sql.DataSource</res-type>
		<res-auth>Container</res-auth>
	</resource-ref>
	<persistence-unit-ref>
		<persistence-unit-ref-name>
			persistence/depot_development
		</persistence-unit-ref-name>
		<persistence-unit-name>jpatest</persistence-unit-name>
	</persistence-unit-ref>
	<persistence-context-ref>
		<persistence-context-ref-name>
			persistence-context/depot_development
		</persistence-context-ref-name>
		<persistence-unit-name>jpatest</persistence-unit-name>
	</persistence-context-ref>
</web-app>

persistence-context-refを設定することによって、EntityManagerがJNDIから取得できるようになります。persistence-unit-refはEntityManagerFactory取得用。こちらは今回は利用していません。
viewは省略。これで一応検索・登録はうまく動きました。JNDIで取得したコンテナ管理のEntityManagerは、close処理をしようとすると例外が発生するので、使い捨て状態にしています。GlassFishの場合はEntityManagerのラッパーオブジェクトを返すので、これでOK・・・なのかな? ただ、あくまでこれは実験でしかないので、本当にコンテナ管理JPAを使おうと思ったら、更なる調査・確認が必要だと思います。
ちなみに、Servlet上でEntityオブジェクトを直接利用するとClassCastExceptionが発生しました。これはEntityのエンハンスタイミングに関する問題ですが、どうやらGlassFish管理のTopLinkはこの問題に完全には対応できてないみたいです。とりあえず、Servlet上ではEntityオブジェクトを直接扱わないようにして、JSFのManagdBeanやStrutsのActionなどで処理すれば大丈夫だと思います。