EJB 3.0(Public Draft)入門記 Java Persistence API Chapter4 その8

EJB QL の SELECT節についてです。
EJB QLを使って動かしてみますが、エンティティの定義はChapter 4 その4で作成したものを使います。

4.8 SELECT Clause

カンマ区切りで複数の値を返せます。ただ、次のようにコレクションをあらわす値は返せないようです。

select o.lineItems from Order o --誤ったクエリ

あれ、でも動いちゃいました。Hibernateの実装ではOKということでしょうか。LineItemのインスタンスが入ったListが返ってきます。EJB QL的には次のようなクエリじゃなきゃ駄目だといっているんだと思います。

select l from Order o inner join o.lineItems l

どちらのEJB QLを実行しても次のようなSQL(ほかのSQLも実行されますが)が実行されます。

Hibernate: select lineitems1_.id as id1_, lineitems1_.quantity as quantity1_, lineitems1_.shipped as shipped1_, 
lineitems1_.order_id as order4_1_, lineitems1_.product_id as product5_1_ 
from ODR order0_ inner join LineItem lineitems1_ on order0_.id=lineitems1_.order_id

4.8.1 Constructor Expressions in the SELECT Clause

SELECT節にコンストラクタを指定してそのインスタンスのListを返すことができるらしいです。その際に使用するクラスはデータベースとマッピングされていなくてもいいそうです(されていてもOKです)。試してみます。

public class LineItemDetail {
  private String productName;
  private int price;
  private int quantity;
  
  public LineItemDetail(String productName, int price, int quantity) {
    this.productName = productName;
    this.price = price;
    this.quantity = quantity;
  }
  
  @Override
  public String toString() {
    return productName + ", " + price + ", " + quantity; 
  }

}

SELECT NEW節にコンストラクタを記述して、値を渡します。

select new study.ejb.entity.LineItemDetail(p.name, p.price, l.quantity) from Order o inner join o.lineItems l inner join l.product p

これでLineItemDetailのListが返ってきます。

ちょっと気になることが書いてあります。もし、SELECT NEW節にエンティティを指定した場合ですが、クエリから返ってきたエンティティのステートは「新規に作成された」(new)となるそうです。すなわち、永続コンテキストに管理されていない状態ということです(試してみたら確かにそうでした)。通常のエンティティは管理された(managed)状態で返ってくるので、違いに気をつけないといけないですね。

4.8.2 Null Valuse in the Query Result

数値のプリミティブ型で定義されたステートフィールド(state-field)はクエリの結果でNULLを作ること(含むこと?)ができないそうです。そのようなステートフィールドの型を返す
EJB QLはNULLを返してはいけないそうです。

では何が返ってくるのか?ということで、PRODUCTテーブルのpriceカラムにNULLを入れて試してみようと思ったのですが、NULLが入らない。そうか、hbm2ddlってエンティティのprimitive型のフィールドにマップされたテーブルのカラムにはNOT NULL制約つけてくれるんですね。
仕方ないので新しく別のテーブルPRODUCTX(単に後ろにXつけただけです)とエンティティクラスProductXをつくってマッピングさせることにします。
PRODUCTXテーブルのPRICEの定義にはNOT NULL制約をつけないことにします。

CREATE TABLE PRODUCTX(
  ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1)  NOT NULL PRIMARY KEY,
  NAME VARCHAR(255),
  PRICE INTEGER,
  PRODUCTTYPE VARCHAR(255))

エンティティですがProductクラスをコピペしました。名前だけをProductXにかえてあとはまったく同じです。
次にデータを入れておきます。

INSERT INTO PRODUCTX (ID, NAME, PRICE) VALUES(1, 'プロダクトX', NULL)

それからプログラムを動かして次のEJB QLを実行してみます。(デプロイはプログラムを動かしたときに勝手にやってくれるはず)

select p from ProductX p

実行結果

Hibernate: select productx0_.id as id6_, productx0_.name as name6_, productx0_.price as price6_, productx0_.productType as productT4_6_ from ProductX productx0_
Exception in thread "main" javax.ejb.EJBException: null; CausedByException is:
	could not set a field value by reflection setter of study.ejb.entity.ProductX.price
	at org.jboss.ejb3.tx.Ejb3TxPolicy.handleExceptionInOurTx(Ejb3TxPolicy.java:46)
	at org.jboss.aspects.tx.TxPolicy.invokeInOurTx(TxPolicy.java:70)
	at org.jboss.aspects.tx.TxInterceptor$Required.invoke(TxInterceptor.java:134)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.aspects.tx.TxPropagationInterceptor.invoke(TxPropagationInterceptor.java:61)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.ejb3.stateless.StatelessInstanceInterceptor.invoke(StatelessInstanceInterceptor.java:39)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.aspects.security.AuthenticationInterceptor.invoke(AuthenticationInterceptor.java:63)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.ejb3.ENCPropagationInterceptor.invoke(ENCPropagationInterceptor.java:32)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.ejb3.asynchronous.AsynchronousInterceptor.invoke(AsynchronousInterceptor.java:91)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.ejb3.stateless.StatelessContainer.localInvoke(StatelessContainer.java:163)
	at org.jboss.ejb3.stateless.StatelessLocalProxy.invoke(StatelessLocalProxy.java:60)
	at $Proxy12.print(Unknown Source)
	at study.ejb.ClientBean.main(ClientBean.java:86)
