먼저 Querydsl과 JPQL을 비교해보겠다.
Querydsl vs JPQL
JPAQueryFactory qf;
@Test
public void startJPQL(){
// JPQL을 사용한 member1 찾기
String qlString = "select m from Member m " +
"where m.username = :username";
Member findMember = em.createQuery(qlString, Member.class)
.setParameter("username", "member1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
@Test
public void startQuerydsl(){
//Querydsl 사용한 member1 찾기
JPAQueryFactory qf = new JPAQueryFactory(em);
Memeber findMember =qf
.select(Qmember.member)
.from(Qmember.member)
.where(Qmember.member.name.eq("member1") //파라미터 바인딩 처리
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member"1);
}
EntityManager 로 JPAQueryFactory 생성
Querydsl 은 JPQL 빌더
JPQL: 문자(실행 시점 오류), Querydsl: 코드(컴파일 시점 오류)
JPQL: 파라미터 바인딩 직접, Querydsl: 파라미터 바인딩 자동 처리
JPAQueryFactory 를 필드로 설정 할 수도 있다.
JPQQueryFactory를 필드로 제공하면 동시성 문제는 어떻게 될까?
동시성 문제는 JPAQueryFactory를 생성할 때 제공하는 EntiryManager(em)에 달려있다. 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도
트랙재션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다.
기본적으로 QueryDsl을 사용할때 QClass가 생성이 된다. 초기에 QueryDSL을 사용하면서 궁금했던 내용은 그냥 Entity를 사용해도 될거같은데 굳이 QClass를 만들어서 사용을 할까? 어떻게 만드는거지? 라는 기본적인 궁금증에서 래퍼런스 문서 부터 많은 블로그의 내용을 찾아봤으며 해당 내용을 정리 해보려고 한다.
JPA_APT(JPAAnnotationProcessorTool)가 @Enttiy 와 같은 특정 어노테이션을 찾고 해당 클래스를 분석해서 QClass를 만들어 준다. 빌드 도구를 통해서 만드는 방법은 다른곳을 찾아봐도 나오니 생략한다.
(Gradle의 경우, 버전별로 설정을 하는 방식이 다르기 때문에 버전에 맞게 잘 찾아서 사용 해야 한다.)
Annotation 이 있는 기존코드를 바탕으로 새로운 코드와 새로운 파일들을 만들 수 있고, 이들을 이용한 클래스에서 compile 하는 기능도 지원해준다.
쉬운 예시로는 Lombok의 @Getter, @Setter가 있다. 해당 어노테이션을 사용하는 경우 apt가 컴파일 시점에 해당 어노테이션을 기준으로 getter 와 setter를 만들어 주기 때문에 코드를 작성하지 않고 사용이 가능해진다.
엔티티 클래스의 메타 정보를 담고 있는 클래스로, Querydsl은 이를 이용하여 타입 안정성(Type safe)을 보장하면서 쿼리를 작성할 수 있게 된다.
QClass는 엔티티 클래스와 대응되며 엔티티의 속성을 나타내고 있다. 이러한 QClass를 사용하여 쿼리를 작성하면 엔티티 속성을 직접 참조하고 조합하여 쿼리를 구성할 수 있다. QClass를 사용하면 컴파일 시점에 오류를 확인할 수 있고, IDE의 자동완성 기능을 활용하여 쿼리 작성을 보다 편리하게 할 수 있다.
그렇다면 굳이 엔티티 클래스 대신 Q클래스를 만들어서 사용하는 이유에 대해서 정리 하려고 한다.
QClass와 엔티티 클래스는 많은 장점을 공유하고 있지만 그럼에 QClass를 사용하는 이유는 다음과 같다.
QClass는 엔티티 속성을 정적인 방식으로 표현하므로 IDE의 자동 완성 기능을 활용할 수 있고, 속성 이름을 직접 기억하거나 확인하지 않아도 된다는 장점을 가지고 있다.
QClass는 엔티티 속성의 타입을 정확하게 표현하므로, 타입에 맞지 않는 연산이나 비교를 시도하면 컴파일러가 오류를 감지할 수 있다.
QClass는 엔티티 클래스의 확장으로 생각할 수 있다. 엔티티 클래스는 데이터베이스 테이블의 매핑을 담당하고, QClass는 쿼리 작성을 위한 편의성과 안전성을 제공을 해주면서 유지보수의 편의성 및 실수 방지를 하지 않도록 해준다고 생각한다.
기본 Q-Type 활용
Q클래스 인스턴스를 사용하는 3가지 방법
QMember qMember = new QMember("m"); //별칭 직접 설정
QMember qMember = QMember.member; //기본 인스턴스 사용
import static kbds.querydsl.domain.QMember.member; //static 상수 설정
참고 : 같은 테이블을 조인해야하는 경우가 아니면 기본 인스턴스를 사용하자.
검색 조건 쿼리
@Test
public void search(){
Member findMember = qf
.selectFrom(member)
.where(member.username.eq("member1")
.and(member.age.eq(10))
.fetchOne();
}
사용할 데이터

Author : Book = 1 : N
Author(저자)는 여러 개의 Book(책)을 가진다.
Author : Organization = N : 1
Author(저자)는 한곳의 Organization(조직)에 속한다.
Book : Review = 1 : N
Book(책)은 여러 개의 Review(리뷰)를 가진다.
@Entity
@Table(name = "Organization")
@Getter
@Setter
public class Organization {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orgName;
@OneToMany(mappedBy = "organization", cascade = CascadeType.ALL)
private List<Author> authors = new ArrayList<>();
}
@Entity
@Table(name = "Author")
@Getter
@Setter
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
private List<Book> book = new ArrayList<>();
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "organization_id")
private Organization organization;
}
@Entity
@Table(name = "Book")
@Getter
@Setter
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
@OneToMany(mappedBy = "book", cascade = CascadeType.ALL)
private List<Review> reviews = new ArrayList<>();
}
@Entity
@Table(name = "Review")
@Getter
@Setter
public class Review {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String comment;
@ManyToOne
@JoinColumn(name = "book_id")
private Book book;
}
public List<Book> findBookList() {
List<Book> result = queryFactory
.selectFrom(book)
.fetch();
return result;
}
public Book findBookByTitle(String title) {
Book result = queryFactory.selectFrom(book)
.where(book.title.eq(title))
.fetchOne();
return result;
}
public List<String> findBookListTitle() {
List<String> result = queryFactory.select(book.title)
.from(book)
.fetch();
return result;
}
public Book findBookByTitle(String title) {
Book result = queryFactory.selectFrom(book)
.where(book.title.eq(title))
.fetchOne();
return result;
}
// 동일 여부
author.name.eq("John"); // 일치
author.name.ne("John"); // 일치X
author.name.isNotNull(); // NullX
// 포함
author.age.in(20, 30, 40); // 포함
author.age.notIn(25, 35, 45); // 미포함
// 문자열
author.name.like("J%"); // LIKE : J로 시작
author.name.startsWith("J"); // J로 시작
author.name.contains("Jo"); // J 포함
// 수 비교
author.age.between(25, 35); // 25 ~ 35
author.age.lt(30); // < 30
author.age.loe(30); // <= 30
author.age.gt(30); // > 30
author.age.goe(30); // >= 30
public List<Author> findAuthorByCondition() {
List<Author> result = queryFactory.selectFrom(author)
.where(
author.age.notBetween(20, 30)
.and(author.age.gt(10))
.and(author.age.lt(50))
)
.fetch();
return result;
}
select * from author
where age NOT BETWEEN 20 and 30
and age > 10 and age < 50;
public List<Author> findAuthorByCondition2() {
List<Author> result = queryFactory.selectFrom(author)
.where(
(
author.age.notBetween(20, 30)
.and(author.age.gt(10))
.and(author.age.lt(50))
).or(
author.name.like("%John%")
)
)
.fetch();
return result;
}
select * from author
where (age NOT BETWEEN 20 and 30
and age > 10 and age < 50) or
(name LIKE "%John%");
.where(
boardIdEq(boardId),
cardTitleEq(cardTitle),
cardExplanationEq(cardExplanation),
endAtEq(endAt),
cardManagerNicknameEq(cardMangerName)
)
...
private BooleanExpression boardIdEq(Long boardId) {
return boardId != null ? card.boardList.board.id.eq(boardId) : null;
}
private BooleanExpression cardTitleEq(String cardTitle) {
return cardTitle != null ? card.cardTitle.contains(cardTitle) : null;
}
private BooleanExpression cardExplanationEq(String cardExplanation) {
return cardExplanation != null ? card.cardExplanation.contains(cardExplanation) : null;
}
private BooleanExpression endAtEq(String endAt) {
if (endAt == null) {
return null;
}
LocalDate ConvertedEndAt = LocalDate.parse(endAt);
return card.endAt.eq(ConvertedEndAt);
}
private BooleanExpression cardManagerNicknameEq(String userNickname) {
return userNickname != null ? user.nickName.eq(userNickname) : null;
}
public List<Book> findBooksByAuthorName(String name) {
List<Book> result = queryFactory
.selectFrom(book)
.where(book.author.name.eq(name))
.fetch();
return result;
}
SELECT Book.*
FROM Book
INNER JOIN Author ON Book.author_id = Author.id
WHERE Author.name = 'John Doe';
public List<Book> findBookListOrderBy() {
List<Book> result = queryFactory
.selectFrom(book)
.orderBy(book.title.desc())
.fetch();
return result;
}
정렬조건
public List<Book> findBookListOrderBy() {
List<Book> result = queryFactory
.selectFrom(book)
.orderBy(book.title.desc().nullsLast())
.fetch();
return result;
}
public List<Book> findBookListPagenation(int offset, int limit) {
List<Book> result = queryFactory
.selectFrom(book)
.offset(offset)
.limit(limit)
.fetch();
return result;
}
public List<Book> findBookListPagenation(int offset, int limit) {
QueryResults<Book> res = queryFactory
.selectFrom(book)
.offset(offset)
.limit(limit)
.fetchResults();
System.out.println("Total : " + res.getTotal());
System.out.println("Limit : " + res.getLimit());
System.out.println("Offset : " + res.getOffset());
List<Book> result = res.getResults();
return result;
}
그룹 함수와 함께 주로 사용된다.
public List<Tuple> findAuthorAggregation() {
List<Tuple> result = queryFactory
.select(author.count(), author.age.avg())
.from(author)
.fetch();
return result;
}
public List<Tuple> findAuthorGroupByGender() {
List<Tuple> result = queryFactory
.select(author.gender, author.count(), author.age.avg())
.from(author)
.groupBy(author.gender)
.fetch();
return result;
}
SELECT
author.gender,
COUNT(author.id),
AVG(author.age)
FROM
author
GROUP BY
author.gender;
public List<Tuple> findAuthorGroupBy() {
List<Tuple> result = queryFactory
.select(author.organization.orgName, author.count(), author.age.avg())
.from(author)
.groupBy(author.organization.id)
.having(author.age.avg().gt(10))
.fetch();
return result;
}
SELECT
author_organization.org_name,
COUNT(author.id),
AVG(author.age)
FROM
author
JOIN
organization AS author_organization ON author.organization_id = author_organization.id
GROUP BY
author.organization_id
HAVING
AVG(author.age) > 10;
출처: https://ssow93.tistory.com/60 [soTech:티스토리]
출처: https://sjh9708.tistory.com/175 [데굴데굴 개발자의 기록:티스토리]