Tags: java querydsl spring springboot

QueryDSL 3から4へ

Querydsl - Unified Queries for Java というO/Rマッパーがあります。去年の5月ごろにメジャーバージョンアップした4.x系統でかなり使い勝手がよくなりました。3.x系統ではspring frameworkとの連携でQueryDslJdbcTemplateというサブライブラリを使うのですが、これがなんだか使いやすいような使いにくいような微妙な感じでした。4.x系統ではその課題も克服され、これによってspringframeworkのQueryDSLサポートも無事に@Deprecatedになりました。 java packageも com.mysena.* から com.querydsl.* に変更されたようです。

サンプルコードを作ってみたので、今日はそれに沿って簡単な解説。なお、QueryDSLには、単純なSQL発行による使い方の他にも、JPAやHibernate経由で使う方法も用意されています。今回はシンプルにSQLを発行する方法です。

DBにテーブルを張って、querydsl-sql-codegen でテーブルにあわせたJavaクラスを自動生成する

"Type safety is the core principle of Querydsl."(タイプセーフであることは、QueryDSLのコア原則です) とうたうからには、DBのテーブル定義に合わせたJavaクラスが必要です。QueryDSLは、DBに接続してテーブル定義を読み出して必要なJavaクラスを自動生成する機能を提供しています。ここではmavenのプラグインを使いますが、antのタスクもあります。

サンプルコードでは、QueryDSL用のクラスを自動生成する専用のプロジェクトと、それを使う側のプロジェクトとを分けて作っています。ソースコードの自動生成を伴うフレームワークを使う場合はこうして分けておいたほうが何かと無難でしょう。普通に書くコードと自動生成されたコードとではバージョン管理する対象を分けて考えるべきだからです。


$ git clone git@github.com:nabedge/querydsl4-sample.git
$ git checkout refs/tags/20160313
$ cd querydsl-sample-db
$ mvn clean install

(省略)

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

これで、サンプルとなるテーブル定義のSQLの実行と、そのテーブルに対するQueryDSLのタイプセーフクラスの自動生成からコンパイル、jarファイル化とmavenローカルリポジトリへの格納まで、一気にできました。詳しい説明は省略しますが、このへんでsql-maven-pluginを使ってテーブル定義用SQLを一時ファイル上のh2databaseに流し込み、このへんでquerydsl-maven-pluginがそのDBに接続してテーブル定義の読み込み、そしてJavaクラスを自動生成しています。あとは通常のmavenのライフサイクルどおり、compile, test(は無いけど), package, installが続きます。

自動生成されたソースコードはquerydsl4-sample-db/target/generated-sources の配下にできます。IDE上で作業する場合にはここをソースフォルダとしてIDEに認識させる必要が有ります。 別なところでmvn package deployしてインハウスのMavenリポジトリサーバにアップロードしておく使い方でもよいでしょう。

一応、テーブル定義と初期データをここに抜粋しておきます。BookテーブルとAuthorテーブルの2つだけです。


create table author (
    id      int primary key
    ,name   varchar(512)
);

create table book (
    isbn            varchar(64) primary key
    ,title          varchar(512)
    ,author_id      int
    ,publish_date   date
    ,foreign key (author_id) references author (id)
);

insert into author (id,name) values (1, 'Arthur Conan Doyle');
insert into author (id,name) values (2, 'Haruki Murakami');
insert into author (id,name) values (3, '江戸川乱歩');
insert into author (id,name) values (4, '諫山創');

insert into book (isbn,title,author_id,publish_date) values ('001-0000000001', 'A Study in Scarlet', 1, '1887-01-01');
insert into book (isbn,title,author_id,publish_date) values ('001-0000000002', 'The Sign of Four', 1, '1890-01-01');
insert into book (isbn,title,author_id,publish_date) values ('001-8888880001', '少年探偵団', 3, '1937-01-01');
insert into book (isbn,title,author_id,publish_date) values ('001-9999990001', 'Norwegian Wood', 2, '1987-01-01');
insert into book (isbn,title,author_id,publish_date) values ('001-9999990002', '1Q84, Volumes 1-2', 2, '2009-01-01');

実際にクエリーを流すほうのプロジェクトを試してみる


$ cd querydsl-sample-core
$ mvn clean test

(省略)

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

いくつかSQLが発行されたとおぼしきログが出て、最後に "[INFO] BUILD SUCCESS" が出たら成功です。

では、SpringBootとQueryDSLによるO/Rマッピングのコーディングを見てみましょう。

Springの設定

設定用のJavaConfigはここです。QueryDSLのリファレンスのとおりなのですが、ちょっとしたしかけが入っています。


@EnableAutoConfiguration
@Configuration
public class Config {

    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        builder.setType(EmbeddedDatabaseType.H2);
        builder.addScript("classpath:sql/00_init.sql");
        EmbeddedDatabase ds = builder.build();
        net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy proxyDs = new net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy(ds);
        return new TransactionAwareDataSourceProxy(proxyDs);
    }

    @Bean
    public JdbcTemplate jdbcTempate() {
        JdbcTemplate template = new JdbcTemplate();
        template.setDataSource(dataSource());
        return template;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }

    @Bean
    public com.querydsl.sql.Configuration querydslConfiguration() {
        SQLTemplates templates = H2Templates.builder().build(); //change to your Templates
        com.querydsl.sql.Configuration configuration = new com.querydsl.sql.Configuration(templates);
        configuration.setExceptionTranslator(new SpringExceptionTranslator());
        return configuration;
    }

    @Bean
    public SQLQueryFactory sqlQueryFactory() {
        Provider<Connection> provider = new SpringConnectionProvider(dataSource());
        return new SQLQueryFactory(querydslConfiguration(), provider);
    }
}

DataSourceインスタンスを返すところで、springのTransactionAwareDataSourceProxyで包み込んでおくのがポイントです。これによってトランザクション管理をspringに任せることで、複数のO/Rマッパーを同時に使い分けることができます(後述)。


        net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy proxyDs = new net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy(ds);
        return new TransactionAwareDataSourceProxy(proxyDs);

また、net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy は開発用途のライブラリで、JDBCドライバがDBにSQLを発行する直前でそれを横取りし、発行されるSQLをログ出力させることができます。

SQLTemplatesは使用しているデータベースに合わせて選択します。 MySQL,PostgreSQL,Oracleなど主要なデータベースはほとんど用意されています。

単純なSELECT

まずは 単純なSELECTです。


    private final QAuthor qAuthor = QAuthor.author;
    private final QBook qBook = QBook.book;

    @Test
    public void simpleFetch() {
        Book book = sqlQueryFactory
                .select(qBook)
                .from(qBook)
                .where(qBook.isbn.eq("001-0000000001"))
                .fetchOne();
        log.info(book.toString());
    }

これで、こんなSQLが発行されます。

$ mvn test -Dtest=ReadTest#simpleFetch

[INFO] jdbc.sqlonly main select BOOK.AUTHOR_ID, BOOK.ISBN, BOOK.PUBLISH_DATE, BOOK.TITLE from BOOK BOOK where BOOK.ISBN 
= '001-0000000001' limit 2 

[INFO] com.example.ReadTest main authorId = 1, isbn = 001-0000000001, publishDate = 1887-01-01, title = A Study in Scarlet

sqlQueryFactoryからメソッドチェインで、SELECT, FROM, WHERE...のように、通常のSQLを書くのと同じ要領で、しかもタイプセーフに書くことができます。QBook,QAuthorがquerydsl-sample-dbプロジェクトのほうであらかじめ自動生成しておいたクラスです。

fetchOne()は1件だけ取得できることを想定したメソッドです。そのためか、発行されるSQLには "limit 2" が自動的に指定されます。2件ヒットしたら例外になります。また、もしも数百件ヒットしてしまってもリソースの無駄だし。

メソッドチェーンを使わなくてもかけます。↓ 途中でif文を挟みたいときなどは便利です。


    @Test
    public void simpleFetch2() {
        SQLQuery<Book> query = sqlQueryFactory.select(qBook);
        query.from(qBook);
        query.where(qBook.isbn.eq("001-0000000001"));
        Book book = query.fetchOne();
        log.info(book.toString());
    }

取得するカラムを指定してSELECT

