jboss7.1.1 上で JPA(hibernate)を使ってみた。
「JPAのエラーは全てRuntimeExceptionのサブクラスなので例外をキャッチする必要はない」というノリでJboss7.1.1 でEJBと組み合わせてJPAを使ってみましたが、なかなか思うようにはいきませんでした。
簡単なWebアプリケーションを作成してみると同一のレコードを更新しあう競合の問題に出会います。この回避策として version 番号による方法が知られていて、JPAでは@VERSIONアノテーションで簡単に実装できます。しかし、この場合の例外もRuntimeException です。
一方、EJBはトランザクションの自動管理をしてくれますが、自動でロールバックしてくれるのは「ロールバックされる例外はRuntimeExceptionと
java.rmi.RemoteExceptionのサブクラス」なのだそうです。
EJBのロールバックの条件とJPAの例外発生の方針から、例外処理については何もしないのが正解に思えてしまいます。しかし、アプリケーションによって競合の場合はその旨を知らせて再度オペレーションを促したり、WEB画面にスタックトレースが表示されるのを避けたいことも多いはずです。また、ビジネスロジックでエラーが発生した場合もロールバックさせたいはずです。
EJBで通常のException系のエラーでEJBにロールバックさせるには、例外オブジェクトの定義に@ApplicationException(rollback = true)を記入することで実現できるそうです。今回は競合の発生時にこのユーザ定義エラーにして throw することにしました。競合の検出には、更新処理内で flush() することにより、OptimisticLockExceptionを拾う方法が一般的なようです。
また、予期せぬ例外でWEB画面にスタックトレースが表示される問題は、 EJBExceptionを拾ってメッセージに置き換えることにしました。実際のアプリケーションはどのように作られているのでしょう。
jboss7.1.1 で JPA
$ tree .
.
├── DATA
│ └── create.sh
├── jpatest
│ ├── WEB-INF
│ │ └── classes
│ │ ├── META-INF
│ │ │ ├── persistence.xml -> persistence.xml.hibernate
│ │ │ ├── persistence.xml.eclipselink
│ │ │ ├── persistence.xml.hibernate
│ │ │ └── persistence.xml.openjpa
│ │ └── jpa
│ │ ├── MyException.class
│ │ ├── Srvtest01.class
│ │ ├── VersionException.class
│ │ ├── foods.class
│ │ ├── foodsdao.class
│ │ └── foodsdaoBean.class
│ └── test01.jsp
├── jpatest.war
└── src
├── MyException.java
├── Srvtest01.java
├── build.xml
├── foods.java
├── foodsdao.java
└── foodsdaoBean.java
7 directories, 19 files
(1) foods.java
package jpa;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name="foods")
public class foods implements Serializable {
public foods(){};
@Id
@Column(name="code")
private String code;
@Column(name="name")
private String name;
@Column(name="price")
private Integer price;
@Version
@Column(name="version")
private Integer version;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
}
(2) MyException.java
package jpa;
import javax.ejb.ApplicationException;
@ApplicationException(rollback = true)
public class MyException extends Exception {
}
(3) foodsdao.java
package jpa;
import java.util.*;
public interface foodsdao {
public List allfoods();
public foods sltfoods(String code);
public void updfoods(foods foods) throws MyException;
}
(4) foodsdaoBean.java
package jpa;
import java.util.*;
import javax.ejb.*;
import javax.persistence.*;
@Stateless
@Remote
public class foodsdaoBean implements foodsdao {
@PersistenceContext(unitName="foods")
private EntityManager em;
public List allfoods() {
Query query = em.createQuery("select p from foods p");
List foods = query.getResultList();
return(foods);
}
public foods sltfoods(String code) {
return(em.find(foods.class,code));
}
public void updfoods(foods foods) throws MyException {
try {
foods foods1 = em.find(foods.class, foods.getCode());
foods foods2 = em.merge(foods);
em.flush();
}
catch(OptimisticLockException e){
throw new MyException();
}
}
}
(4) Srvtest01.java
package jpa;
import java.util.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;
import javax.ejb.*;
import javax.persistence.*;
import jpa.foodsdao;
@WebServlet(name="Srvtest01", urlPatterns={"/Srvtest01"})
public class Srvtest01 extends HttpServlet {
@EJB
private foodsdao foodsdao;
@Override
public void doGet(HttpServletRequest req,
HttpServletResponse res) throws ServletException,IOException {
res.setContentType("text/html; charset=UTF-8");
PrintWriter out = res.getWriter();
String HTML_TEXT = "</head>"
+ "<body>"
+ "<form action='Srvtest01' method='post'>"
+ "<input type='hidden' name='mode' value='select'/>"
+ "CODE:"
+ "<input type='text' name='code'/>"
+ "<input type='submit' value='検索'/>"
+ "</form>"
+ "</body>"
+ "</html>";
out.println(HTML_TEXT);
out.close();
}
@Override
public void doPost(HttpServletRequest req,
HttpServletResponse res) throws ServletException,IOException {
req.setCharacterEncoding("UTF-8");
res.setContentType("text/html; charset=UTF-8");
String mode = req.getParameter("mode");
if (mode.equals("select")) {
foods foods = null;
try {
foods = foodsdao.sltfoods(req.getParameter("code"));
}
catch(EJBException e) {
String HTML_TEXT = "</head>"
+ "<body>"
+ "検索に失敗しました(EJBException):"
+ "<a href='Srvtest01'>戻る</a>"
+ "</body>"
+ "</html>";
PrintWriter out = res.getWriter();
out.println(HTML_TEXT);
out.close();
return;
}
if (foods != null) {
String HTML_TEXT = "</head>"
+ "<body>"
+ "<form action='Srvtest01' method='post'>"
+ "<input type='hidden' name='mode' value='update'/>"
+ "CODE:%s"
+ "<input type='hidden' name='code' value='%s'/>"
+ "NAME:"
+ "<input type='text' name='name' value='%s'/>"
+ "PRICE:"
+ "<input type='text' name='price' value='%d'/>"
+ "<input type='hidden' name='version' value='%d'/>"
+ "<p>"
+ "<a href='Srvtest01'>戻る</a>"
+ "<input type='submit' value='更新'/>"
+ "</form>"
+ "</body>"
+ "</html>";
PrintWriter out = res.getWriter();
out.println(String.format(HTML_TEXT,
foods.getCode(),
foods.getCode(),
foods.getName(),
foods.getPrice(),
foods.getVersion()));
out.close();
}
else {
String HTML_TEXT = "</head>"
+ "<body>"
+ "検索に失敗しました(対象なし):"
+ "<a href='Srvtest01'>戻る</a>"
+ "</body>"
+ "</html>";
PrintWriter out = res.getWriter();
out.println(HTML_TEXT);
out.close();
}
}
else if (mode.equals("update")) {
foods foods = new foods();
foods.setCode(req.getParameter("code"));
foods.setName(req.getParameter("name"));
foods.setPrice(Integer.parseInt(req.getParameter("price")));
foods.setVersion(Integer.parseInt(req.getParameter("version")));
try {
foodsdao.updfoods(foods);
String HTML_TEXT = "</head>"
+ "<body>"
+ "更新完了しました:"
+ "<a href='Srvtest01'>戻る</a>"
+ "</body>"
+ "</html>";
PrintWriter out = res.getWriter();
out.println(HTML_TEXT);
out.close();
}
catch(MyException e) {
String HTML_TEXT = "</head>"
+ "<body>"
+ "更新失敗しました(競合):"
+ "<a href='Srvtest01'>戻る</a><br />"
+ "</body>"
+ "</html>";
PrintWriter out = res.getWriter();
out.println(HTML_TEXT);
out.close();
}
catch(EJBException e) {
String HTML_TEXT = "</head>"
+ "<body>"
+ "更新失敗しました(EJBException):"
+ "<a href='Srvtest01'>戻る</a><br />"
+ "</body>"
+ "</html>";
PrintWriter out = res.getWriter();
out.println(HTML_TEXT);
out.close();
}
}
}
}
(5) build.xml
<project name="test01" default="default">
<property name="hibernate" location="/home/ユーザ/jboss7/modules" />
<target name="default">
<javac srcdir="." destdir="../jpatest/WEB-INF/classes" >
<classpath>
<pathelement path="${hibernate}/javax/persistence/api/main/hibernate-jpa-2.0-api-1.0.1.Final.jar" />
<pathelement path="${hibernate}/javax/ejb/api/main/jboss-ejb-api_3.1_spec-1.0.1.Final.jar" />
<pathelement path="${hibernate}/javax/servlet/api/main/jboss-servlet-api_3.0_spec-1.0.0.Final.jar" />
</classpath>
</javac>
</target>
</project>
(6) persistence.xml
<?xml version="1.0" encoding="UTF-8" ?>
<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_2_0.xsd"
version="2.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="foods" transaction-type="JTA">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<jta-data-source>java:jboss/datasources/MySqlDS</jta-data-source>
<class>jpa.foods</class>
</persistence-unit>
</persistence>
(7) create.sh
#! /bin/sh
mysql -uユーザ -ppassword sampledb <<EOF
drop table if exists foods;
create table foods(
code varchar(8) primary key,
name varchar(100),
price integer,
version integer
) engine=InnoDB;
insert into foods values('000001','りんご',398,0);
insert into foods values('000002','みかん',480,0);
insert into foods values('000003','柿' ,450,0);
select * from foods;
EOF
(8) コンパイルと配備
% cd src
% ant
% cd ../jpatest
% jar cvf ../jpatest.war .
% rm ~/jboss7/standalone/deployments/jpa*
% cp ../jpatest.war ~/jboss7/standalone/deployments
% cd ../DATA
% ./create.sh
(9) 実行確認