도메인 설정
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을 함께 사용함으로써 다음과 같은 이점을 얻을 수 있습니다:
- 트랜잭션 경계 설정: @Transactional을 통해 트랜잭션 경계를 설정하여, 테스트 메서드가 실행되는 동안 일관된 트랜잭션이 유지됩니다.
- 커밋 강제: 테스트 메서드가 종료될 때, 기본 롤백 동작을 무시하고 @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");
}

'QueryDSL' 카테고리의 다른 글
[Query DSL] 스프링 데이터JPA 리포지토리와 Querydsl (0) | 2024.08.10 |
---|---|
[Query DSL] 순수 JPA 리포지토리와 Querydsl (0) | 2024.08.07 |
[Query DSL] 중급 문법 (1) | 2024.08.07 |
[Query DSL] 검색 조건 쿼리 (0) | 2024.07.04 |
[Query DSL] 프로젝트 세팅 ( import 인식 문제 확인 ) (0) | 2024.06.12 |