Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성하게 됩니다. 간단한 로직을 작성하는데 큰 문제는 없으나, 복잡한 로직의 경우 개행이 포함된 쿼리 문자열이 상당히 길어집니다. JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우, 정적 쿼리라면 어플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생합니다.
이러한 문제를 어느 정도 해소하는데 기여하는 프레임워크가 바로 QueryDSL입니다. QueryDSL은 정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 프레임워크입니다.
Predicate
인터페이스로 조건문을 여러개를 구성하여 따로 관리할 수 있다.findOne(Predicate)
, findAll(Predicate)
주로 이 2개 메소드가 사용된다.findOne
= Optional 리턴findAll
= List | Page | Iterable | Slice 리턴QueryDslPredicateExecutor
인터페이스가 추가된다.QueryDslPredicateExecutor
는 Repository가 QueryDsl 을 실행할 수 있는 인터페이스를 제공하는 역할을 합니다.plugins {
...
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
...
querydsl.extendsFrom compileClasspath
}
def querydslSrcDir = 'src/querydsl/generated'
querydsl {
library = "com.querydsl:querydsl-apt"
jpa = true
querydslSourcesDir = querydslSrcDir
}
sourceSets {
main {
java {
srcDirs = ['src/main/java', querydslSrcDir]
}
}
}
project.afterEvaluate {
project.tasks.compileQuerydsl.options.compilerArgs = [
"-proc:only",
"-processor", project.querydsl.processors() +
',lombok.launch.AnnotationProcessorHider$AnnotationProcessor'
]
}
dependencies {
implementation("com.querydsl:querydsl-jpa") // querydsl
implementation("com.querydsl:querydsl-apt") // querydsl
...
}
@Entity
어노테이션을 선언한 클래스를 탐색하고, JPAAnnotationProcessor
를 사용해 Q클래스를 생성합니다.querydsl-apt
가 @Entity 및 @Id 등의 매핑정보 Annotation을 알 수 있도록 javax.persistence
과 javax.annotation
을 annotationProcessor에 함께 추가합니다.annotationProcessor
는 Java 컴파일러 플러그인으로서, 컴파일 단계에서 어노테이션을 분석 및 처리함으로써 추가적인 파일을 생성합니다.QueryDSL로 쿼리를 작성할 때, Q 클래스를 사용함으로써 쿼리를 Type-Safe하게 작성할 수 있습니다.
Gradle 설정을 통해 $projectDir/src/main/java의 프로덕션 코드에서 Q 클래스를 import해 사용할 수 있습니다.
dependencies {
....
// 9. QueryDSL 적용을 위한 의존성 (SpringBoot3.0 부터는 jakarta 사용해야함)
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
Channel 조건으로 메세지 본문이 있는 Thread 목록 조회 쿼리수행
💁♂️ 이처럼 Join 없이 조건이 많이 추가될수록 **QuerydslPredicateExecutor**
를 활용할 수 있다.
@Service
public class ThreadServiceImpl implements ThreadService {
@Autowired
ThreadRepository threadRepository;
@Override
public List<Thread> selectNotEmptyThreadList(Channel channel) {
var thread = QThread.thread;
// 메세지가 비어있지 않은 해당 채널의 쓰레드 목록
var predicate = thread
.channel.eq(channel)
.and(thread.message.isNotEmpty());
var threads = threadRepository.findAll(predicate);
return IteratorAdapter.asList(threads.iterator());
}
@Override
public Thread insert(Thread thread) {
return threadRepository.save(thread);
}
}
@Test
void getNotEmptyThreadList() {
// given
var newChannel = Channel.builder().name("c1").type(Type.PUBLIC).build();
var savedChannel = channelRepository.save(newChannel);
var newThread = Thread.builder().message("message").build();
newThread.setChannel(savedChannel);
threadService.insert(newThread);
var newThread2 = Thread.builder().message("").build();
newThread2.setChannel(savedChannel);
threadService.insert(newThread2);
// when
var notEmptyThreads = threadService.selectNotEmptyThreadList(savedChannel);
// then 메세지가 비어있는 newThread2 는 조회되지 않는다.
assert !notEmptyThreads.contains(newThread2);
}
모든 채널에서 내가 멘션된 쓰레드 목록조회기능 만들기
1. Mention 엔티티 생성
2. User- Mention - Thread 다대다 연관관계 설정
3. User, Mention조건으로 Thread조회 쿼리수행
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
Set<CMention> mentions = new LinkedHashSet<>();
@OneToMany(mappedBy = "thread", cascade = CascadeType.ALL, orphanRemoval = true)
Set<Mention> mentions = new LinkedHashSet<>();
//연관관계 편의 메서드
public void addMention(User user){
Mention mention = Mention.builder().user(user).thread(this).build();
//양방향이므로 양쪽에다 생성한 멘션을 추가해줘야함 this.mentions.add(mention);
user.getMentions.add(mention);
}
ThreadRepository에 QuerydslPredicateExecutor 구현
public interface ThreadRepository extends JpaRepository<Thread, Long>, QuerydslPredicateExecutor<Thread> {
}
Thread Service에 QueryDsl 사용할 predicate(조건 설정)
@Service
public class ThreadServiceImpl implements ThreadService{
@Autowired
ThreadRepository threadRepository;
@Override
public List<Thread> getMentiondThreadList(User user){
var thread = Quthread.thread;
//모든 채널에서 내가 멘션된 스레드 목록 조회 기능 만들기
var predicate = thread.mention.any().user.eq(user)
var threads = threadRepository.findAll(predicate);
return IteratorAdapter.asList(threads.iterator());
Var thread = QThread.thread; 받아오고
Var predicate = thread.mention.any().user.eq(user)
var threads = threadRepository.findAll(predicate); //여기에 넣어주면 predicate가 조건문으로 바뀐다. 그래서 조건에 맞는 스레드를 찾아옴
Return threads; 는 리스트가 아니여서 리턴 할수 없다 .
// 여러가지 이터레이터블을 리스트를 바꾸는게 있다.
return IteratorAdapter.asList(threads.iterator());
이 경우 테스트 했을때 에러 발생한다.
var mentiondThreads = threadService.getMentiondThreadList(savedUser);
** 📌 그 이유는?? 멘션이 매핑이 되어 있는데 QueryDsl의 경우 QuerydslPredicateExecutor이 조인연산이 되지 않기 때문에 에러가 발생한다. **
그래서 jpaRepository 기능으로 구현 해야함
var MentiondThreads = savedUser.getMentions().stream.map(Mention::getThread).toList();
IteratorAdapter.asList(threads.iterator())
다른 Iterator(a.k.a 반복자)를 받아 새로운 반복자를 반환하는 함수를 iterator adapter라고 부른다.
Iterator adpaters 클래스에 속한 asList()는 명세에 보면 반복자 Iterator iter 로 매개변수를 받으면 iterator()의 값을 리스트로 반환시켜준다.
https://velog.io/@wonizizi99/Design-Pattern-java-Iterator
참고
자바 ORM 표준 JPA 프로그래밍
자바 ORM 표준 JPA 프로그래밍
equals와 hashCode는 왜 같이 재정의해야 할까?