第11章 HQL: Hibernateクエリ言語

HibernateはSQLに非常によく似た(意図的に似せた)強力な問い合わせ言語を備えています。 しかしSQLに似た構文に惑わされないでください。HQLは完全にオブジェクト指向であり、継承、ポリモーフィズム、関連といった概念を理解してください。

11.1. 大文字と小文字の区別

クエリはJavaのクラス名とプロパティ名を除いて大文字、小文字を区別しません。従ってSeLeCTsELEct と同じで、かつSELECT とも同じですがnet.sf.hibernate.eg.FOOnet.sf.hibernate.eg.Foo とは違い、かつfoo.barSetfoo.BARSETとも違います。

このマニュアルでは小文字のHQLキーワードを使用します。大文字のキーワードのクエリの方が読みやすいと感じるユーザーもいると思います。しかし我々はJavaのコードにHQLを埋め込むときには、この習慣はコードを見づらくすると考えています。

11.2. from句

可能な、もっとも単純なHibernateクエリは次の形式です。:

from eg.Cat

これは単純にeg.Catクラスのインスタンスをすべて返します。

ほとんどの場合クエリのほかの部分でCatを参照するので、エイリアスを割り当てる必要があるでしょう。

from eg.Cat as cat

このクエリはCatインスタンスにエイリアスcatを割り当てます。そのため我々はあとのクエリでこのエイリアスを使用できます。asキーワードはオプションです。つまりこのように書けます。:

from eg.Cat cat

直積、つまりクロスジョインによって多数のクラスが出現することもあります。

from Formula, Parameter
from Formula as form, Parameter as param

ローカル変数のJavaのネーミング基準と一致した、頭文字に小文字を使ったクエリ・エイリアスをつけることはいい習慣です(例えばdomesticCat)。

11.3. 関連とジョイン

関連するエンティティあるいは値コレクションの要素にさえ、ジョインを使ってエイリアスを割り当てることが出来ます。

from eg.Cat as cat 
    inner join cat.mate as mate
    left outer join cat.kittens as kitten

from eg.Cat as cat left join cat.mate.kittens as kittens

from Formula form full join form.parameter param

サポートしているジョインのタイプはANSI SQLと同じです。

  • インナー・ジョイン

  • レフト・アウター・ジョイン

  • ライト・アウター・ジョイン

  • フル・ジョイン (たいていの場合使いづらい)

インナー・ジョインレフト・アウター・ジョインライト・アウター・ジョインの構造は省略されることもあります。

from eg.Cat as cat 
    join cat.mate as mate
    left join cat.kittens as kitten

加えて、ジョインによるフェッチは関連や値のコレクションを親オブジェクトと一緒に1度のselect句で初期化します。これは特にコレクションの場合に有用です。関連とコレクションに対するマッピング定義ファイルのアウター・ジョインとlazy初期化の宣言を効果的に無効にします。

from eg.Cat as cat 
    inner join fetch cat.mate
    left join fetch cat.kittens

ジョインによるフェッチは関連するオブジェクトがwhere句(または他のどんな句でも)で使われてはならないので、通常エイリアスを割り当てる必要がありません。また関連オブジェクトは問い合わせ結果として直接返されません。代わりに親オブジェクトを通してアクセスできます。

現在の実装では、1つのクエリ中ではただひとつのコレクションのロールだけがフェッチされることに注意してください(その他のものはすべて事前にフォーマットされません)。またfetch構造はscroll()iterate()を使ったクエリ呼び出しで使用できないことにも注意してください。 最後にアウター・ジョインによるフェッチライト・ジョインによるフェッチは有用ではないことに注意してください。

11.4. Select句

select句は以下のようにどのオブジェクトと属性をクエリ・リザルトセットに返すかを選択します。:

select mate 
from eg.Cat as cat 
    inner join cat.mate as mate

上のクエリは他のCatsのmatesを選択します。実際には次のように、より簡潔に表現できます。:

select cat.mate from eg.Cat cat

特別のelements関数を利用して、コレクション要素を選択できます。次のクエリは任意のCatのkittensをすべて返します。

select elements(cat.kittens) from eg.Cat cat

クエリはコンポーネント型のプロパティを含む、あらゆるバリュー・タイプのプロパティも返せます。:

select cat.name from eg.DomesticCat cat
where cat.name like 'fri%'

select cust.name.firstName from Customer as cust

クエリは多数のオブジェクトと(または)プロパティをObject[]型の配列として返せます。

