Edited at

QueryDSL を Spring Data JPA に導入した結果、save メソッドにて insert が発行されない現象が発生した話

More than 1 year has passed since last update.


概要

QueryDSL を Spring Boot JPA に導入した際、公式ページ 2.3.23. Spring integrationにあるように Bean を定義した結果、要件どおりのクエリーは発行される一方、JpaRepository#save メソッド実行時に insert が発行されなくなりました。このため、save 実行後に、インスタンスは取得できますが、データベースへの書き込みを行うことができなくなりました。


JdbcConfiguration.java

package com.querydsl.example.config;

import com.querydsl.sql.H2Templates;
import com.querydsl.sql.SQLQueryFactory;
import com.querydsl.sql.SQLTemplates;
import com.querydsl.sql.spring.SpringConnectionProvider;
import com.querydsl.sql.spring.SpringExceptionTranslator;
import com.querydsl.sql.types.DateTimeType;
import com.querydsl.sql.types.LocalDateType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.sql.DataSource;
import java.sql.Connection;

@Configuration
public class JdbcConfiguration {

@Bean
public DataSource dataSource() {
// implementation omitted
}

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

}



回避策

transactionManager に DataSourceTransactionManager を設定していることが問題のようです。

こちらのページ にあるように、transactionManager には JpaTransactionManager を設定する必要がありました。

そのため、transactionManager には JpaTransactionManager を定義するか、あるいは、transactionManager の Bean 定義を削除することで現象を回避できました。

    @Bean

public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(entityManagerFactory);
return jpaTransactionManager;
}


QueryDSL について

こちらのページにあるように、JPAQuery クラスに EntityManager をラップすることで、型安全のクエリを作成することができます。

タスクランナーにてビルドする際、JPAAnnotationProcessor をコンパイル用のプロセッサーとして指定すると、@Entity が設定されたクラスを見つけてくれて、自動的に Entity のメタ情報を保持した java ファイルを生成してくれます。

このファイルがあるので、 JPQL を型安全で表現できるようになります。


CompanyRepository.java

@Repository

@RequiredArgsConstructor
public class CompanyRepositoryImpl implements CompanyRepository {
private QCompany company = QCompany.company;
private QCompany user = QUser.user;
private QUser u = new QUser("u"); //エイリアスとして使用します。
private final EntityManager em;

@Override
public List<Company> findById(List<Integer> users) {
return qFactory
.selectDistinct(company).from(company)
.innerJoin(company.users, u).fetchJoin()
.where(u.id.in(users))
.fetch();

}
// Eager で一挙にロードしたいので、fetchJoin を指定します。



Company.java

@Entity

@Getter
public class Company {
@id
private int id;
@OneToMany(mappedBy = "company")
Set<User> users;
}


User.java

@Entity

@Getter
public class User {
@Id
private int id;
@ManyToOne
@JoinColumn(name = "company_id", insertable = false, updatable = false, nullable = false0
private Company company;
}
// 結合する列はプロパティとして定義する必要はありません。また、insertable = false, updatable = false を指定することで、度々発生する
//caused by: org.hibernate.MappingException: Repeated column in mapping for entity:を回避できます。


Lombok 使用時の対処策

ただし、Lombok を使用している場合、Lombok のアノテーションをコンパイラが解釈できなくなるため、タスクランナー実行時にコンパイルエラーが発生してしまいます。

これは、Gradle にて Lombok のアノテーションプロセッサーをコンパイル時に渡すことで回避できます。

build タスクを実行すると、$projectRoot/src/main/generated 配下にソースコードが生成されます。


build.gradle

dependencies {

compile('com.querydsl:querydsl-apt:4.1.4')
compile('com.querydsl:querydsl-sql:4.1.4')
compile('com.querydsl:querydsl-sql-spring:4.1.4')
compile('com.querydsl:querydsl-jpa:4.1.4')
}

def queryDslOutput = file("src/main/generated")
sourceSets {
main {
java {
srcDir queryDslOutput
}
}
}

task generateQueryDSL(type: JavaCompile, group: 'build') {
source = sourceSets.main.java
classpath = configurations.compile
destinationDir = queryDslOutput
options.compilerArgs = [
"-proc:only",
"-processor", 'com.querydsl.apt.jpa.JPAAnnotationProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor'
]
}
compileJava.dependsOn(generateQueryDSL)

clean {
delete queryDslOutput
}



補足 : ON 句の使用について

色々調べましたが、Spring Data JPA の JPQL には ON 句や、あるいはサブクエリでの JOIN がサポートされていないようです。

公式ページ にもそんなようなことが書かれていますので、条件をフィルタリングしたエンティティとの結合はできないようです。# Eclipse Link では使用できる、という情報もございましたが、未確認です。


Subqueries may be used in the WHERE or HAVING clause. The syntax for subqueries is as follows:

Subqueries are restricted to the WHERE and HAVING clauses in this release. Support for subqueries in the FROM clause will be considered in a later release of the specification.


on は QueryDSL に存在するため、下記のように書くことが可能ですが、残念ながら、上記背景からか問題点があるため、on 句を使用することが実質できない状況のようでした。


CompanyRepository.java

@Repository

@RequiredArgsConstructor
public class CompanyRepositoryImpl implements CompanyRepository {
private QCompany company = QCompany.company;
private QCompany user = QUser.user;
private QUser u = new QUser("u"); //エイリアスとして使用します。
private final EntityManager em;

@Override
public List<Company> findById(List<Integer> users) {
return qFactory
.selectDistinct(company).from(company)
.innerJoin(company.users, u).on(u.age.gt(Expressions.asNumber(20))
.where(u.id.in(users))
.fetch();

}
}



問題点

メソッド実行時に発行される SQL のSELECT の射影項目に on を使用したエンティティのプロパティが生成されません。上記の式の場合、Companyクラスのプロパティのみ取得されます。

これは、データの取得モードが Lazy となるため、データの取得が遅延しており、実際にプロパティにアクセスした際に、該当データの取得 SQL が流れるためです。

ここで、on, または, join の後に fetchJoin メソッドを指定すると、with-clause は使用できません、というような JPQL のシンタックスエラーになります。

また、遅延ロードをした際に、getUsers メソッドにを実行すると、

select * from user where id = ?

のようなクエリが流れますが、age の条件は無視されます。

これは、JPA が知っている @OneToMany の項目でデータを検索しているためであると考えらます。

Spring Data JPA にて使用した場合、SQL を発行するためではなく、QueryDSL はあくまで JPQL を発行するためのフレームワークになるようです。