上の例ではBOOKテーブルのすべてのカラムを取得していますが、カラムを指定したい場合も当然あります。


    @Test
    public void simpleFetch3() {
        SQLQuery<Tuple> query = sqlQueryFactory.select(qBook.isbn, qBook.title);
        query.from(qBook);
        query.where(qBook.isbn.eq("001-0000000001"));
        Tuple tuple = query.fetchOne();
        log.info("isbn = {}, title = {}", tuple.get(qBook.isbn), tuple.get(qBook.title));
    }

fetchOne()の戻り値が Tuple になります。Tupleから指定のカラムの値を取得する場合はタイプセーフクラスを使って該当のカラムを指定するだけです。
実行するとこうなります。


$ mvn test -Dtest=ReadTest#simpleFetch3

[INFO] jdbc.sqlonly main select BOOK.ISBN, BOOK.TITLE from BOOK BOOK where BOOK.ISBN = '001-0000000001' limit 2 

[INFO] com.example.ReadTest main isbn = 001-0000000001, title = A Study in Scarlet

isbnとtitleの2カラムだけSELECTできました。

複数件SELECT

戻り値がListになるだけです。ついでに order by 句もつけてみます。


    @Test
    public void simpleFetchList() {
        List<Book> books = sqlQueryFactory
                .select(qBook)
                .from(qBook)
                .orderBy(qBook.publishDate.asc())
                .fetch();
        books.forEach(book -> log.info(book.toString()));
    }

$ mvn test -Dtest=ReadTest#simpleFetchList

[INFO] jdbc.sqlonly main select BOOK.AUTHOR_ID, BOOK.ISBN, BOOK.PUBLISH_DATE, BOOK.TITLE from BOOK BOOK order by BOOK.PUBLISH_DATE asc 

[INFO] com.example.ReadTest main authorId = 1, isbn = 001-0000000001, publishDate = 1887-01-01, title = A Study in Scarlet
[INFO] com.example.ReadTest main authorId = 1, isbn = 001-0000000002, publishDate = 1890-01-01, title = The Sign of Four
[INFO] com.example.ReadTest main authorId = 3, isbn = 001-8888880001, publishDate = 1937-01-01, title = 少年探偵団
[INFO] com.example.ReadTest main authorId = 2, isbn = 001-9999990001, publishDate = 1987-01-01, title = Norwegian Wood
[INFO] com.example.ReadTest main authorId = 2, isbn = 001-9999990002, publishDate = 2009-01-01, title = 1Q84, Volumes 1-2

テーブルのJOIN

もちろんjoinもできます。まずはWHERE句のなかでやるやつ。


    @Test
    public void simpleJoin() {
        List<Book> books = sqlQueryFactory
                .select(qBook)
                .from(qBook, qAuthor)
                .where(
                    qBook.authorId.eq(qAuthor.id)
                    .and(qAuthor.name.eq("Haruki Murakami"))
                )
                .fetch();
        books.forEach(book -> log.info(book.toString()));
    }
$ mvn test -Dtest=ReadTest#simpleJoin

[INFO] jdbc.sqlonly main select BOOK.AUTHOR_ID, BOOK.ISBN, BOOK.PUBLISH_DATE, BOOK.TITLE from BOOK BOOK, AUTHOR AUTHOR 
where BOOK.AUTHOR_ID = AUTHOR.ID and AUTHOR.NAME = 'Haruki Murakami' 

[INFO] com.example.ReadTest main authorId = 2, isbn = 001-9999990001, publishDate = 1987-01-01, title = Norwegian Wood
[INFO] com.example.ReadTest main authorId = 2, isbn = 001-9999990002, publishDate = 2009-01-01, title = 1Q84, Volumes 1-2

whereメソッドを複数あてることで、上と同じことができます。WHERE句のなかではand条件になります。


    @Test
    public void simpleJoin2() {
        List<Book> books = sqlQueryFactory
                .select(qBook)
                .from(qBook, qAuthor)
                // two "where" clause becomes to " foo AND bar "
                .where(qBook.authorId.eq(qAuthor.id))
                .where(qAuthor.name.eq("Haruki Murakami"))
                .fetch();
        books.forEach(book -> log.info(book.toString()));
    }

