Red Hat Training

A Red Hat training course is available for Red Hat JBoss Web Server

第17章 ネイティブ SQL

データベースのネイティブ SQL 方言を使ってクエリを表現することもできます。クエリヒントや Oracle の CONNECT キーワードのように、データベース独自の機能を利用したいときに使えます。SQL/JDBC ベースのアプリケーションからHibernate への明確な移行パスを提供しています。
Hibernate3 では、ストアドプロシージャなど、生成、更新、削除、読み込み処理のような手書きのSQL を指定できます。

17.1. SQLQueryを使用

ネイティブな SQL クエリの実行は SQLQuery インターフェースを通して制御します。SQLQuery インターフェースは Session.createSQLQuery() を呼び出して取得します。以下の章では、この API を使って問い合わせする方法を説明します。

17.1.1. スカラーのクエリ

最も基本的な SQL クエリはスカラー(値)のリストを得ることです。
sess.createSQLQuery("SELECT * FROM CATS").list();
sess.createSQLQuery("SELECT ID, NAME, BIRTHDATE FROM CATS").list();
これらはどちらも、 CATS テーブルの各カラムのスカラ値を含む Object 配列(Object[ ])のリストを返します。返すスカラ値の実際の順番と型を推定するために、Hibernate は ResultSetMetadata を使用します。
ResultSetMetadata を使用するオーバーヘッドを避けるため、もしくは単に何が返されるか明確にするため、addScalar() を使えます:
sess.createSQLQuery("SELECT * FROM CATS")
 .addScalar("ID", Hibernate.LONG)
 .addScalar("NAME", Hibernate.STRING)
 .addScalar("BIRTHDATE", Hibernate.DATE);
このクエリで指定されているものを下記に示します:
  • SQL クエリ文字列
  • 返されるカラムと型
これは Object 配列を返しますが、ResultSetMetdata を使用しません。ただし、その代わりに基礎にあるリザルトセットから ID、NAME、BIRTHDATE カラムをそれぞれ Long、String、Short として明示的に取得します。これは3つのカラムを返すのみであることも意味します。たとえ、クエリが * を使用し、列挙した3つより多くのカラムを返せるとしてもです。
スカラーの型情報を省くこともできます。
sess.createSQLQuery("SELECT * FROM CATS")
 .addScalar("ID", Hibernate.LONG)
 .addScalar("NAME")
 .addScalar("BIRTHDATE");
これは本質的に前と同じクエリですが、NAME と BIRTHDATE の型を決めるために ResultSetMetaData を使用します。一方、ID の型は明示的に指定されています。
Dialectの制御で、どのようにResultSetMetaData から返される java.sql.Types を Hibernate の型に マッピングされるかが決まります。特定の型がマッピングされていないか、結果の型が期待したものと異なる場合、 Dialect のregisterHibernateType を呼び出すことでカスタマイズできます。

17.1.2. エンティティのクエリ

ここまでのクエリは、すべてスカラー値を返すものでした。基本的に、リザルトセットから「未加工」の値を返します。以降では、 addEntity() により、ネイティブ SQL クエリからエンティティオブジェクトを取得する方法を示します。
sess.createSQLQuery("SELECT * FROM CATS").addEntity(Cat.class);
sess.createSQLQuery("SELECT ID, NAME, BIRTHDATE FROM CATS").addEntity(Cat.class);
このクエリで指定されているものを下記に示します:
  • SQL クエリ文字列
  • クエリが返すエンティティと SQL テーブルの別名
Cat が ID 、 NAME 、 BIRTHDATE のカラムを使ってクラスにマッピングされる場合、上記のクエリはどちらも、要素が Cat エンティティであるリストを返します。
エンティティを別のエンティティに 多対一 でマッピングしている場合は、ネイティブクエリを実行する際に、この別のエンティティを返すことも要求します。さもなければ、データベース固有の「column not found(カラムが見つかりません)」エラーが発生します。 * 表記を使用した際は、追加のカラムが自動的に返されますが、次の例のように、 Dog多対一 であることを明示することを私たちは好みます。
sess.createSQLQuery("SELECT ID, NAME, BIRTHDATE, DOG_ID FROM CATS").addEntity(Cat.class);
これにより cat.getDog() が正しく機能します。

17.1.3. 関連とコレクションの操作

プロキシを初期化するための余分な処理を避けるため、 Dog の中で即時結合できます。これは addJoin() メソッドにより行います。関連もしくはコレクションに結合できます。
sess.createSQLQuery("SELECT c.ID, NAME, BIRTHDATE, DOG_ID, D_ID, D_NAME FROM CATS c, DOGS d WHERE c.DOG_ID = d.D_ID")
 .addEntity("cat", Cat.class)
 .addJoin("dog", "cat.dog");
この例の中で、返される Cat は、データベースへの余分処理なしで、完全に初期化された dog プロパティを持ちます。結合対象のプロパティへのパスを指定できるように、別名(「cat」)を追加したことに注意してください。コレクションの即時結合も同じようにできます。たとえば、 Cat が一対多で Dog を持っていた場合などです。
sess.createSQLQuery("SELECT ID, NAME, BIRTHDATE, D_ID, D_NAME, CAT_ID FROM CATS c, DOGS d WHERE c.ID = d.CAT_ID")
 .addEntity("cat", Cat.class)
 .addJoin("dog", "cat.dogs");
