- 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使用例
I don't understand the text, so forgive me if this is off topic, but Querydsl SQL already provides this https://github.com/mysema/querydsl/blob/master/querydsl-sql/src/main/java/com/mysema/query/sql/SQLQueryFactory.java
It is comparable to your QueryDslSintaxSupport class.