Skip to content

Instantly share code, notes, and snippets.

@ryo-murai
Created September 18, 2012 08:56
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryo-murai/3742122 to your computer and use it in GitHub Desktop.
Save ryo-murai/3742122 to your computer and use it in GitHub Desktop.
QueryDsl-SQL概要

QueryDsl-SQL

はじめに

  • 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を生成した前提の例である。

Diagram

基本操作

  • 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を読んでいただきたい

query

  • SQL記法に似ている。
	Connection con = .....

	List<Order> ordersBySpecificDomain = 
		queryDsl(con)
			.queryFrom(order)
			.where(customer.email.endsWith("@specific.domain.com"))
			.list(order);
  • PKによるクエリなど、結果が1行であることが明らかな場合は、listの代わりにuniqueResultを使う
  • listuniqueResultの引数には、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("foo@example.com"))
			.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 = 'foo@example.com'

insert

  • SQLの記法に似た書き方。
	Connection con = .....

	queryDsl(con).insertInto(customer)
		.columns(customer.name, customer.email)
		.values("YAMADA TARO", "foo@example.com")
		.execute();
  • Beanオブジェクトをレコードとしてinsertしたい場合はpopulateを使う
	Connection con = .....
	Customer customer = new ......

	queryDsl(con).insertInto(customer)
		.populate(newCustomer)
		.execute();
  • populateに指定するBeanの型は特に制約はなく、いわゆるJavaBean仕様のプロパティアクセスで、columnsvaluesの中身が決まる。それから nullの値をもつプロパティは無視される様子。例えば idカラムがDBで自動連番付与するようにしていた場合は、idnullにしておけば 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(日付型)を指定

update / delete

  • これも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();

QueryDsl-SQLのその他の機能

Connectionの管理

  • これまでのコード例の通り、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使用例
@timowest
Copy link

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.

@ryo-murai
Copy link
Author

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.

public class QueryDslSintaxSupport {
  public static SQLQueryFactory queryDsl(Connection conn) {
      return new SQLQueryFactoryImpl (getTemplate(conn), getProvider(conn));
  }

  private static SQLTemplate getTemplate(Connection conn) {....}

  private static Provider getProvider(Connection conn) {....}
}

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment