- QueryDslは、Open Sourceのライブラリ。クエリを型安全で流れるようなインタフェースなDSL (Java内部DSL)で記述することができるライブラリで、JPAのCriteria APIに変換するDSLと、直接SQLに変換するDSLとがある。
- JPAの方については、QueryDslのBlog記事を見れば他にいうことはほとんどないので、この記事ではSQLの方について書く。
- この記事の執筆時点のバージョンはQueryDsl 2.7.2
- QueryDsl-SQLが提供する antタスク
com.mysema.query.sql.ant.AntMetaDataExporter
を用いると、データベーススキーマから Java Beanとメタクラスを生成してくれる。今回は下記のようなリレーションをもつBeanを生成した前提の例である。
- QueryDsl-SQLのDSLの始まりは、
select
,update
,delete
,insert
文にそれぞれSQLQueryImpl
,SQLUpdateClause
,SQLDeleteClause
,SQLInsertClause
クラスが対応する。 - 下記は
select
の例で、はじめにSQLQueryImpl
を生成してそこから流れるようにクエリを記述する。(QueryDslの記事より抜粋)
SQLQuery query = new SQLQueryImpl(connection, dialect);
List<String> lastNames = query.from(customer)
.where(customer.firstName.eq("Bob"))
.list(customer.lastName);
- これをもう少し使いやすくするために、このようなクラスを書いて使用してみる。
public class QueryDslSintaxSupport {
private final Connection conn;
private QueryDslSintaxSupport(Connection conn) {
this.conn = conn;
}
public static QueryDslSintaxSupport queryDsl(Connection conn) {
return new QueryDslSintaxSupport(conn);
}
public SQLQuery query() {
return new SQLQueryImpl(getConnection(), getDialect());
}
public SQLQuery queryFrom(Expression<?>... o) {
return query().from(o);
}
public SQLInsertClause insertInto(RelationalPath<?> o) {
return new SQLInsertClause(getConnection(), getDialect(), o);
}
public SQLUpdateClause update(RelationalPath<?> o) {
return new SQLUpdateClause(getConnection(), getDialect(), o);
}
public SQLDeleteClause delete(RelationalPath<?> o) {
return new SQLDeleteClause(getConnection(), getDialect(), o);
}
protected Connection getConnection() {
return conn;
}
protected SQLTemplates getDialect() {
return createDialect(getConnection());
}
private static SQLTemplates createDialect(Connection connection) {
// hsqldbの場合。
return new HSQLDBTemplates();
}
}
- 上記クラスをクライアント側のコードで staticインポートする。
import static QueryDslSintaxSupport.*
- また、メタクラスを簡単に使うために、クライアントのクラスに下記のようなstaticフィールドを定義しておく。(これも staticインポートできるようなクラスを1つ定義しておいてもよいかもしれない)
private static final QCustomer customer = QCustomer.customer;
private static final QOrder order = QOrder.order;
- このようなお膳立てを前提に、下記のDSLを読んでいただきたい
- SQL記法に似ている。
Connection con = .....
List<Order> ordersBySpecificDomain =
queryDsl(con)
.queryFrom(order)
.where(customer.email.endsWith("@specific.domain.com"))
.list(order);
- PKによるクエリなど、結果が1行であることが明らかな場合は、
list
の代わりにuniqueResult
を使う list
やuniqueResult
の引数には、SQLのSELECT
対象カラムを指定する。上記のようにメタクラスorder
を指定すれば、Order
オブジェクト(テーブルの全カラムが対象)としてクエリするし、order.item
のように文字列型のプロパティを指定すれば、item
カラムがSELECT
対象となり、クエリ結果はList<String>
で応答される。.list(order.item, order.date)
のように複数指定も可能。その場合はList<Object[]>
で応答される。- 下記のようにJOINも書ける
Connection con = .....
String item =
queryDsl(con)
.queryFrom(order)
.innerJoin(customer)
.on(order.custId.eq(customer.custId))
.where(customer.email.eq("[email protected]"))
.list(order.item);
- 上記のコードは下記のようなSQLに変換される
com.mysema
以下のログレベルをDEBUG
に設定することで、実際に変換されたSQLをログに出力してくれる様子。厳密な設定などは未確認。
SELECT order.item
FROM Order order
INNER JOIN CUSTOMER customer
ON order.cust_id = customer.cust_id
WHERE customer.email = '[email protected]'
- SQLの記法に似た書き方。
Connection con = .....
queryDsl(con).insertInto(customer)
.columns(customer.name, customer.email)
.values("YAMADA TARO", "[email protected]")
.execute();
- Beanオブジェクトをレコードとして
insert
したい場合はpopulate
を使う
Connection con = .....
Customer customer = new ......
queryDsl(con).insertInto(customer)
.populate(newCustomer)
.execute();
populate
に指定するBeanの型は特に制約はなく、いわゆるJavaBean仕様のプロパティアクセスで、columns
とvalues
の中身が決まる。それからnull
の値をもつプロパティは無視される様子。例えばid
カラムがDBで自動連番付与するようにしていた場合は、id
をnull
にしておけばINSERT
文で何も指定されなかった。- QueryDsl-SQLの
insert
については、型についての制約が緩いので、型安全はそれほど期待できない。例えば下記はコンパイルエラーにならない。 insertInto(customer).poplulate( /* Customerテーブルに Orderオブジェクト指定 */ )
insertInto(customer).columns( order.date )
// customerテーブルへのinsertにorderテーブルのカラムを指定
insertInto...columns(customer.name).values(new Date())
// nameカラム(文字列型)の値として Date(日付型)を指定
- これもSQLの記法に似ている
Connection con = .....
queryDsl(con).update(order)
.set(order.item, "new order item")
.where(order.date.before(orderedDate))
.execute();
-
where()
内の条件指定は型安全。例えば日付型プロパティの条件指定で文字列を指定するコード(例:date.before("a string")
)はコンパイルエラーとなる。 -
とはいえ、update(order).where(customer.prop)のように、テーブルと無関係のカラムを指定することはできてしまう。メソッドのシグネチャでそこまで制限するのは難しいのかも。
-
余談だが、QueryDsl-SQLでスキーマから生成したBeanは、日付型のカラムに対応して
java.sql.Date
型のプロパティとしてしまうので、日付を扱う処理はjava.util.Date
などからの変換が頻繁に発生するかもしれない。このマッピングはカスタマイズできるかもしれないが、未調査。 -
delete
も同様
Connection con = .....
queryDsl(con).delete(order)
.where(order.item.startsWith("Special"))
.execute();
- DBMSのNativeなFUNCTION呼出などもできるらしい。
- その他の機能について、詳しくはMYSEMA BLOG: Querying in SQL with QueryDsl 参照
- これまでのコード例の通り、
java.sql.Connection
の取得&解放をクライアントコード側で行う必要があるので、それがJPAなどのライブラリと比べると残念なところ。(いまさら自前でConnection管理はしたくないので裏でやってほしいのだが) - このようなニーズにこたえるべく開発中の(?)、Spring DATA JDBC Extensionsという拡張的なライブラリを発見した。下記は公式ページから引用。
There is also support for using the QueryDSL SQL module to provide type-safe query, insert, update and delete functionality.- Spring DATA JDBC Extensionsには`QueryDslJdbcTemplate`([javadoc](http://static.springsource.org/spring-data/data-jdbc/docs/1.0.0.RC1/api/org/springframework/data/jdbc/query/QueryDslJdbcTemplate.html))というクラスがあり、一応Connection管理は Spring JDBCの `JdbcTemplate`のように行ってくれるが、せっかくのQueryDsl-SQLの流れるようなインタフェースを、**途中で流れない**ようにすることで実現している。
SQLQuery query =
template.newSqlQuery()
.from(order)
.innerJoin(customer)
.on(order.custId.eq(customer.custId))
.where(customer.email.eq(customerEmail));
// ここまでは流れるよう
// しかし一旦流れが止まる
// 最後の一発だけ分離して実行
List<String> items = template.query(query, new OrderItemMappingProjection());
- しかもコード例の
new OrderItemMappingProjection()
はMappingProjection<T>
抽象クラスを実装クラス(自前実装)を生成して渡す仕様。執筆時点のバージョンは 1.0.0RC1。API設計としてもこれからなライブラリである。 - 途中で流れないようにしたのは Connectionを開放するタイミングの処理と分けるためと勝手に想像。(そうしないと裏で勝手に解放できないんじゃないかと)。これを流れるようにしたまま Spring側が解放トリガを得るには QueryDsl-SQL本体の改造や拡張が必要そう。
- 参考までに Spring DATA JDBC Extensionsには Oracle用拡張もあり、たとえば RAC対応の DataSourceクラスなどが提供されるらしい。今回はOracle使わなかったしRAC構成など作れないからパス。
- QueryDsl-SQLは、SQLクエリをJavaの内部DSLで書けるライブラリ。
- 型安全については完全ではないため、過度に信頼するものではない。
- これについては QueryDsl-JPAも同程度の様子。
where
句に閉じた条件指定の型安全は保たれる。 - ただ、実際に書いてみた感想としては、メタクラスやBeanのプロパティを EclipseなどのIDEで補完してくれるので、いちいちDBのスキーマを見なくてもクエリが書けるという点で効率的だと感じた。
- また、スキーマからBeanやメタクラスを生成すれば、スキーマ(テーブル名やカラム名)の変更をコンパイル時に検出できるので、コードに散在したSQL文を文字列検索するより確実。
- 既存テーブルやカラム名は滅多に変更しない前提とするのではなく、データベースのスキーマも、ソースコードなみにリファクタリング容易性は高く保つべきだと思う。
- 参考までに、動かしてみたときのコードはこちら
- QueryDsl-SQL使用例
- QueryDslJdbcTemplate使用例
Thanks for your info !
I found that almost of code in my class are unnecessary, if it uses the class you taught.
more or less, my class should be like below.
Using the SQLQueryFactory, my class can really concentrate on supporting query syntax.
As a query coder, I still prefer hiding instantiation of the query object.
Thanks.