taediumの日記

2007-02-19

[][] JPQLのHAVING句の変な仕様?

JPQLのHAVING句ですが、TopLinkでは以下のクエリが動きませんでした(Hibernateでは問題なく動きます)。

SELECT 
  e.department.id, 
  MAX(e.salary)  
FROM 
  Employee e
GROUP BY 
  e.department.id
HAVING 
  MAX(e.salary) > 1000 

一見、まったく問題なさそうなのですが、HAVING句に MAX(e.salary) と指定しているのがいけないみたいです。

上のJPQLを実行すると、TopLinkはJPA仕様の4.7の最後にある次の文章を例外のメッセージに含めます。

The HAVING clause must specify search conditions over the grouping items or aggregate functions that apply to grouping items.

つまり、HAVING句に指定できるのは、「GROUP BY句に指定した項目(上の例だとe.department.id)」か「GROUP BY句に指定した項目に対する集計関数(上の例だとCOUNT(e.department.id)とか)」でなければいけないと言いたいらしい。でも、GROUP BY句に指定した項目に対する集計関数って意味ないような。それにgrouping itemsって「GROUP BY句に指定した項目」を指す用語なのかなぁ?

ちなみに、ProEJB 3 のp.217のHAVING句を使ったJPQLがTopLinkでは動きませんでした。仕様じゃなくて僕が使ったTopLink(build35)の不具合のような気もしちゃいます。

おっ、これが関係あるかも。

https://glassfish.dev.java.net/issues/show_bug.cgi?id=1403

GlassFishのバージョン番号で管理されているからCurrentlyってTopLink Essentialsのどのビルドバージョンかわからない...。

2007-02-16

[] ProEJB 3(asin:1590596455) 7章の資料up

前回の勉強会では、JPQL(JPAの問い合わせ言語)から生成されたSQLEclipseのコンソールで見たりしたのですが、なんだかわかりにくかったので別のパワポにまとめました。

JPQLがどんなSQLに変換されるのかは面白いと思うのでJPAに否定的な人も肯定的な人もよかったら見てみてください。

資料は http://groups.yahoo.co.jp/group/without-ejb/ あたりからダウンロードできます。

2007-01-27

[][] Joined Subclass の バルク更新

JPQLのバルク更新はポリモーフィックです。どういうことかというと、たとえば親エンティティへのDELETE操作は子エンティティへのDELETE操作でもあるのです。その結果、マッピングによってはJPQLのDELETE文がSQLでは複数のDELETE文になることがあります。

Java EE勉強会でこのことを実際に動かして示したかったのですがうまく動かなくてお見せできませんでした。でも、家に帰ってもういちど試してみるとちゃんと動きました。警告のログが出るのですが、これを例外が発生して正常に動作していないと思ってしまったようです。

次回でもいいのですが、忘れないうちにちょっと書いておきます。以下簡単なサンプルです。

  • エンティティの定義。Joined Subclassの継承関係があります。
@Entity @Inheritance(strategy = InheritanceType.JOINED) public abstract Project {...}
@Entity public class DesignProject extends Project {...}
@Entity public class QualityProjectProject extends Project {...}
  • バルクDELETEのJPQL。
DELETE FROM Project p WHERE p.name like ('a%')
  • DBにはH2 Databaseを使います
  • 上記JPQLの実行結果(発行されるSQLとそのときの警告のログ出力)
