Tags: java querydsl spring springboot

先週の前編からの続きです

副問い合わせ

SQLExpressionsクラスを使えば直感的に書けます。


    @Test
    public void subQuerySample() {
        List<Author> list = sqlQueryFactory.select(qAuthor)
                .from(qAuthor)
                .where(SQLExpressions.select(qBook.count()).from(qBook).where(qBook.authorId.eq(qAuthor.id)).gt(0L))
                .fetch();
        log.info(list.toString());
    }

$ mvn test -Dtest=ReadTest#subQuerySample

Running com.example.ReadTest
[INFO] jdbc.sqlonly main select AUTHOR.ID, AUTHOR.NAME from AUTHOR AUTHOR where (select count(BOOK.ISBN) from BOOK BOOK 
where BOOK.AUTHOR_ID = AUTHOR.ID) > 0 

[INFO] com.example.ReadTest main [id = 1, name = Arthur Conan Doyle, id = 2, name = Haruki Murakami, id = 3, name = 江戸川乱歩]

between句

なんの変哲もない感じですが。


    @Test
    public void select_between() {
        SQLQuery<Book> query = sqlQueryFactory.select(qBook);
        query.from(qBook);
        java.sql.Date from = Date.valueOf("1980-01-01");
        java.sql.Date to = Date.valueOf("2000-01-01");
        query.where(qBook.publishDate.between(from, to));
        List books = query.fetch();
        books.forEach(book -> log.info(book.toString()));
    }

$ mvn test -Dtest=ReadTest#select_between

[INFO] jdbc.sqlonly main select BOOK.AUTHOR_ID, BOOK.ISBN, BOOK.PUBLISH_DATE, BOOK.TITLE from BOOK BOOK where BOOK.PUBLISH_DATE 
between '01/01/1980 00:00:00.000' and '01/01/2000 00:00:00.000' 

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

もしもbetweenが特殊な関数だとしたら?

RDBMSの種類によって、SQLの書き方にはそれぞれ方言や独自の関数があります。ほとんどの場合はQueryDSLが提供するSQLTemplateが吸収してくれますが、それではカバーしきれない独特のSQLを書かねばならないことが、実際の開発現場でもよくあります。そんなときでもQueryDSLは柔軟に対応できます。下の例では、between句が特殊な関数だと仮定して、それを通常のプレースホルダつきの文字列としてQueryDSLでのSQL組み立てに加えています。


    @Test
    public void select_between2() {
        SQLQuery<Book> query = sqlQueryFactory.select(qBook);
        query.from(qBook);
        // The case that you need special dialect.
        // (supposing that "BETWEEN" is a dialect.)
        String template = "publish_date between {0} and {1}";
        query.where(Expressions.booleanTemplate(template, "1980-01-01", "2000-12-31"));
        List books = query.fetch();
        books.forEach(book -> log.info(book.toString()));
    }

一つ前で紹介した通常のbetween句用のメソッドと違い、文字列そのままでSQLを組み立てるので、
between '1980-01-01' and '2000-12-31'
のように時刻情報は無い状態でのbetween句となります。


$ mvn test -Dtest=ReadTest#select_between2

[INFO] jdbc.sqlonly main select BOOK.AUTHOR_ID, BOOK.ISBN, BOOK.PUBLISH_DATE, BOOK.TITLE from BOOK BOOK where publish_date 
between '1980-01-01' and '2000-12-31' 

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

INSERTは?

ここまですべてSELECTでした。INSERTはどうかというと、見ての通りです。INSERTしたい情報をそのテーブル用のBeanにつめこんで、populate()メソッドに渡してexecute()することでINSERT文を発行できます。


    @Test
    @Transactional()
    public void insert1() {
        Book book = new Book();
        book.setIsbn("999-9999999999");
        book.setAuthorId(4);
        book.setTitle("進撃の巨人 (1)");
        java.sql.Date publishDate = java.sql.Date.valueOf(LocalDate.parse("2010-03-17"));
        book.setPublishDate(publishDate);
        sqlQueryFactory.insert(qBook).populate(book).execute();
    }

$ mvn test -Dtest=CreateTest#insert1