select mother, offspr, mate.name 
from eg.DomesticCat as mother
    inner join mother.mate as mate
    left outer join mother.kittens as offspr

あるいはFamilyクラスが適切なコンストラクタを持っているとするならば、

select new Family(mother, mate, offspr)
from eg.DomesticCat as mother
    join mother.mate as mate
    left join mother.kittens as offspr

タイプ・セーフなJavaオブジェクトを返せます。

11.5. 集約関数

HQLのクエリはプロパティの集約関数も返せます。

select avg(cat.weight), sum(cat.weight), max(cat.weight), count(cat)
from eg.Cat cat

コレクションはselect句の集約関数の内部にも記述できます。

select cat, count( elements(cat.kittens) ) 
from eg.Cat cat group by cat

サポートしている集約関数は以下のものです。

  • avg(...), sum(...), min(...), max(...)

  • count(*)

  • count(...), count(distinct ...), count(all...)

distinctallキーワードはSQLと同じ意味で使われ、同じ意味を持っています。

select distinct cat.name from eg.Cat cat

select count(distinct cat.name), count(cat) from eg.Cat cat

11.6. ポリモーフィックなクエリ

下のクエリはCatだけでなく、DomesticCatのようなサブクラスのインスタンスも返します。

from eg.Cat as cat

Hibernateクエリーはfrom句中で、どんなJavaクラスあるいはインターフェースも指定できます。クエリはそのクラスを拡張した、もしくはインターフェースを実装した全ての永続クラスを返します。次のクエリは永続オブジェクトをすべて返します:

from java.lang.Object o

Namedインターフェースは様々な永続クラスによって実装されます。:

from eg.Named n, eg.Named m where n.name = m.name

最後の2つのクエリは、2つ以上のSQL SELECTを要求していることに注意してください。 このことはorder by句がリザルトセット全体を正確には整列しないことを意味します(さらにそれは、Query.scroll()を使用してこれらのクエリを呼ぶことができないことを意味します。)。

11.7. where句

where句は返されるインスタンスのリストを絞ることができます。

from eg.Cat as cat where cat.name='Fritz'

上のHQLはFritzという名前のCatを返します。

select foo 
from eg.Foo foo, eg.Bar bar
where foo.startDate = bar.date

上のHQLは、FoostartDateプロパティと等しいdateプロパティを持ったbarインスタンスが存在する、すべてのFooインスタンスを返します。コンパウンド・パス表現はwhere句を非常に強力にします。

from eg.Cat cat where cat.mate.name is not null

上のクエリはテーブル・ジョイン(インナー・ジョイン)を持つSQLクエリに変換します。

from eg.Foo foo  
where foo.bar.baz.customer.address.city is not null

もし上のクエリを記述したらクエリ内に4つのテーブル・ジョインを必要とするSQLクエリに変換されます。

=オペレーターは以下のように、プロパティだけでなくインスタンスを比較するためにも使われます。

from eg.Cat cat, eg.Cat rival where cat.mate = rival.mate

select cat, mate 
from eg.Cat cat, eg.Cat mate
where cat.mate = mate

id(小文字)は特別なプロパティであり、オブジェクトのユニークな識別子に参照を付けるために使用できます。(さらに、そのプロパティ名を使用できます。)

from eg.Cat as cat where cat.id = 123

from eg.Cat as cat where cat.mate.id = 69

2番目のクエリは効率的です。テーブル・ジョインを要求されません!

また複合識別子のプロパティも使用できます。ここでPersoncountrymedicareNumberからなる複合識別子を持つと仮定します。

from bank.Person person
where person.id.country = 'AU' 
    and person.id.medicareNumber = 123456

from bank.Account account
where account.owner.id.country = 'AU' 
    and account.owner.id.medicareNumber = 123456

もう一度言いますが、2番目のクエリはテーブル・ジョインを要求されません。

同様にclassは特別なプロパティであり、ポリモーフィックな永続化におけるインスタンスの識別値にアクセスします。where句に埋め込まれたJavaのクラス名はその識別値に変換されます。

from eg.Cat cat where cat.class = eg.DomesticCat

またコンポーネントやコンポジット・ユーザ・タイプ(またそのコンポーネントのコンポーネントなど)のプロパティも指定できます。しかし決してコンポーネント・タイプのプロパティ中で終了するパス表現をしようとしないでください。例えばもしstore.owneraddressコンポーネントを持つエンティティならば以下のような結果となります。

store.owner.address.city    // OK
store.owner.address         // エラー!