Hibernate: insert into HT_Project select project0_.id as id from Project project0_ where name like 'a%'
Hibernate: delete from QualityProject where (id) IN (select id from HT_Project)
Hibernate: delete from DesignProject where (id) IN (select id from HT_Project)
Hibernate: delete from Project where (id) IN (select id from HT_Project)
2007-01-28 10:17:51,046 [main] WARN  org.hibernate.hql.ast.exec.MultiTableDeleteExecutor - unable to drop temporary id table after use [Timeout trying to lock table HT_PROJECT [HYT00-40]
org.h2.jdbc.JdbcSQLException: Timeout trying to lock table HT_PROJECT [HYT00-40]
	at org.h2.message.Message.getSQLException(Message.java:67)
	at org.h2.message.Message.getSQLException(Message.java:49)
	at org.h2.table.TableData.lock(TableData.java:311)
	at org.h2.command.ddl.DropTable.prepareDrop(DropTable.java:61)
	at org.h2.command.ddl.DropTable.update(DropTable.java:83)
	at org.h2.command.CommandContainer.update(CommandContainer.java:64)
	at org.h2.command.Command.executeUpdate(Command.java:120)
	at org.h2.server.TcpServerThread.process(TcpServerThread.java:209)
	at org.h2.server.TcpServerThread.run(TcpServerThread.java:86)
	at java.lang.Thread.run(Unknown Source)
 [HYT00-40]]

JPQLはひとつのDELETE文ですが、SQLでは3つのDELETE文になっています。警告は出るのですが結果には影響ないです。


バルクDELETEは次のような流れで行われるようです。

  1. 一時テーブル(ここではHT_Project)を作成
  2. 一時テーブルにJPQLで指定されたエンティティに対応するテーブル(ここではProject)の削除対象IDを格納
  3. 継承関係に属するテーブル(ここではDesignProject、QualityProject、Project)に対し一時テーブルに格納した値と同じIDをもつレコードを削除
  4. 一時テーブルを除去

2・3番目と4番目の処理は別トランザクションで実行されるのですが、2番目で一時テーブルのレコードを排他ロックしているため4番目の処理が実行できずタイムアウトとなってこれが警告として表示されるみたいです。結果、一時テーブルは残ってしまうのですが、1番目の処理ではちゃんと存在チェックを行っているのでもう一度同じ処理をしても大丈夫です。

このあたり、HibernateはDBのMetaDataやDialectを見て処理方法を変えているのですが、H2 Databaseについてはうまくいっていないっぽいです。他のDBは試していないのですけど...

[][] S2JUnit4でコンテナ作成前に環境名を変更する

Java EE勉強会でのはなし。

@Prerequisiteに指定するOGNL式はコンテナ作成前に評価されるので、これを利用できますです。ただ、最終的に評価される式はtrueにならないとテストケースの実行がスキップされてしまうので注意。

@RunWith(Seasar2.class)
public class HogeTest {

    public void hoge() throws Exception {
        assertEquals(Env.UT, Env.getValue());
    }

    @Prerequisite("@org.seasar.framework.env.Env@setFilePath('hogeEnv.txt'), true")
    public void hoge2() throws Exception {
        assertEquals("hoge", Env.getValue());
    }

}

env.txtに「ut」が指定され、hogeEnv.txtに「hoge」が指定されている場合上の2つのテストケースが成功します。

2006-07-22

[][] エンティティクラスとテーブル名

「@Tableでテーブル名を指定しなければクラス名がテーブル名になる」というは正確ではなないと言いたかったのですが、うまく説明できたようなできなかったような...。

ちょっとまとめてみます。エンティティクラスを作るときって、意識する名称が3つあるってことなのかなと思います。クラス名とエンティティ名とテーブル名です。これに関するConfiguration by Exceptionのルールは2つです。

  • エンティティ名が明示的に指定されない場合、エンティティ名はパッケージ名なしのクラス名と同じ
  • テーブル名が明示的に指定されない場合、テーブル名は(クラス名ではなく)エンティティ名と同じ

つまりパターンは4種類あります。

1.エンティティ名もテーブル名も明示的に指定されない場合

@Entity
public class Emp {
  //...
}

エンティティ名: Emp

テーブル名: EMP

クラス名: Emp


2.エンティティ名のみ明示的に指定される場合

@Entity(name = "Emp")
public class Employee {
  //...
}

エンティティ名: Emp

テーブル名: EMP

クラス名: Employee


3.テーブル名のみ明示的に指定される場合

@Entity
@Table(name="EMP")
public class Employee {
  //...
}