Running com.example.CreateTest
[INFO] jdbc.sqlonly main insert into BOOK (AUTHOR_ID, ISBN, PUBLISH_DATE, TITLE) 
 values (4, '999-9999999999', '03/17/2010 00:00:00.000', '進撃の巨人 (1)') 

下のように書いても同じです。ただしsetメソッドを呼び出した順でカラムが指定されます。(あまり意味は無いですが)


    public void insert2() {
        java.sql.Date publishDate = java.sql.Date.valueOf(LocalDate.parse("2010-03-17"));
        SQLInsertClause insert = sqlQueryFactory.insert(qBook)
                .set(qBook.isbn, "999-9999999999")
                .set(qBook.title, "Attack of Titan vol 1")
                .set(qBook.authorId, 4)
                .set(qBook.publishDate, publishDate);
        insert.execute();
    }

$ mvn test -Dtest=CreateTest#insert2

Running com.example.CreateTest
[INFO] jdbc.sqlonly main insert into BOOK (ISBN, TITLE, AUTHOR_ID, PUBLISH_DATE) 
 values ('999-9999999999', 'Attack of Titan vol 1', 4, '03/17/2010 00:00:00.000') 

UPDATE, DELETEも似たような感じで書けてしまうのでここでは省略です。

QueryDSL以外のO/Rマッパーも併用したい場合

すでに開発中/運用中のアプリケーションでは他のO/Rマッパーが使われていることでしょう。それがSpringフレームワークに沿っている場合なら、Springの使い方一つで簡単に併用できます。下の例では、

  1. 昔ながらのJdbcTemplateで1件INSERTし、
  2. それをQueryDSLでUPDATEしたうえで、
  3. さらにJdbcTemplateでそのレコードをSELECTしています。

    @Test
    @Transactional
    public void multiple_ORMapper() {

        // insert by jdbcTemplate
        String insertSql = "insert into book (isbn,title,author_id,publish_date) " +
                "values ('001-0000000003', 'TThe Hound of the Baskervilles', 1, '1901-01-01')";
        jdbcTemplate.execute(insertSql);

        // update the above record by QueryDSL
        sqlQueryFactory
                .update(qBook)
                .set(qBook.title, "The Hound of the Baskervilles")
                .where(qBook.isbn.eq("001-0000000003"))
                .execute();

        // select by jdbcTemplate
        String selectSql = "select isbn from book where isbn='001-0000000003'";
        Map result = jdbcTemplate.queryForMap(selectSql);
        log.info(result.toString());

    }

$ mvn test -Dtest=CreateTest#multiple_ORMapper

[INFO] jdbc.sqlonly main insert into book (isbn,title,author_id,publish_date) values ('001-0000000003', 'TThe Hound 
of the Baskervilles', 1, '1901-01-01') 

[INFO] jdbc.sqlonly main update BOOK set TITLE = 'The Hound of the Baskervilles' where BOOK.ISBN = '001-0000000003' 

[INFO] jdbc.sqlonly main select isbn from book where isbn='001-0000000003' 

[INFO] com.example.CreateTest main {ISBN=001-0000000003}

想定通りに動いています。もちろん途中で例外が発生した場合はINSERTもUPDATEも、@Transactionalがついたこのメソッド内のすべてのSQLの発行がロールバックされます。どんなO/Rマッパーを使うのであれ、それに与えるDataSourceオブジェクトをTransactionAwareDataSourceProxyで包み込んでおくことでトランザクション管理をSpringの支配下に置いておくのがポイントです。


@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 provider = new SpringConnectionProvider(dataSource());
        return new SQLQueryFactory(querydslConfiguration(), provider);
    }
}

上のSpringのJavaConfigをよく見ると、QueryDSL用のSQLQueryFactoryもJdbcTemplateも、与えられるdataSource()は一つのメソッドから発生するものに集約されていて、それはTransactionAwareDataSourceProxyに包まれています。

今回紹介したのはquerydsl-sqlですが、querydsl-jpa, querydsl-lucene, querydsl-mongodbなどいろいろあるようです。リファレンスが充実しているので詳しくはそちらで。

ソースコードなど

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


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