SpringbootでQueryDSLを設定して動的クエリを作成しよう

SpringbootでQueryDSLを設定して動的クエリを作成しよう

카테고리
Springboot
태그
Spring Data JPA
QueryDSL

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導入の参考になれば嬉しいです。