エンティティ名: Employee

テーブル名: EMP

クラス名: Employee


4.エンティティ名とテーブル名が明示的に指定される場合

@Entity(name="Hoge")
@Table(name="EMP")
public class Employee {
  //...
}

エンティティ名: Hoge

テーブル名: EMP

クラス名: Employee


[][] Merge実行時にSELECT文が実行されるのか?

試してみました。

使ったEntityはS2Hibernate-JPAのテストケースで使っているEmployeeクラスです。ただ、バージョンチェックがいつ行われるかの確認を行いたかったのでversionNoフィールドを追加して@Versionをつけました。(Empテーブルに対応するカラムも追加しました。)

public class MergeTest extends S2TestCase {

  private UserTransaction utx;

  private EntityManager em;

  @Override
  protected void setUp() throws Exception {
    include("javaee5.dicon");
    include("s2hibernate-jpa.dicon");
  }

  public void test() throws Exception {
    System.out.println("\n## FIND ##");
    Employee emp = em.find(Employee.class, 7369L);
    emp.setEname("hoge");
    utx.begin();
    System.out.println("\n## MERGE ##");
    em.merge(emp);
    System.out.println("\n## COMMIT ##");
    utx.commit();
  }

  public void testVersionCheck() throws Exception {
    Employee emp = em.find(Employee.class, 7369L);
    emp.setEname("hoge");
    utx.begin();
    em.merge(emp);
    em.flush();
    try {
      em.merge(emp);
      fail();
    } catch (OptimisticLockException expected) {
    }
    utx.rollback();
  }

}

上記のtestメソッドを実行したときのSQLはこんな風になりました。mergeでSELECT文が発行されていることがわかります。

## FIND ##
Hibernate: 
    select
        employee0_.empno as empno0_1_,
        employee0_.ename as ename0_1_,
        employee0_.job as job0_1_,
        employee0_.mgr as mgr0_1_,
        employee0_.hiredate as hiredate0_1_,
        employee0_.sal as sal0_1_,
        employee0_.comm as comm0_1_,
        employee0_.tstamp as tstamp0_1_,
        employee0_.deptno as deptno0_1_,
        employee0_.versionNo as versionNo0_1_,
        department1_.deptno as deptno1_0_,
        department1_.dname as dname1_0_,
        department1_.loc as loc1_0_,
        department1_.versionNo as versionNo1_0_,
        department1_.active as active1_0_ 
    from
        Emp employee0_ 
    left outer join
        Dept department1_ 
            on employee0_.deptno=department1_.deptno 
    where
        employee0_.empno=?

## MERGE ##
Hibernate: 
    select
        employee0_.empno as empno0_0_,
        employee0_.ename as ename0_0_,
        employee0_.job as job0_0_,
        employee0_.mgr as mgr0_0_,
        employee0_.hiredate as hiredate0_0_,
        employee0_.sal as sal0_0_,
        employee0_.comm as comm0_0_,
        employee0_.tstamp as tstamp0_0_,
        employee0_.deptno as deptno0_0_,
        employee0_.versionNo as versionNo0_0_ 
    from
        Emp employee0_ 
    where
        employee0_.empno=?
Hibernate: 
    select
        department0_.deptno as deptno1_0_,
        department0_.dname as dname1_0_,
        department0_.loc as loc1_0_,
        department0_.versionNo as versionNo1_0_,
        department0_.active as active1_0_ 
    from
        Dept department0_ 
    where
        department0_.deptno=?

## COMMIT ##
Hibernate: 
    update
        Emp 
    set
        ename=?,
        job=?,
        mgr=?,
        hiredate=?,
        sal=?,
        comm=?,
        tstamp=?,
        deptno=?,
        versionNo=? 
    where
        empno=? 
        and versionNo=?

testVersionCheckメソッドでは一度DBのversionNoを更新してから古いversionNoを持つEmployeeをmergeしています。commitのときではなくmergeのときにバージョンチェックが行われているみたいです。

