개요
QueryDSL을 통해서 JPQL을 동적으로 구성할 수 있는 법을 공부하고 Spring Boot에 적용하는 방법을 찾아보고 적용시킨 방법을 기록한다.
QueryDSL은 JPA에서 공식적으로 제공하는 JPQL 빌더가 아니기 때문에 따로 구성해주어야 한다.
적용시킨 Spring Boot 버전과 의존성들은 다음과 같다.
- version : 3.2.4
- java version : 17
Spring Data JPA
lombok
MariaDB Driver
Spring MVC
환경이 다르면 문제가 발생할 수 있다는 점, 안된다면 댓글을 남겨주세요
본론
Gradle에 QueryDSL 의존성 추가
// query dsl 버전 명시해야 jakarta사용 (javax X)
implementation 'com.querydsl:querydsl-apt:5.0.0'
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
implementation 'com.querydsl:querydsl-core:5.0.0'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
- 처음에 적용할 때는 버전을 명시해주지 않는다면 자바 버전과 호환이 되지 않는 QueryDSL이 설치된다.
- jakarta가 아닌 javax 패키지를 찾아서 오류가 발생한다.
아래의 두 줄은 예외에 대한 처리를 위해 작성해주었다.
다음을 참고해주길 바란다.
https://github.com/querydsl/querydsl/issues/3466
Gradle에 Q-Class 관련 설정 추가
// Querydsl 빌드 옵션 설정
def generated = 'src/main/generated'
// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(generated))
}
// java source set 에 querydsl QClass 위치 추가
sourceSets {
main.java.srcDirs += [ generated ]
}
// gradle clean 시에 QClass 디렉토리 삭제
clean {
delete file(generated)
}
- 프로젝트를 빌드하면 generated에 명시된 경로에 Q-Class가 생성된다.
JPAQueryFactory 빈 등록
@Configuration
public class QueryDSLConfiguration {
// EntityManager를 빈으로 주입받기 위해 사용하는 어노테이션
// @Autowired 안됨.
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
- QueryDSL을 사용하기 위한 JPAQueryFactory를 빈으로 등록해준다.
- EntityManager를 빈으로 주입받기 위해서는 @PersistenceContext 어노테이션을 사용하면된다.
여기까지 진행하면 JPAQueryFactory를 사용해서 QueryDSL을 사용할 수 있다.
Sample Entity 정의
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@ToString
public class SampleEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
private int age;
}
QueryDSL을 사용해볼 Sample Entity를 정의한다.
QSampleEntity Generate
QueryDSL을 사용하기 위해서 Q-Class를 생성해주어야 한다.
Gradle Build를 통해서 위에서 설정한 경로에 QClass를 생성해줄 수 있다.
- ⚠️ build할때는 데이터베이스가 켜져있어야 한다.
/**
* QSampleEntity is a Querydsl query type for SampleEntity
*/
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QSampleEntity extends EntityPathBase<SampleEntity> {
private static final long serialVersionUID = -1295075762L;
public static final QSampleEntity sampleEntity = new QSampleEntity("sampleEntity");
public final NumberPath<Integer> age = createNumber("age", Integer.class);
public final NumberPath<Long> id = createNumber("id", Long.class);
public final StringPath name = createString("name");
public QSampleEntity(String variable) {
super(SampleEntity.class, forVariable(variable));
}
public QSampleEntity(Path<? extends SampleEntity> path) {
super(path.getType(), path.getMetadata());
}
public QSampleEntity(PathMetadata metadata) {
super(SampleEntity.class, metadata);
}
}
성공적으로 생성된 것을 볼 수 있다.
Sample Repository 정의
@Repository
public interface SampleRepository
extends JpaRepository<SampleEntity, Long>, CustomSampleRepository {}
public interface CustomSampleRepository {
public SampleEntity SampleQuery();
}
// 필요하다.!
import static dev.changuii.project.entity.QSampleEntity.sampleEntity;
public class CustomSampleRepositoryImpl implements CustomSampleRepository {
private JPAQueryFactory jpaQueryFactory;
public CustomSampleRepositoryImpl(
@Autowired JPAQueryFactory jpaQueryFactory
){
this.jpaQueryFactory = jpaQueryFactory;
}
@Override
public List<SampleEntity> SampleQuery() {
return jpaQueryFactory
.select(sampleEntity)
.from(sampleEntity)
.fetch();
}
}
Spring Data JPA에서 기본적으로 제공하는 네임드 쿼리 외의 자신만의 커스텀 쿼리들을 정의하고 싶으면 다음과 같이 하면 된다.
- 새로운 인터페이스를 만들고 해당 인터페이스의 구현체를 {repository 명} + Impl로 만든다.
- Repository에서 해당 커스텀 인터페이스를 상속받는다.
새로운 커스텀 인터페이스의 구현체의 이름을 {repository명} + Impl로 구성하면 스프링이 스캔을 통해 찾아서 빈으로 등록해준다.
- ⚠️ import static을 통해서 가져오면 간편하게 사용할 수 있다.
Sample DAO 정의
@Repository
public class SampleDAOImpl implements SampleDAO {
private SampleRepository sampleRepository;
public SampleDAOImpl(
@Autowired SampleRepository sampleRepository
){
this.sampleRepository = sampleRepository;
}
@Override
public List<SampleEntity> readAll() {
return this.sampleRepository.SampleQuery();
}
@Override
public void insert(SampleEntity e) {
this.sampleRepository.save(e);
}
}
테스트하기위해 insert와 readAll()을 구현해주었다.
Sample Query
위에서 작성한 QueryDSL은 다음과 같다.
// QueryDSL
jpaQueryFactory
.select(sampleEntity)
.from(sampleEntity)
.fetch();
JPQL과 예상되는 SQL은 다음과 같다.
// JPQL
SELECT s
FROM SampleEntity s
// SQL
SELECT S.id, S.name, S.age
FROM SAMPLE_ENTITY AS S
Sample Query Test
테스트 코드를 작성해서 결과를 보자.
@Test
public void testQuery(){
List<SampleEntity> sampleEntityList = new ArrayList<>();
for(int i=1; i<10; i++){
SampleEntity sampleEntity = SampleEntity.builder()
.id(i)
.age(i*10)
.name("LEE" + i).build();
sampleEntityList.add(sampleEntity);
this.sampleDAO.insert(sampleEntity);
}
StringBuilder sb = new StringBuilder();
for(SampleEntity e : sampleEntityList){
sb.append(e.toString()).append("\n");
}
// before 출력
System.out.println(sb);
sb = new StringBuilder();
List<SampleEntity> after = this.sampleDAO.readAll();
for(SampleEntity e : after){
sb.append(e.toString()).append("\n");
}
// after 출력
System.out.println(sb);
// 두 리스트가 같은지 검사
assertThat(sampleEntityList).usingRecursiveComparison().isEqualTo(after);
}
9개의 엔티티를 데이터베이스에 넣고 위의 쿼리문을 날려서 모든 결과가 나오는지 테스트해보았다.
출력 결과는 위와 같고 아래와 같이 테스트에도 성공하였다.
결론
다음 포스트에서는 다양한 QueryDSL을 만들어보고 테스트해보겠다.
'Spring' 카테고리의 다른 글
[Spring Boot] 등록 요청의 중복 방지하기, 멱등성 보장 (0) | 2024.08.01 |
---|---|
[Spring Boot] 스프링 동시성 제어하기 (Java Synchronized keyword) (1) | 2024.04.25 |
[Spring Boot] 도커 컨테이너에 환경에서 application.yml 민감한 정보 환경변수로 묶어내기 (0) | 2024.04.04 |
[Spring] 스프링 프로젝트에 카카오 로그인, 회원가입 구현 - (2) (0) | 2024.03.31 |
[Spring] 스프링 프로젝트에 카카오 로그인, 회원가입 구현 - (1) (0) | 2024.03.31 |