今の段階では、Hibernate で使えるように SQL クエリの拡張を始めなければ、ネイティブクエリで実現できることも限界に達してしまいます。同じ型のエンティティを複数返す際や、デフォルトの別名や列名で十分ではない場合に、問題が発生する可能性があります。

17.1.4. 複数エンティティの取得

ここまでは、結果のカラム名は、マッピングドキュメントで指定したカラム名と同じであると仮定していました。複数のテーブルが同じカラム名を持つ場合があるため、複数テーブルを結合する SQL クエリで問題となる場合があります。
下記のような(失敗しそうな)クエリでは、カラム別名インジェクション(column alias injection)が必要です:
sess.createSQLQuery("SELECT c.*, m.*  FROM CATS c, CATS m WHERE c.MOTHER_ID = c.ID")
 .addEntity("cat", Cat.class)
 .addEntity("mother", Cat.class)
このクエリは、1行ごとに2つの Cat インスタンス、つまりCatとそのmotherを返すように設計されていました。しかし、インスタンスを同じカラム名にマッピングすること、つまり名前が衝突するため、このクエリは失敗します。また、データベースによっては、返されるカラムの別名が "c.ID"、"c.NAME" などの形式であり、マッピングで指定されたカラム("ID" と "NAME")と等しくないため、失敗します。
下記の形式は、カラム名が重複しても大丈夫です:
sess.createSQLQuery("SELECT {cat.*}, {mother.*}  FROM CATS c, CATS m WHERE c.MOTHER_ID = c.ID")
 .addEntity("cat", Cat.class)
 .addEntity("mother", Cat.class)
このクエリで指定されているものを下記に示します:
  • SQL クエリ文字列 (Hibernate がカラムの別名を挿入するためのプレースホルダを含む)
  • クエリによって返されるエンティティ
上記で使用している {cat.*} と {mother.*} という表記は、「すべてのプロパティ」を表す省略形です。代わりに、明示的にカラムを列挙することも可能ですが、この場合でも、Hibernate は各プロパティに対応する SQL カラムの別名を挿入します。カラムの別名のためのプレースホルダは、テーブルの別名によって修飾されたプロパティ名です。下記の例では、別のテーブル(cat_log)から マッピングメタデータデータで宣言したテーブルへCatとそのMotherをリトリーブします。where 節の中でも、プロパティの別名を使えます。
String sql = "SELECT ID as {c.id}, NAME as {c.name}, " + 
         "BIRTHDATE as {c.birthDate}, MOTHER_ID as {c.mother}, {mother.*} " +
         "FROM CAT_LOG c, CAT_LOG m WHERE {c.mother} = c.ID";

List loggedCats = sess.createSQLQuery(sql)
        .addEntity("cat", Cat.class)
        .addEntity("mother", Cat.class).list()

17.1.4.1. 別名とプロパティのリファレンス

多くの場合、上記のような別名インジェクションが必要です。複合プロパティ、継承識別子、コレクションなど、より複雑なマッピングと関連するクエリについて、Hibernate は、特定の別名を使用することにより適切な別名を挿入できます。
以下の表で、別名インジェクションを利用できる様々な方法を示しています。注記:下表の別名は一例です。それぞれの別名は一意であり、使用する際にはおそらく異なる名前を持ちます。

表17.1 別名に挿入する名前

説明構文
単純なプロパティ{[aliasname].[propertyname]A_NAME as {item.name}
複合プロパティ{[aliasname].[componentname].[propertyname]}CURRENCY as {item.amount.currency}, VALUE as {item.amount.value}
エンティティのクラスを識別する値{[aliasname].class}DISC as {item.class}
エンティティの全プロパティ{[aliasname].*}{item.*}
コレクションのキー{[aliasname].key}ORGID as {coll.key}
コレクションの ID{[aliasname].id}EMPID as {coll.id}
コレクションの要素{[aliasname].element}XID as {coll.element}
コレクションにある要素プロパティ{[aliasname].element.[propertyname]}NAME as {coll.element.name}
コレクションの要素の全プロパティ{[aliasname].element.*}{coll.element.*}
コレクションの全プロパティ{[aliasname].*}{coll.*}

17.1.5. 管理されていないエンティティの取得

ネイティブ SQL クエリに ResultTransformer を適用でき、管理されていないエンティティを返すことができるようになります。
sess.createSQLQuery("SELECT NAME, BIRTHDATE FROM CATS")
        .setResultTransformer(Transformers.aliasToBean(CatDTO.class))
このクエリで指定されているものを下記に示します:
  • SQL クエリ文字列
  • 結果を変換したもの
上記のクエリは、インスタンス化し、 NAME と BIRTHDATE の値を対応するプロパティもしくはフィールドに挿入した CatDTO のリストを返します。

17.1.6. 継承の制御

継承の一部としてマッピングされたエンティティを問い合わせるネイティブ SQL クエリは、ベースクラスとそのすべてのサブクラスの全プロパティを含まなければなりません。

17.1.7. パラメータ

ネイティブ SQL クエリは、名前付きパラメータや位置パラメータをサポートします:
Query query = sess.createSQLQuery("SELECT * FROM CATS WHERE NAME like ?").addEntity(Cat.class);
List pusList = query.setString(0, "Pus%").list();
     
query = sess.createSQLQuery("SELECT * FROM CATS WHERE NAME like :name").addEntity(Cat.class);
List pusList = query.setString("name", "Pus%").list();