[][] EntityNotFoundExceptionにはThrowableを受け付けるコンストラクタがない

Java EE勉強会でThrowableなクラスにThrowableを受け付けるコンストラクタがないやつがあるという話がありました。実は下に出てきたEntityNotFoundExceptionもその1つ。

http://java.sun.com/javaee/5/docs/api/javax/persistence/EntityNotFoundException.html

でも、Hibenateが使っているEntityNotFoundExceptionにはThrowableを受け付けるコンストラクタがあってHibernate内で実際に使われているんです。

http://www.hibernate.org/hib_docs/ejb3-api/javax/persistence/EntityNotFoundException.html

これって...。

S2Tigerにもjavax.persistenceパッケージのクラスを持っている(これはsunの方(JPA仕様)と同じ)のですが、S2Hibernate-JPAを使っているときに、先にS2Tigerの方のEntityNotFoundExceptionを読み込んでいると、Hibenateがこのコンストラクタを使おうとした時点でNoSuchMethodErrorが出ちゃうんですよね...

実はEntityNotFoundException以外にも、JPAで定義されているクラスとHibernateのjavax.persistenceに属するクラスで定義が違うものがいくつかあるのです。

Hibernateの3.2のfinalでちゃんとJPA仕様に合わせてくれるのでしょうか?というか合わせてくれないとまずいような気がします。

[][] FlushModeType.COMMITでSELECTすると削除されたEntityが読めるのか?

Java EE勉強会で「FlushModeType.COMMITを指定してremovedなエンティティを読もうとしたら例外が起きるということが仕様書に書いてあったような...」と言ったのですが、言った後だんだん自信がなくなってきました。

ちょっとS2Hibernate-JPAを使って試してみました。

public class FlushModeTest extends S2TestCase {

  private EntityManager em;

  @Override
  protected void setUp() throws Exception {
    include("javaee5.dicon");
    include("s2hibernate-jpa.dicon");
  }

  public void testRemoveFindTx() throws Exception {
    final Employee emp = em.find(Employee.class, 7369L);
    em.remove(emp);
    try {
      em.find(Employee.class, 7369L);
      fail();
    } catch (EntityNotFoundException expected) {
    }
  }

  public void testRemoveFindWithCommitTypeTx() throws Exception {
    em.setFlushMode(FlushModeType.COMMIT);
    final Employee emp = em.find(Employee.class, 7369L);
    em.remove(emp);
    try {
      em.find(Employee.class, 7369L);
      fail();
    } catch (EntityNotFoundException expected) {
    }
  }

  public void testRemoveSelectTx() throws Exception {
    final Employee emp = em.find(Employee.class, 7369L);
    assertNotNull(emp);
    em.remove(emp);
    try {
      em.createQuery("select e from Emp e where empno = 7369")
          .getSingleResult();
      fail();
    } catch (NoResultException expected) {
    }
  }

  public void testRemoveSelectWithCommitTypeTx() throws Exception {
    em.setFlushMode(FlushModeType.COMMIT);
    final Employee emp = em.find(Employee.class, 7369L);
    assertNotNull(emp);
    em.remove(emp);
    final Employee emp2 = (Employee) em.createQuery(
        "select e from Emp e where empno = 7369").getSingleResult();
    assertNotNull(emp2);
  }

}

結論から言うと、私の言った内容は正しくなかったですorz。仕様書にそんなことかいてないし。

まぁそれはそれとして上記のテストコードからわかることは

  • removeした後そのEntityをfindした場合はFlushModeがAUTOでもCOMMITでも読み取れない。
  • removeした後JPQLでEntityをSELECTした場合、FlushModeがAUTOのときは読み取らないが、FlushModeがCOMMITのときは読み取れてしまう(読み取れちゃうことに気をつけろ!っていうのが勉強会での話でしたね)。

あと、findってEntityNotFoundExceptio投げることがあるんですね。でもこれってJPAの仕様外のような気のせいが...。findはEntityが存在しないときはnullを返すべきじゃない?