org.hibernate.PropertyAccessException: could not set a field value by reflection setter of study.ejb.entity.ProductX.price
	at org.hibernate.property.DirectPropertyAccessor$DirectSetter.set(DirectPropertyAccessor.java:82)
	at org.hibernate.tuple.AbstractEntityTuplizer.setPropertyValues(AbstractEntityTuplizer.java:330)
	at org.hibernate.tuple.PojoEntityTuplizer.setPropertyValues(PojoEntityTuplizer.java:188)
	at org.hibernate.persister.entity.AbstractEntityPersister.setPropertyValues(AbstractEntityPersister.java:3231)
	at org.hibernate.engine.TwoPhaseLoad.initializeEntity(TwoPhaseLoad.java:126)
	at org.hibernate.loader.Loader.initializeEntitiesAndCollections(Loader.java:842)
	at org.hibernate.loader.Loader.doQuery(Loader.java:717)
	at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:223)
	at org.hibernate.loader.Loader.doList(Loader.java:2147)
	at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2026)
	at org.hibernate.loader.Loader.list(Loader.java:2021)
	at org.hibernate.loader.hql.QueryLoader.list(QueryLoader.java:369)
	at org.hibernate.hql.ast.QueryTranslatorImpl.list(QueryTranslatorImpl.java:296)
	at org.hibernate.impl.SessionImpl.list(SessionImpl.java:993)
	at org.hibernate.impl.QueryImpl.list(QueryImpl.java:74)
	at org.hibernate.ejb.QueryImpl.getResultList(QueryImpl.java:38)
	at study.ejb.ClientBean.query1(ClientBean.java:56)
	at study.ejb.ClientBean.print(ClientBean.java:67)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:100)
	at org.jboss.ejb3.AllowedOperationsInterceptor.invoke(AllowedOperationsInterceptor.java:32)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.aspects.tx.TxPolicy.invokeInOurTx(TxPolicy.java:66)
	at org.jboss.aspects.tx.TxInterceptor$Required.invoke(TxInterceptor.java:134)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.aspects.tx.TxPropagationInterceptor.invoke(TxPropagationInterceptor.java:61)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.ejb3.stateless.StatelessInstanceInterceptor.invoke(StatelessInstanceInterceptor.java:39)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.aspects.security.AuthenticationInterceptor.invoke(AuthenticationInterceptor.java:63)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.ejb3.ENCPropagationInterceptor.invoke(ENCPropagationInterceptor.java:32)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.ejb3.asynchronous.AsynchronousInterceptor.invoke(AsynchronousInterceptor.java:91)
	at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:89)
	at org.jboss.ejb3.stateless.StatelessContainer.localInvoke(StatelessContainer.java:163)
	at org.jboss.ejb3.stateless.StatelessLocalProxy.invoke(StatelessLocalProxy.java:60)
	at $Proxy12.print(Unknown Source)
	at study.ejb.ClientBean.main(ClientBean.java:86)
Caused by: java.lang.IllegalArgumentException
	at sun.reflect.UnsafeIntegerFieldAccessorImpl.set(Unknown Source)
	at java.lang.reflect.Field.set(Unknown Source)
	at org.hibernate.property.DirectPropertyAccessor$DirectSetter.set(DirectPropertyAccessor.java:79)
	... 41 more

うがっ、怒られます。primitive型にNULLをセットしようとしてExceptionが起きているように見えます。ためしにProductXクラスのpriceフィールドをInteger型にしてみたら問題なく動きました。

結局、何が返ってくるか?の答えはJBoss + Hibernateでは何も返ってこない(Exceptionがおきる)ということのようです。

4.8.3 Aggregate Functions int he SELECT Clause

次の集計関数がEJB QLのSELECT節で使えるそうです。

  • AVG
  • COUNT
  • MAX
  • MIN
  • SUM

COUNT以外の集計関数はステートフィールドで終わるパス式を引数にとらないといけないようです。COUNT関数の場合は、パス式が関連フィールドでもOKです。さらにCOUNT関数は識別変数を引数に受けてもいいそうです。あれアスタリスクは指定できないのかな?Chapter4 その7でCOUNT(*)とアスタリスク渡しちゃったけどEJB QL的には識別変数を渡したほうがよかったのかも。

SUM関数やAVG関数へ渡す引数は数値でなければいけないです。MAXやMIN関数に渡す引数は順序付けが可能なステートフィールド型(数値、文字列、文字, 日付の型)でなければいけないそうです。

集計関数を使ったクエリの戻り値に含まれるJavaの型は次のようになるそうです。

  • COUNTはLongを返す
  • MAXとMINは引数に渡されたステートフィールドの型を返す
  • AVGはDoubleを返す
  • SUMはステートフィールドの型によってLong、Double、BigInteger、BigDecimalと違う型を返す。(賢い)

集計関数に適用される値がないときAVG、MAX、MIN、SUMはNULLを返し、COUNTは0を返すらしいです。DISTINCTを使って重複値を省くことができます。DISTINCTをつけようがつけまいがNULL値は除かれて集計されます。SQLと同じだと思います。(SQLのCOUNT(*)はNULLを含みますがEJB QL的にはCOUNT(*)は許さないということですね、きっと。)

COUNTを使ってみます。

select count(p) from Product p

上のEJB QLは次のようなSQLに変換されました。

Hibernate: select count(product0_.id) as col_0_0_ from Product product0_