QueryDSLとは
Querydsl was born out of the need to maintain HQL queries in a typesafe way. Incremental construction of HQL queries requires String concatenation and results in hard to read code.
QueryDSLはHQL(Hibernate Query Language)のクエリをタイプセーフで管理するライブラリです。
JPAでHQL(JPQL)を使用した場合の短所
- 文字列ベースのクエリ作成:タイピングミスやエンティティの変更に対する脆弱性があります。そのため、コンパイル時にエラーを検出しにくく、ランタイム時に問題が発生する可能性があります。
- 動的クエリの可読性:JPQLで動的クエリを作成するためには
Criteria API
を使用します。Criteria API
で動的クエリを作成するとコードの複雑性が増し、可読性が低下する可能性があります。
依存性追加
QueryDSLプロジェクトは現在2024年1月にリリーズされた5.1.0バージョンからアップデートされてない状況です。JDK21またはHibernate6.4以上の環境でQueryDSLを使用する場合はOpenFeignでForkしたリポジトリを利用してください。
Maven
<dependencies> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>5.1.0</version> <classifier>jakarta</classifier> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>5.1.0</version> <classifier>jakarta</classifier> </dependency> </dependencies> <plugins> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin> </plugins>
OpenFeignのQueryDSLを使用する場合
<dependency> <groupId>io.github.openfeign.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>6.11</version> </dependency> <dependency> <groupId>io.github.openfeign.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>6.11</version> </dependency> <plugins> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin> </plugins>
Gradle
dependencies { implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" }
OpenFeignのQueryDSLを使用する場合
dependencies { implementation 'io.github.openfeign.querydsl:querydsl-jpa:6.11' annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:6.11:jpa" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" }
QueryDSL 5.1.0でSQL/HQLインジェクション脆弱性があります。CVE-2024-49203 新しいプロジェクトを設定する際には、OpenFeignのQueryDSL 5.6.1または ≥ 6.10.1 バージョンを使用してください。
application.yml設定
spring: jpa: show-sql: true properties: hibernate: format_sql: true highlight_sql: true
Bean設定
@Configuration public class QuerydslConfig { @PersistenceContext private EntityManager entityManager; @Bean public JPAQueryFactory jpaQueryFactory() { return new JPAQueryFactory(entityManager); } }
Entity作成
@Entity @Table(name = "`user`") @Getter @Builder @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class User { @Id private String id; @Column(nullable = false) private String name; private String email; }
Repository作成
JPARepositoryインタフェース作成
@Repository public interface UserRepository extends JpaRepository<User, String>, UserCustomRepository { }
CustomRepositoryインタフェース作成
interface UserCustomRepository { }
CustomRepositoryImplクラス作成
@Repository @RequiredArgsConstructor class UserCustomRepositoryImpl implements UserCustomRepository { private final JPAQueryFactory queryFactory; }
動的クエリ作成
DTO作成
@Getter @Builder public class FindUserRequestDto { private String name; private String email; }
CustomRepositoryにメソッド追加
interface UserCustomRepository { List<User> findAllUsers(FindUserRequestDto requestDto); }
CustomRepositoryImplでメソッド実装
import static com.example.querydsl.user.entity.QUser.user; @Repository @RequiredArgsConstructor class UserCustomRepositoryImpl implements UserCustomRepository { private final JPAQueryFactory queryFactory; @Override public List<User> findAllUsers(FindUserRequestDto requestDto) { return queryFactory .selectFrom(user) .where( eqName(requestDto.getName()), eqEmail(requestDto.getEmail()) ) .fetch(); } private BooleanExpression eqName(String name) { // nullをリターンすることで、where句に条件を追加しない return name != null ? user.name.eq(name) : null; } private BooleanExpression eqEmail(String email) { // nullをリターンすることで、where句に条件を追加しない return email != null ? user.email.eq(email) : null; } }
テスト
@SpringBootTest class UserRepositoryTest { @Autowired private UserRepository userRepository; @BeforeEach void setUp() { userRepository.deleteAll(); userRepository.save(new User("id1", "name1", "test1@test.com")); userRepository.save(new User("id2", "name2", "test2@test.com")); userRepository.save(new User("id3", "name3", "test3@test.com")); } }
テスト1(emailのみ設定)
@Test void findAllUsers_WhenNameIsNull() { // given FindUserRequestDto requestDto = FindUserRequestDto.builder() .email("test3@test.com") .build(); // when List<User> users = userRepository.findAllUsers(requestDto); // then assertEquals(1, users.size()); }
テスト1実行時のクエリ
select u1_0.id, u1_0.email, u1_0.name from "user" u1_0 where u1_0.email=?
テスト2(nameのみ設定)
@Test void findAllUsers_WhenEmailIsNull() { // given FindUserRequestDto requestDto = FindUserRequestDto.builder() .name("name1") .build(); // when List<User> users = userRepository.findAllUsers(requestDto); // then assertEquals(1, users.size()); }
テスト2実行時のクエリ
select u1_0.id, u1_0.email, u1_0.name from "user" u1_0 where u1_0.name=?
テスト3(nameとemail設定)
@Test void findAllUsers_WhenNameAndEmailAreNotNull() { // given FindUserRequestDto requestDto = FindUserRequestDto.builder() .name("name1") .email("test2@test.com") .build(); // when List<User> users = userRepository.findAllUsers(requestDto); // then assertTrue(users.isEmpty()); }
テスト3実行時のクエリ
select u1_0.id, u1_0.email, u1_0.name from "user" u1_0 where u1_0.name=? and u1_0.email=?
テスト4(nameとemail設定なし)
@Test void findAllUsers_WhenNameAndEmailAreNull() { // given FindUserRequestDto requestDto = FindUserRequestDto.builder() .build(); // when List<User> users = userRepository.findAllUsers(requestDto); // then assertEquals(3, users.size()); }
テスト4実行時のクエリ
select u1_0.id, u1_0.email, u1_0.name from "user" u1_0
DTOフィルドのデータがNULLの場合、条件から除外されることが確認できます。
最後まで読んでいただきありがとうございました。
この投稿がQueryDSL導入の参考になれば嬉しいです。