"any"型は特別なプロパティであるidclassを持ち、以下の方法でジョインを表現することを可能にします(ただしAuditLog.item<any>でマッピングされたプロパティです)。

from eg.AuditLog log, eg.Payment payment 
where log.item.class = 'eg.Payment' and log.item.id = payment.id

log.item.classpayment.classが上記のクエリ中で全く異なるデータベース・カラムの値を参照するということに注意してください。

11.8. 式

SQLのwhere句で記述することが出来る式のほとんどをHQLでも記述できます。:

  • 数学的なオペレーター:+, -, *, /

  • 2項比較演算:=, >=, <=, <>, !=, like

  • 論理演算:and, or, not

  • ストリングの連結:||

  • SQLのスカラー関数:upper() , lower()など

  • グループ分けを表す括弧:( )

  • in, between, is null

  • JDBC入力パラメータ:?

  • 名前付きパラメータ::name, :start_date, :x1

  • SQL文字列:'foo', 69, '1970-01-01 10:00:01.0'

  • Javaのpublic static final定数:eg.Color.TABBY

inbetweenは以下のように使用できます。:

from eg.DomesticCat cat where cat.name between 'A' and 'B'

from eg.DomesticCat cat where cat.name in ( 'Foo', 'Bar', 'Baz' )

また、否定の形式は下のように記述されます。

from eg.DomesticCat cat where cat.name not between 'A' and 'B'

from eg.DomesticCat cat where cat.name not in ( 'Foo', 'Bar', 'Baz' )

同様にis nullis not nullはnull値をテストするために使用できます。

booleanはHibernateの設定ファイルでHQL query substitutionsを宣言することで、式として簡単に使用できます。:

<property name="hibernate.query.substitutions">true 1, false 0</property>

こうすることで下記のHQLをSQLに変換するときにtrue , falseキーワードを1 , 0に置き換えます。:

from eg.Cat cat where cat.alive = true

特別なプロパティsize、または特別な関数size()を使ってコレクションのサイズをテストできます。:

from eg.Cat cat where cat.kittens.size > 0

from eg.Cat cat where size(cat.kittens) > 0

インデックスが付けられたコレクションについては、minIndexmaxIndexを使用して最小および最大のインデックスが参照できます。同様にminElementmaxElementを使用して、基本型のコレクション要素の最小と最大が参照できます。

from Calendar cal where cal.holidays.maxElement > current date

さらに関数形式もあります。(それらは上記の構造と異なり、大文字小文字を区別しません。):

from Order order where maxindex(order.items) > 100

from Order order where minelement(order.items) > 10000

コレクションの要素やインデックスの集合(elements , indices関数)、またはサブクエリの結果が渡される場合には、SQL関数any, some, all, exists, inがサポートされます。

select mother from eg.Cat as mother, eg.Cat as kit
where kit in elements(foo.kittens)

select p from eg.NameList list, eg.Person p
where p.name = some elements(list.names)

from eg.Cat cat where exists elements(cat.kittens)

from eg.Player p where 3 > all elements(p.scores)

from eg.Show show where 'fizard' in indices(show.acts)

これらsize, elements, indices, minIndex, maxIndex, minElement, maxElementの構造には、以下の使用上の制限があることに注意してください。:

  • where句では:副問い合わせを備えたデータベースでのみ使用可能。

  • select句では:要素および索引だけが使用可能。

インデックスが付けられたコレクション(配列、list、map)の要素はindexによって引用できます(where句でのみ)。

from Order order where order.items[0].id = 1234

select person from Person person, Calendar calendar
where calendar.holidays['national day'] = person.birthDay
    and person.nationality.calendar = calendar

select item from Item item, Order order
where order.items[ order.deliveredItemIndices[0] ] = item and order.id = 11

select item from Item item, Order order
where order.items[ maxindex(order.items) ] = item and order.id = 11

[]の内部の表現は演算式も可能です。

select item from Item item, Order order
where order.items[ size(order.items) - 1 ] = item

HQLはさらにmany-to-one関連の要素、あるいは値のコレクションに対してindex()関数を用意しています。

select item, index(item) from Order order 
    join order.items item
where index(item) < 5

ベースとなるデータベースがサポートしているスカラーSQL関数が使用できます

from eg.DomesticCat cat where upper(cat.name) like 'FRI%'

もしまだ全てを理解していないなら、下のクエリをSQLでどれだけ長く、読みづらく出来るか考えてください。:

select cust
from Product prod,
    Store store
    inner join store.customers cust
