[Querydsl] Querydsl 소개, 환경 설정, 예제 도메인 모델
📌 Topic
- Querydsl 소개
- 프로젝트 생성
- Querydsl 설정과 검증
- 라이브러리 살펴보기
- H2 데이터베이스 설치
- 스프링 부트 설정 - JPA, DB
01. Querydsl 소개
동적 쿼리의 한계를 극복하기 위해 나온 기술 중 하나 Querydsl F/W
최신 자바 백엔드 기술로는 Spring Boot + Spring Data JPA 조합으로 사용이 된다.
하지만 이러한 기술만으로는 복잡한 쿼리(동적 쿼리)를 해결할 수가 없다.
이러한 문제를 해결하기 위해 나온 기술이 Querydsl이다. Querydsl은 쿼리를 자바 코드로 작성할 수 있게 만들어 문법 오류를 컴파일 시점에 잡아낼 수 있다는 장점이 존재한다.
@Test
public void querydsl() {
String userName = "kim";
List<Member> members = queryFactory
.select(member)
.from(member)
.where(member.userName.eq(userName))
.fetch();
}
위와 같이 메서드 체이닝을 사용하여 각 요소에 맞는 값을 셋팅해주고
이상이 있는 경우 컴파일 시점에 오류를 발견할 수 있다.
02. 프로젝트 생성
기본적인 셋팅은 제외하고 Querydsl 관련된 내용만 정리
02-1. Tech Spec
Tech | Version |
---|---|
Spring Boot | 2.7.1 |
Java | 11 |
H2 | 2.1.2 |
02-2. 간단한 애플리케이션 테스트
package com.study.querydsl.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
/**
* Test Mapping
* @return
*/
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
간단한 컨트롤러를 생성하여 이상이 없는지 테스트.
02-3. Lombok 관련 설정
Build, Execution, Deployment > Compiler > Annotation Processors
- Lombok plugin 설치
- Enable annotation processing 선택
- 기존에 체크가 안되있는 경우가 있음
- Intellij 재시작
03. Querydsl 설정과 검증
이번 시간에는 Querydsl의 설정과 검증 과정을 진행 한다
03-1. Querydsl 플러그인 추가
// build.gradle
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" // querydsl 추가
id 'java'
}
- Querydsl plugin 추가
implementation 'com.querydsl:querydsl-jpa' // querydsl lib 추가
- Querydsl 라이브러리 추가
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
- build와 관련된 프로세싱 내용 추가
03-2. Querydsl 빌드
package com.study.querydsl.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Getter
@Setter
@Entity
public class Hello {
@Id
@GeneratedValue
private Long id;
}
- Querydsl 빌드 프로세싱 테스트를 위해 Hello 엔티티 생성
- Gradle > Tasks > build > build 더블 킄릭(이 부분으로 카바 가능)
- Gradle > Tasks > other > compileQuerydsl 더블 클릭
- build.gradle에서 설정한 경로에 Querydsl 관련 파일이 생성됨
- QHello라는 Java 파일이 생성된 것 역시 확인 가능
package com.study.querydsl.entity;
/**
* QHello is a Querydsl query type for Hello
*/
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QHello extends EntityPathBase<Hello> {
private static final long serialVersionUID = -1353511186L;
public static final QHello hello = new QHello("hello");
public final NumberPath<Long> id = createNumber("id", Long.class);
public QHello(String variable) {
super(Hello.class, forVariable(variable));
}
public QHello(Path<? extends Hello> path) {
super(path.getType(), path.getMetadata());
}
public QHello(PathMetadata metadata) {
super(Hello.class, metadata);
}
}
- QHello 파일은 위와 같이 작성이 되어있다
- 해당 파일은 엔티티를 보고 Querydsl에 의해 만들어진 파일이다
- Generated QFile은 Git 같은 오픈소스에서 관리가 되면 안된다
03-3. Querydsl 설정 테스트
src/test/java/com/study/querydsl/SpringDataJpaQuerydslApplicationTests.java
package com.study.querydsl;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.study.querydsl.entity.Hello;
import com.study.querydsl.entity.QHello;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Transactional
class SpringDataJpaQuerydslApplicationTests {
@PersistenceContext
private EntityManager em;
@Test
void contextLoads() {
// 객체 생성 : Hello Entity
Hello hello = new Hello();
em.persist(hello);
// Querydsl 사용을 위해 JPAQueryFactory 생성
JPAQueryFactory query = new JPAQueryFactory(em);
QHello qHello = new QHello("h");
// Querydsl 사용
Hello result = query
.selectFrom(qHello)
.fetchOne();
// 검증 : 하나의 트랜잭션 단위 내에서 동일한 엔티티 반환을 보장해야 한다
assertThat(result).isEqualTo(hello);
assertThat(result.getId()).isEqualTo(hello.getId());
}
}
지금까지 위에서 Querydsl 관련 설정을 진행 하였다.
간단한 테스트 코드를 작성하여 설정이 잘 되었는지 확인 해보자.
테스트 코드를 실행하여 위와 같이 성공이 화면이 보이면
Querydsl 설정이 잘된 것이다.
04. 예제 도메인 모델
참고: 스프링 데이터 JPA와 동일한 예제 도메인 모델을 사용한다.
- 하나의 Team이 한 명의 Member를 갖는 구조
- Team(N) : Member(1) 관계
- xToOne(ManyToOne, OneToOne) 관계는 지연로딩(fetch = LAZY) 설정 필요
04-1. Member 엔티티 작성
package com.study.querydsl.entity;
import lombok.*;
import javax.persistence.*;
@Entity
@Getter
@Setter // 실무 지양
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA는 스펙상 기본 생성자가 필요하다
@ToString(of = {"id", "userName", "age"})
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String userName;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_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);
}
}
스프링 데이터 JPA를 진행할 때 만들었던
Member 엔티티와 동일한 구조로 작성 되었다.
id, userName, age를 기본 필드로 갖고
Team 과의 연관관계 설정을 위해 team 변수를 갖는다.
또한 현재 ManyToOne 관계이기에 지연로딩 설정을 추가 하였다.
04-2. Team 엔티티 작성
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Tostring(of = {"id", "name"})
public class Team {
@Id
@GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
// - 1:N 관계로, 연관관계의 주인을 Member의 Team 변수로 설정
// - 연관관계 주인쪽만 등록(INSERT), 수정(UPDATE), 삭제(DELETE)가 가능하다
// - 연관관계가 아닌 경우 읽기(SELECT)는 가능하지만, 나머지는 불가능하다
// - 정확하지는 않지만 실무에서는 양방향 관계보다는 단방향 연관관계로 전부 설정해서 사용한다고 함
@OneToMany(mappedby = "team")
private List<Member> members = new ArrayList<>();
// team명 셋팅 생성자
public Team(String name) {
this.name = name;
}
}
Team 엔티티의 경우 기본적으로 id, name(팀명)을
필드로 갖고 List<Member
> members 변수를 통해 연관관계 설정을 하였다.
04-3. 엔티티 테스트 코드 작성
package com.study.querydsl.entity;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Transactional
class MemberTest {
@PersistenceContext
EntityManager em;
@Test
@Rollback(false)
public void testEntity() {
Team teamA = new Team("데이터 플랫폼 팀");
Team teamB = new Team("인프라 팀");
Team teamC = new Team("웹 개발 팀");
Team teamD = new Team("APP 개발 팀");
em.persist(teamA);
em.persist(teamB);
em.persist(teamC);
em.persist(teamD);
// teamA
Member member1 = new Member("김영민", 30, teamA);
Member member2 = new Member("원영식", 30, teamA);
em.persist(member1);
em.persist(member2);
// teamB
Member member3 = new Member("김진엽", 29, teamB);
Member member4 = new Member("박진우", 29, teamB);
Member member5 = new Member("임수현", 29, teamB);
em.persist(member3);
em.persist(member4);
em.persist(member5);
// teamD
Member member6 = new Member("김주한", 19, teamD);
em.persist(member6);
// 초기화
em.flush(); // DB 반영
em.clear(); // 영속성 컨텍스트 초기화
//---------------------------------------------------------------------------//
// 01. 단일 회원 테스트
Member findMemberByUserName = em.createQuery("select m from Member m where m.userName = :userName", Member.class)
.setParameter("userName", "김영민")
.getSingleResult();
// assertThat(member1).isEqualTo(findMemberByUserName); // em.flush(), em.clear()를 사용하지 않고 비교 == 'true'
assertThat(member1.getUserName()).isEqualTo(findMemberByUserName.getUserName());
// 02. 전체 팀 조회
List<Team> findTeam = em.createQuery("select t from Team t", Team.class).getResultList();
assertThat(findTeam.size()).isEqualTo(4); // 팀이 4개가 맞는가?
// assertThat(findTeam.size()).isEqualTo(5); // fail
// 03. 전체 회원 조회
/*List<Member> findMember = em.createQuery("select m from Member m", Member.class).getResultList();
System.out.println("----------------------------------------------------");
for (Member member : findMember) {
System.out.println("[01: AAA] -> ✅ member = " + member);
System.out.println("[02: BBB] ---> ⚡ member.team = " + member.getTeam()); // 지연로딩 -> 실제 team.get
System.out.println();
}
System.out.println("----------------------------------------------------");*/
}
}
- Team, Member를 각각 생성한 후 전체 회원을 조회하는 테스트 코드 작성
- 지연 로딩으로 설정된 team의 경우 실제 사용이 될 때 쿼리가 발생 된다
- 테스트 코드를 돌렸을 때 문제가 없으면 연관관계 셋팅과 엔티티 생성이 잘 된 것이다
댓글남기기