独自のJavaBeanにマッピングする。(ついでにjoin句を使う)

ここまでは、QueryDSLで自動生成した、1テーブル=1クラスのJavaBeanにクエリーの結果をマッピングしていましたが、自作のJavaBeanにあてることももちろん可能です。ついでにjoin句を使ってみましょう。


public class BookAndAuthorDTO extends Book {
    
    private static final long serialVersionUID = 1L;
    
    private String authorName;

    public String getAuthorName() {
        return authorName;
    }

    public void setAuthorName(String authorName) {
        this.authorName = authorName;
    }

}

自作したクラスにSELECTの結果を格納するには、Projections.bean()を使います。beanのプロパティ名とSELECTの結果を合わせるためにas()句で名前を合わせます.


    @Test
    public void select_inner_join() {
        SQLQuery<BookAndAuthorDTO> query = sqlQueryFactory.select(
                Projections.bean(BookAndAuthorDTO.class,
                        qBook.isbn,
                        qBook.title,
                        qAuthor.name.as("authorName"),
                        qBook.publishDate));
        query.from(qBook);
        query.innerJoin(qAuthor).on(qAuthor.id.eq(qBook.authorId));
        query.where(qBook.authorId.eq(qAuthor.id));
        query.orderBy(qBook.publishDate.asc(), qAuthor.id.asc());

        List<BookAndAuthorDTO> list = query.fetch();
        list.stream().forEach(e -> {
            log.debug("---------");
            log.debug("isbn: {}", e.getIsbn());
            log.debug("title: {}", e.getTitle());
            log.debug("author: {}", e.getAuthorName());
            log.debug("publishDate: {}", e.getPublishDate());
        });
    }

$ mvn test -Dtest=ReadTest#select_inner_join

[INFO] jdbc.sqlonly main select BOOK.ISBN, BOOK.TITLE, AUTHOR.NAME as authorName, BOOK.PUBLISH_DATE 
from BOOK BOOK inner join AUTHOR AUTHOR on AUTHOR.ID = BOOK.AUTHOR_ID 
where BOOK.AUTHOR_ID = AUTHOR.ID order by BOOK.PUBLISH_DATE asc, AUTHOR.ID asc 

[DEBUG] com.example.ReadTest main ---------
[DEBUG] com.example.ReadTest main isbn: 001-0000000001
[DEBUG] com.example.ReadTest main title: A Study in Scarlet
[DEBUG] com.example.ReadTest main author: Arthur Conan Doyle
[DEBUG] com.example.ReadTest main publishDate: 1887-01-01
[DEBUG] com.example.ReadTest main ---------
[DEBUG] com.example.ReadTest main isbn: 001-0000000002
[DEBUG] com.example.ReadTest main title: The Sign of Four
[DEBUG] com.example.ReadTest main author: Arthur Conan Doyle
[DEBUG] com.example.ReadTest main publishDate: 1890-01-01
[DEBUG] com.example.ReadTest main ---------
[DEBUG] com.example.ReadTest main isbn: 001-8888880001
[DEBUG] com.example.ReadTest main title: 少年探偵団
[DEBUG] com.example.ReadTest main author: 江戸川乱歩
[DEBUG] com.example.ReadTest main publishDate: 1937-01-01
[DEBUG] com.example.ReadTest main ---------
[DEBUG] com.example.ReadTest main isbn: 001-9999990001
[DEBUG] com.example.ReadTest main title: Norwegian Wood
[DEBUG] com.example.ReadTest main author: Haruki Murakami
[DEBUG] com.example.ReadTest main publishDate: 1987-01-01
[DEBUG] com.example.ReadTest main ---------
[DEBUG] com.example.ReadTest main isbn: 001-9999990002
[DEBUG] com.example.ReadTest main title: 1Q84, Volumes 1-2
[DEBUG] com.example.ReadTest main author: Haruki Murakami
[DEBUG] com.example.ReadTest main publishDate: 2009-01-01

後編に続く!

花粉症がつらすぎて寝不足なのでこのへんで。後編に続きます

ソースコードなど

念のためタグを切ってありますのでそれをチェックアウトしてからおためしください。


git clone git@github.com:nabedge/querydsl4-sample.git
git checkout refs/tags/20160313