EJB 3.0(Public Draft)入門記 Java Persistence API Chapter5 その5

今日のお題は拡張永続コンテキストを使ってアプリケーショントランザクションを体験してみよう、です。


まず、アプリケーショントランザクションに拡張永続コンテキストを使用することについてHibernate In ActionやHibernate Entity Managerのドキュメントに書いてあることをまとめてみます。

  • 拡張永続コンテキストを使う利点
    • Entity Managerがflush時にバージョンチェックを自動的に行ってくれる
    • エンティティのmergeが不要(detachされないので)
    • エンティティのリロードを行う必要がない
    • JDBC Connectionの切断、接続はちゃんとおこなっている(つなぎっぱなしにはしない)
    • 状態はデータベースやWeb層に持つより中間層に持ったほうが効率的
  • 拡張永続コンテキストの欠点
    • 永続コンテキストが大きくなりすぎて格納するのに困る可能性がある
    • コミット時に永続コンテキスト内のエンティティはすでに最新でない可能性がある

一言で言えば、メモリ上に溜め込んでおいて最後にcommitする戦略と言えそうです。要するにデータアクセスの効率が良いというのが重要みたい。

ステートフルセッションBeanを使うことによって生じるうれしくないことについてはあまり触れられていませんでした。ステートフルセッションBeanの実装が非効率なアプリケーションサーバがあるから、ちゃんと調べておいたほうがいいという記述はありました。でもステートフルセッションBeanのパフォーマンスが上がれば問題は解決するんでしょうか?


拡張永続コンテキストの欠点は、上に挙げられたものに加えて、僕の思いつく限りですが次のものがあると思います。

  • アプリケーショントランザクション中データが古くなる可能性があるが、commitまでバージョンチェックしないよりはrequestのたびにバージョンをチェックしたほうがよい。(リロードが必要ないというのは利点ではない。)
  • ステートフルセッションBeanを使うことでパフォーマンスが悪くなる。
  • ステートを持つことで設計、テストが難しくなる。
  • mergeはAOPで処理できる(S2のMLでひがさんがKuinaの構想としてそのような話をしていたと思います。太田さんからもそういう話を聞いたことがありました。)
  • トランザクション外でLazy Loadingできてしまうのがイヤ。(実験してみてわかったのですが、Lazy Loadingができてしまうようです)


拡張永続コンテキストの利点/欠点はこれくらいにしておいて、では、いよいよ拡張永続コンテキストを使った場合にどんな感じのコードになるのか実験してみます。


実験のシナリオはこんな感じです。

  1. ユーザは注文を開始する。(1回)
    • システムは注文データを永続化する。
  2. ユーザは品物を購入する。(0回以上)
    • システムは購入データを作成するがこの時点では永続化しない。
  3. ユーザは注文を確定する。(1回)
    • システムは購入データを永続化する。

これらはすべて別トランザクションで処理します。


実験コードのポイントを挙げるとこんな感じ。

  • 登場エンティティは4つ。関連は次のとおり。
    • Customer (1) --- (0..*) Order (1) --- (0..*) LineItem (0..*) --- (1) Product
  • 楽観排他制御を使うためにエンティティに@Versionをつけておく。
  • DAOクラスにDIされる永続コンテキストには拡張永続コンテキストを使う。
    • そのためLogicクラスとDAOクラスはステートフルセッションBeanとする。
  • 事前データの作成と結果の確認にもPersistence APIを使う。永続コンテキストには拡張永続コンテキストを使う。(拡張永続コンテキストを使った場合トランザクションが終了していてもデータベースのアクセスができることを確認するため)


以下、コードと実行結果です。

Customer

@Entity(access = AccessType.FIELD)
public class Customer {

  @Id(generate = GeneratorType.AUTO)
  private int id;

  @Version
  private int version;

  private String name;

  public Customer() {    
  }

  public Customer(String name) {
    this.name = name;
  }
  
  //getter, setter省略
}

Product

@Entity(access=AccessType.FIELD)
public class Product {
  @Id(generate = GeneratorType.AUTO)
  private int id;

  private String name;

  private int price;

  @Version
  private int version;

  public Product() {
  }

  public Product(String name, int price) {
    this.name = name;
    this.price = price;
  }
  
  //getter, setter省略

}

Order : テーブルの名称は別のものにしました。

@Entity(access = AccessType.FIELD)
@Table(name = "ORDERDATA")
public class Order {
  @Id(generate = GeneratorType.AUTO)
  private int id;
  
  private String orderCode;
  
  @ManyToOne
  private Customer customer;
  
  @Version
  private int version;

