본문 바로가기

QueryDSL

[Query DSL] 기본문법

 

 

도메인 설정

 

Member 

package com.example.querytest.entity;

        import jakarta.persistence.*;
        import lombok.*;

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String username;

    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "taem_id")
    private Team team;

    public Member(String username) {
        this(username, 0);
    }

    public Member(String username, int age) {
        this(username, age, null);
    }

    public Member(String username, int age, Team team){
        this.username = username;
        this.age = age;
        if(team != null){
            changeTeam(team);
        }
    }
    private void changeTeam(Team team){
        this.team = team;
        team.getMembers().add(this);
    }
}

 

  • Lombok의 @Builder와 @AllArgsConstructor를 사용하면 this 키워드를 사용하는 생성자를 여러 개 작성할 필요가 없습니다.
  • 빌더 패턴을 통해 객체를 유연하게 생성할 수 있으며, 필요한 필드만 선택적으로 초기화할 수 있습니다.
  • 코드는 더 간결해지고, 가독성이 높아집니다.

 

 

Team

package com.example.querytest.entity;

import jakarta.persistence.*;
import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();


    public Team(String name){
        this.name = name;
    }


}

 

 

  • Setter 사용하지 않기: 객체의 일관성을 위해.
  • 기본 생성자 제한: JPA 요구사항이지만 외부에서 호출되지 않도록 protected로 설정.
  • @ToString 설정: 내부 필드만 출력하여 순환 참조 문제 방지.
  • 연관관계 편의 메소드: 양방향 연관관계를 한 번에 처리.
  • 양방향 연관관계 설정: Member.team이 연관관계의 주인, Team.members는 읽기 전용.

데이터 확인

package com.example.querytest.entity;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
@Commit
class MemberTest {

    @PersistenceContext
    EntityManager em;

    @Test
    public void memberTeat(){
        Team teamA  = new Team("teamA");
        Team teamB  = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);


        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        em.flush();
        em.clear();

        List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();

        for(Member member : members){
            System.out.println("member = " + member);
            System.out.println("member.team = " + member.getTeam());
        }
    }
}

 

 

@Transactional과 @Commit을 함께 사용함으로써 다음과 같은 이점을 얻을 수 있습니다:

  1. 트랜잭션 경계 설정: @Transactional을 통해 트랜잭션 경계를 설정하여, 테스트 메서드가 실행되는 동안 일관된 트랜잭션이 유지됩니다.
  2. 커밋 강제: 테스트 메서드가 종료될 때, 기본 롤백 동작을 무시하고 @Commit을 사용하여 트랜잭션을 커밋합니다. 이를 통해 데이터베이스에 실제 변경사항을 적용할 수 있습니다.

Querydsl vs JPQL**

package com.example.querytest.controller;

import com.example.querytest.entity.*;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Transactional
class QuerydslBasicTest {
    @PersistenceContext
    EntityManager em;

    @BeforeEach
    public void memberTeat(){
        Team teamA  = new Team("teamA");
        Team teamB  = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);


        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        em.flush();
        em.clear();

    }


    @Test
    public void startJPQL(){

        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(){

        JPAQueryFactory queryFactory = new JPAQueryFactory(em);

        QMember m = QMember.member;

        Member findmember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq("member1"))
                .fetchOne();

        assertThat(findmember.getUsername()).isEqualTo("member1");
    }
}

- fetchOne은 QueryDSL에서 단일 결과를 반환하는 메서드입니다.

JPAQueryFactory와 동시성 문제

JPAQueryFactory를 사용하는 QueryDSL은 JPQL 빌더로서, JPQL과의 주요 차이점은 코드 기반의 쿼리 빌더를 제공하여 컴파일 시점에 오류를 검출할 수 있다는 점입니다. JPQL은 문자열 기반으로 실행 시점에 오류가 발생할 수 있습니다. 또한, JPQL은 파라미터 바인딩을 직접 처리해야 하지만, QueryDSL은 파라미터 바인딩을 자동으로 처리합니다.

동시성 문제

JPAQueryFactory를 필드로 제공할 때 동시성 문제에 대해 걱정할 필요가 없는 이유는 Spring 프레임워크가 여러 쓰레드에서 동시에 같은 EntityManager에 접근하더라도 트랜잭션마다 별도의 영속성 컨텍스트를 제공하기 때문입니다. 즉, EntityManager는 스레드 세이프(Thread-Safe)하지 않지만, Spring은 트랜잭션 범위 내에서 각 요청마다 별도의 EntityManager 인스턴스를 사용하게 합니다.

 

QueryDSL은 구문 오류를 컴파일 시점에 잡아줄 수 있습니다. 이는 QueryDSL이 Java 코드로 작성된 타입 안전한 쿼리 빌더를 제공하기 때문입니다. JPQL 또는 SQL과 달리 QueryDSL 쿼리는 Java 컴파일러가 검증하므로, 잘못된 필드 이름이나 잘못된 타입 등의 오류를 컴파일 시점에 발견할 수 있습니다.

 

 

기본 Q-Type 활용

Q클래스 인스턴스를 사용하는 2가지 방법**

QMember qMember = new QMember("m"); //별칭 직접 지정 
QMember qMember = QMember.member; //기본 인스턴스 사용

 

static import와 함께 사용**

import static com.example.querytest.entity.QMember.*;

    @Test
    public void startQueryDsl(){

        JPAQueryFactory queryFactory = new JPAQueryFactory(em);

        Member findmember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq("member1"))
                .fetchOne();

        assertThat(findmember.getUsername()).isEqualTo("member1");
    }

 

다음 설정을 추가하면 실행되는 JPQL을 볼 수 있다.

spring.jpa.properties.hibernate.use_sql_comments: true

같은 테이블을 조인해야 하는 경우가 아니면 기본 인스턴스를 사용하자

 

기본 인스턴스 사용

    @Test
    public void startQueryDsl(){

        JPAQueryFactory queryFactory = new JPAQueryFactory(em);

        Member findmember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq("member1"))
                .fetchOne();

        assertThat(findmember.getUsername()).isEqualTo("member1");
    }

같은테이블 별칭을 사용

    @Test
    public void startQueryDsl(){

        QMember m = new QMember("m1");

        JPAQueryFactory queryFactory = new JPAQueryFactory(em);

        Member findmember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq("member1"))
                .fetchOne();

        assertThat(findmember.getUsername()).isEqualTo("member1");
    }