2014年1月12日日曜日

jboss7.1.1 と JPA(jhibernate)

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) 実行確認

0 件のコメント:

コメントを投稿