where prod.name = 'widget'
    and store.location.name in ( 'Melbourne', 'Sydney' )
    and prod = all elements(cust.currentOrder.lineItems)

Hint:例えばこのように出来ます。

SELECT cust.name, cust.address, cust.phone, cust.id, cust.current_order
FROM customers cust,
    stores store,
    locations loc,
    store_customers sc,
    product prod
WHERE prod.name = 'widget'
    AND store.loc_id = loc.id
    AND loc.name IN ( 'Melbourne', 'Sydney' )
    AND sc.store_id = store.id
    AND sc.cust_id = cust.id
    AND prod.id = ALL(
        SELECT item.prod_id
        FROM line_items item, orders o
        WHERE item.order_id = o.id
            AND cust.current_order = o.id
    )

11.9. order by句

クエリによって返されたlistは、返されたクラスやコンポーネントの属性によって整列できます。:

from eg.DomesticCat cat
order by cat.name asc, cat.weight desc, cat.birthdate

オプションのascdescはそれぞれ昇順か降順の整列を示します

11.10. group by句e

集約値を返すクエリは、返されたクラスやコンポーネントの任意のプロパティによってグループ化できます。:

select cat.color, sum(cat.weight), count(cat) 
from eg.Cat cat
group by cat.color

select foo.id, avg( elements(foo.names) ), max( indices(foo.names) ) 
from eg.Foo foo
group by foo.id

注:副問い合わせのないデータベースでも、select節内部を構成するelements および indicesを使用できます。

having句も同様に可能です。

select cat.color, sum(cat.weight), count(cat) 
from eg.Cat cat
group by cat.color 
having cat.color in (eg.Color.TABBY, eg.Color.BLACK)

もしベースとなるデータベースによってサポートされているなら、having 句と order by句中でSQL関数および集計関数の使用は可能です(つまりMySQLでは使用不可能です)。

select cat
from eg.Cat cat
    join cat.kittens kitten
group by cat
having avg(kitten.weight) > 100
order by count(kitten) asc, sum(kitten.weight) desc

group by句もorder by句も演算式を含むことが出来ないことに注意してください。

11.11. 副問い合わせ

副問い合わせをサポートするデータベースについてはHibernateはクエリ内でサブクエリをサポートします。括弧(SQLの集約関数呼び出しによる事が多い)でサブクエリを囲まなければなりません。関連副問い合わせ(外部クエリ中の別名を参照するサブサブクエリのこと)さえ許可されます。

from eg.Cat as fatcat 
where fatcat.weight > ( 
    select avg(cat.weight) from eg.DomesticCat cat 
)

from eg.DomesticCat as cat 
where cat.name = some ( 
    select name.nickName from eg.Name as name 
)
    
from eg.Cat as cat 
where not exists ( 
    from eg.Cat as mate where mate.mate = cat 
)

from eg.DomesticCat as cat 
where cat.name not in ( 
    select name.nickName from eg.Name as name 
)

11.12. HQLの例

Hibernateクエリは非常に強力で複雑にできます。実際、クエリ言語の威力はHibernateの主要なセールス・ポイントの一つです。ここに最近のプロジェクトで使用したクエリと非常によく似た例があります。ほとんどのクエリはこれらの例より簡単に記述できることに注意してください!

以下のクエリは特定の顧客と与えられた最小の合計値に対する未払い注文の注文ID、商品の数、注文の合計を合計値で整列して返します。結果として返されるSQLクエリはORDER, ORDER_LINE, PRODUCT, CATALOG および PRICEテーブルに対し4つのinner joinと(関連しない)副問い合わせを持ちます。

select order.id, sum(price.amount), count(item)
from Order as order
    join order.lineItems as item
    join item.product as product,
    Catalog as catalog
    join catalog.prices as price
where order.paid = false
    and order.customer = :customer
    and price.product = product
    and catalog.effectiveDate < sysdate
    and catalog.effectiveDate >= all (
        select cat.effectiveDate 
        from Catalog as cat
        where cat.effectiveDate < sysdate
    )
group by order
having sum(price.amount) > :minAmount
order by sum(price.amount) desc

何て巨大なクエリなのでしょう!普段私は副問い合わせをあまり使いません。したがって私のクエリは実際には以下のようにします。:

select order.id, sum(price.amount), count(item)
from Order as order
    join order.lineItems as item
    join item.product as product,
    Catalog as catalog
    join catalog.prices as price
where order.paid = false
    and order.customer = :customer
    and price.product = product
    and catalog = :currentCatalog
group by order
having sum(price.amount) > :minAmount
order by sum(price.amount) desc