  @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
  private Collection lineItems = new HashSet();

  public Order() {
  }

  public Order(String orderCode, Customer customer) {
    this.orderCode = orderCode;
    this.customer = customer;
  }
  
  //getter, setter省略

}


LineItem

@Entity(access = AccessType.FIELD)
public class LineItem {
  @Id(generate = GeneratorType.AUTO)
  private int id;

  private int orderSeq;

  private int quantity;

  @ManyToOne
  private Order order;

  @ManyToOne
  private Product product;

  @Version
  private int version;

  public LineItem() {
  }

  public LineItem(Order order, Product product, int quantity) {
    this.order = order;
    this.orderSeq = order.getLineItems().size();
    this.product = product;
    this.quantity = quantity;
  }
  
  //getter, setter省略
}


StatefulOrderLogic : Logicのインタフェース

public interface StatefulOrderLogic {

  void startOrder(String orderCode, Customer customer);
  
  void buy(Product product, int quantity);
  
  void checkOut();
  
  Order getOrder();
}

StatefulOrderLogicImpl : ロジックの実装

@Stateful
public class StatefulOrderLogicImpl implements StatefulOrderLogic {

  @EJB
  private StatefulOrderDao dao;

  private Order order;

  public void setDao(StatefulOrderDao dao) {
    this.dao = dao;
  }
  
  public void startOrder(String orderCode, Customer customer) {
    order = new Order(orderCode, customer);
    dao.create(order);
  }

  public void buy(Product product, int quantity) {
    LineItem item = new LineItem(order, product, quantity);
    order.getLineItems().add(item);
  }

  public Order getOrder() {
    return order;
  }

  @Remove
  public void checkOut() {
    dao.destroy();
  }
}

StatefulOrderDao : DAOのインタフェース

public interface StatefulOrderDao {
  
  void create(Order order);

  void destroy();
}

StatefulOrderDaoImpl : DAOの実装。拡張永続コンテキスト使ってます。

@Stateful
public class StatefulOrderDaoImpl implements StatefulOrderDao {
  
  @PersistenceContext(type=PersistenceContextType.EXTENDED)
  private EntityManager entityManager;
  
  public void setEntityManager(EntityManager entityManager) {
    this.entityManager = entityManager;
  }
  
  public void create(Order order) {
    entityManager.persist(order);
  }

  @Remove
  public void destroy() {
    entityManager.flush();  
  }
}

StatefulTestDaoImpl : Testデータ作成のDAOの実装。実行結果のデータ取得にも使用します。拡張永続コンテキスト使ってます。

@Stateful
public class StatefulTestDao implements TestDao {

  @PersistenceContext(type = PersistenceContextType.EXTENDED)
  private EntityManager entityManager;

  public void setEntityManager(EntityManager entityManager) {
    this.entityManager = entityManager;
  }

  public void create(Product product) {
    entityManager.persist(product);
  }

  public void create(Customer customer) {
    entityManager.persist(customer);
  }

  public void chengeOrderCode(Order order, String newOrderCode) {
    order.setOrderCode(newOrderCode);
  }

  public Order findOrderById(int id) {
    return entityManager.find(Order.class, id);
  }
  
  public List getAllOrders() {
    return entityManager.createQuery("select o from Order o")
        .getResultList();
  }
}

ExtendedPersistenceClient : Embeddable EJB 3.0 を使って上記のステートフルBeanにアクセスするクライアントです。

public class ExtendedPersistenceClient {

  public static void main(String[] args) throws Exception {
    EJB3StandaloneBootstrap.boot(null);
    EJB3StandaloneBootstrap.scanClasspath();

    ExtendedPersistenceClient client = new ExtendedPersistenceClient();
    client.order();

    EJB3StandaloneBootstrap.shutdown();
  }

  public void order() throws Exception {
    InitialContext ctx = new InitialContext();

    System.out.println("# リソースデータ準備");
    TestDao testDao = (TestDao) ctx.lookup(TestDao.class.getName());
    Customer customer = new Customer("うさはな");
    testDao.create(customer);
    Product product1 = new Product("ポテチ", 150);
    testDao.create(product1);
    Product product2 = new Product("コーラ", 100);
    testDao.create(product2);

    System.out.println("\n# 注文開始");
    StatefulOrderLogic logic = (StatefulOrderLogic) ctx.lookup(StatefulOrderLogic.class.getName());
    logic.startOrder("ORDER-001", customer);

//    System.out.println("\n# 別のセッションBeanでOrderデータを変更");
//    Order chengedTarget = testDao.findOrderById(logic.getOrder().getId());
//    testDao.chengeOrderCode(chengedTarget, "CHENGED-" +
//    chengedTarget.getOrderCode());
    
    System.out.println("\n# 購入");
    logic.buy(product1, 10);
    logic.buy(product2, 20);

    System.out.println("\n# 確定");
    logic.checkOut();

    System.out.println("\n# 確認:データベース内のOrderの表示");
    print(testDao.getAllOrders());
  }

  public void print(Order order) {
    for (LineItem item : order.getLineItems()) {
      System.out.println(
          order.getOrderCode() + "-" + item.getOrderSeq() + ", " 
          + order.getCustomer().getName() + ", " 
          + item.getProduct().getName() + ", " 
          + item.getQuantity());
    }
  }

  public void print(List orders) {
    for (Order order : orders) {
      print(order);
    }
  }
}

実行結果

# リソースデータ準備
Hibernate: insert into Customer (version, name, id) values (?, ?, null)
Hibernate: call identity()
Hibernate: insert into Product (name, price, version, id) values (?, ?, ?, null)
Hibernate: call identity()
Hibernate: insert into Product (name, price, version, id) values (?, ?, ?, null)
Hibernate: call identity()

# 注文開始
Hibernate: insert into ORDERDATA (orderCode, customer_id, version, id) values (?, ?, ?, null)
Hibernate: call identity()

# 購入

# 確定
Hibernate: insert into LineItem (orderSeq, quantity, order_id, product_id, version, id) values (?, ?, ?, ?, ?, null)
Hibernate: call identity()
Hibernate: insert into LineItem (orderSeq, quantity, order_id, product_id, version, id) values (?, ?, ?, ?, ?, null)
Hibernate: call identity()
Hibernate: update ORDERDATA set orderCode=?, customer_id=?, version=? where id=? and version=?

# 確認:データベース内のOrderの表示
Hibernate: select order0_.id as id2_, order0_.orderCode as orderCode2_, order0_.customer_id as customer4_2_, order0_.version as version2_ from ORDERDATA order0_
Hibernate: select lineitems0_.order_id as order5_2_, lineitems0_.id as id2_, lineitems0_.id as id1_1_, lineitems0_.orderSeq as orderSeq1_1_, lineitems0_.quantity as quantity1_1_, lineitems0_.order_id as order5_1_1_, lineitems0_.product_id as product6_1_1_, lineitems0_.version as version1_1_, product1_.id as id3_0_, product1_.name as name3_0_, product1_.price as price3_0_, product1_.version as version3_0_ from LineItem lineitems0_ left outer join Product product1_ on lineitems0_.product_id=product1_.id where lineitems0_.order_id=?
ORDER-001-0, うさはな, ポテチ, 10
ORDER-001-1, うさはな, コーラ, 20

実験してみてわかったことわからなかったことを書いてみます。

  • わかったこと
    • たしかにmergeせずにデータベースの更新ができる
    • flush時にちゃんとバージョンチェックが行われている。
    • 拡張永続コンテキストを使った場合、トランザクションが終了していてもLazy Loadingできる
      • 最後にデータベース内のOrderの表示をしていますが、セッションBeanの外にもかかわらずクエリが発行されています。トランザクションスコープの永続コンテキストを使った場合はここでorg.hibernate.LazyInitializationExceptionが起きます。拡張永続コンテキストを使った場合はOpen Session in ViewのServlet Filterを使わない版となるんでしょうか。
  • わからなかったこと
    • FlushModeの指定が必要かどうか。ステートフルセッションBeanを使ったときはトランザクションをコミットしてもflushしないのかも。FlushMode.NEVERの指定が必要かと思ったが使わなくてもそれっぽく動いた。でもクエリを行うとflushしてしまう。クラスへの@FlushModeやEntityManager.setFlushModeでの指定がうまく効いていないような。メソッドへの@FlushModeは意図通り。EntityManager.persist()は必ずflushしてしまう?
    • キャッシュのデータを見ないようにするにはどうするのか?データの更新前と更新後を同じEJB QLで問い合わせた場合、更新後のデータをとってこなかった。拡張永続コンテキストが関係している?
    • ステートフルセッションBeanをネストさせていいのか?EJB2.1以前の話だが無意味だからネストさせては駄目だという記述を目にしたことがある。
    • LogicクラスのcheckOut()を実行したときにORDERDATAが更新されてします。versionNoが更新されているようだがなぜ?


トランザクションスコープの永続コンテキストとdetachedなエンティティの組み合わせによるアプリケーショントランザクションも試してみないとなぁと思いつつ今回は終わりです。