次のクエリは各状態の支払い数をカウントします。ただしすべての支払いが現在の利用者による最新の状態変更であるAWAITING_APPROVAL状態である場合を除きます。このクエリは2つのインナー・ジョインとPAYMENT, PAYMENT_STATUS および PAYMENT_STATUS_CHANGEテーブルに対する関連副問い合わせを備えたSQLクエリに変換されます。

select count(payment), status.name 
from Payment as payment 
    join payment.currentStatus as status
    join payment.statusChanges as statusChange
where payment.status.name <> PaymentStatus.AWAITING_APPROVAL
    or (
        statusChange.timeStamp = ( 
            select max(change.timeStamp) 
            from PaymentStatusChange change 
            where change.payment = payment
        )
        and statusChange.user <> :currentUser
    )
group by status.name, status.sortOrder
order by status.sortOrder

もし私がsetの代わりにlistとしてstatusChangesコレクションをマッピングしたならば、はるかに簡単にクエリを記述できるでしょう。

select count(payment), status.name 
from Payment as payment
    join payment.currentStatus as status
where payment.status.name <> PaymentStatus.AWAITING_APPROVAL
    or payment.statusChanges[ maxIndex(payment.statusChanges) ].user <> :currentUser
group by status.name, status.sortOrder
order by status.sortOrder

次のクエリは現在のユーザが所属する組織に対するアカウントおよび未払いの支払いをすべて返すMS SQLサーバーのisNull()関数を使用しています。このクエリは3つのインナー・ジョインと1つのアウター・ジョイン 、そしてACCOUNT, PAYMENT, PAYMENT_STATUS, ACCOUNT_TYPE, ORGANIZATION および ORG_USERテーブルに対する副問い合わせ持ったSQLに変換されます。

select account, payment
from Account as account
    left outer join account.payments as payment
where :currentUser in elements(account.holder.users)
    and PaymentStatus.UNPAID = isNull(payment.currentStatus.name, PaymentStatus.UNPAID)
order by account.type.sortOrder, account.accountNumber, payment.dueDate

いくつかのデータベースについては、(関連させられた)副問い合わせの使用を避ける必要があるでしょう。

select account, payment
from Account as account
    join account.holder.users as user
    left outer join account.payments as payment
where :currentUser = user
    and PaymentStatus.UNPAID = isNull(payment.currentStatus.name, PaymentStatus.UNPAID)
order by account.type.sortOrder, account.accountNumber, payment.dueDate

11.13. Tips & Tricks

実際に結果を返さなくてもクエリの結果数を数えることができます。:

( (Integer) session.iterate("select count(*) from ....").next() ).intValue()

コレクションのサイズにより結果を整列するためには以下のクエリを使用します。:

select usr.id, usr.name
from User as usr 
    left join usr.messages as msg
group by usr.id, usr.name
order by count(msg)

使用しているデータベースが副問い合わせをサポートする場合、クエリのwhere句でサイズによる選択条件を設定できます:

from User usr where size(usr.messages) >= 1

使用しているデータベースが副問い合わせをサポートしない場合は、次のクエリを使用してください:

select usr.id, usr.name
from User usr.name
    join usr.messages msg
group by usr.id, usr.name
having count(msg) >= 1

インナー・ジョインをしているせいで上の解決法がmessageの件数がゼロのUserを返すことができないならば、以下の形式が使えます。:

select usr.id, usr.name
from User as usr
    left join usr.messages as msg
group by usr.id, usr.name
having count(msg) = 0

JavaBeanのプロパティは、名前付きのクエリ・パラメータに結びつけることが出来ます。:

Query q = s.createQuery("from foo in class Foo where foo.name=:name and foo.size=:size");
q.setProperties(fooBean); // fooBeanはgetName()とgetSize()を持ちます
List foos = q.list();

コレクションはフィルタ付きQueryインターフェースを使用することでページをつけることができます。:

Query q = s.createFilter( collection, "" ); // the trivial filter
q.setMaxResults(PAGE_SIZE);
q.setFirstResult(PAGE_SIZE * pageNumber);
List page = q.list();

コレクションの要素はクエリ・フィルターを使って整列、グループ分けすることが出来ます。:

Collection orderedCollection = s.filter( collection, "order by this.amount" );
Collection counts = s.filter( collection, "select this.type, count(this) group by this.type" );

コレクションを初期化せずにコレクションのサイズを得ることができます。:

( (Integer) session.iterate("select count(*) from ....").next() ).intValue();