Querydsl

배세훈·2022년 10월 10일
0

JPA

목록 보기
1/3

Querydsl 설정과 검증

스프링 부트 2.6이상, Querydsl 5.0 지원 방법

  • querydsl-jpa, querydsl-apt를 추가하고 버전을 명시해야 한다.
  • Querydsl 5.0 변경 사항
    - PageableExecutionUtils 클래스 사용 패키지 변경
    - 기능이 Deprecated 된 것이 아니고 사용 패키지 위치가 변경되었다. 기존 위치를 신규 위치로 변경하면 문제없이 사용할 수 있다.
    • Querydsl fetchResults(), fetchCount() Deprecated(향후 미지원)
      • Querydsl은 향후 fetchCount(), fetchResult()를 지원하지 않기로 결정했다.
buildscript{
	ext{
    	queryDslVersion = "5.0.0"
    }
}

plugins{
	id 'org.springframework.boot' version '2.6.2'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    // querydsl 추가
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    id 'java'
}

group = 'study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations{
	compileOnly{
    	extendsFrom annotationProcessor
    }
}

repositories{
	mavenCentral()
}

dependencies{
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework-boot:spring-boot-starter-web'
    
    // querydsl 추가
    implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
    annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}"
    
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test{
	useJUnitPlatform()
}

// Querydsl 설정부, default는 build 디렉토리에 생성되나 IntelliJ IDLE을 사용시 gradle에서 한번 읽고 IntelliJ에서 중복 읽음으로써 버그 발생하는 현상을 없애기 위해
def generated = 'src/main/generated'

// query QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile){
	options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// java source set에 querydsl QClass 위치 추가
sourceSets{
	main.java.srcDirs += [ generated ]
}


// gradle clean시에 QClass 디렉토리 삭제
clean{
	delete file(generated)
}

gralde 설정이 완료되었다면
Gradle - Tasks - build - build 실행하여 Q~ 라고 자동으로 생성된 클래스를 확인할 수 있다.

  • Q타입은 컴파일 시점에 자동 생성되므로 버전관리(git)에 포함하지 않는 것이 좋다. 앞서 설정에서 생성 위치를 gradle build 폴더 아래 생성되도록 했기 때문에 이 부분도 자연스럽게 해결된다.
    (대부분 gradle build 폴더를 git에 포함하지 않는다.)

도메인 모델 설계

Member

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member{
	@Id @GeneratedValue
   	private Long id;
    
    @Setter private String useraname;
	
    @Setter private int age;
    
    @Setter
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}

Team

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {
	@Id @GeneratedValue
    private Long id;
    
    @Setter private String name;

	@Setter
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    
    public Team(String name){
    	this.name = name;
    }
}

JPQL vs Querydsl

JPQL

@Test
void startJPQL(){
	Member findByJPQL = em.createQuery(
          "select m from Member m " +
              "where m.username = :username", Member.class)
      .setParameter("username", "member1")
      .getSingleResult();
    
    assertThat(findByJPQL.getUsername()).isEqualTo("member1");
}

JPQL을 작성하려면 문자열로 작성해야 한다. 문자열은 컴파일 단계에서 오류를 발견할 수 없다. 사소한 띄어쓰기로 예외가 발생할 수 있다. 실제로 코드가 동작하는 순간가지 에러를 발견할 수 없을 것이다.

Querydsl

@Test
void startQuerydsl(){
	QMember m = new QMember("m");
    
    Member findMember = jpaQueryFactory
    	.select(m)
        .from(m)
        .where(m.username.eq("member1"))
        .fetchOne();
        
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

JPQL과 동일한 결과를 가져오는 테스트 코드다. Querydsl은 쿼리에서 사용하는 명령어를 자바코드로 작성할 수 있고 컴파일러가 오류를 검증 할 수 있다.

  • JPAQueryFactory를 필드로 제공하면 동시성 문제는 어떻게 될까?
    동시성 문제는 JPAQueryFactory를 생성할 때 제공하는 EntityManager(em)에 달려있다. 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에 동시성 문제는 걱정하지 않아도 된다.

기본 Q-Type 활용

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

QMember qMember = new QMember("m"); // 별칭 직접 지정
QMember qMember = QMember.member; // 기본 인스턴스 사용(권장, static import와 함께 사용하는 것을 권장)

Querydsl에서 사용되는 JPQL이 궁금할 때

application.yml에 spring.jpa.properties.hibernate.use_sql_comments: true

검색조건 쿼리

@Test
void search(){
	Member findMember = jpaQueryFactory
    	.selectFrom(member)
        .where(member.username.eq("member1")
        	.and(member.age.eq(10)))
        .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

검색조건은 메서드 체인으로 연결할 수 있고 모든 검색 조건을 제공하고 있다.

member.username.eq("member1") username = 'member1'
member.username.ne("member1") username != 'member1'
member.username.eq("member1").not() username != 'mebber1'
member.username.isNotNull() username is not null
member.age.in(10, 20) age in (10, 20)
member.age.notIn(10,20) age not in (10, 20)
member.age.between(10,30) between 10, 30
member.age.goe(30) age >= 30
member.age.gt(30) age > 30
member.age.loe(30) age <= 30
member.age.lt(30) age < 30
member.username.like("member%") like 'member%' 검색
member.username.contains("member") like '%member%' 검색
member.username.startsWith("member") like 'member%' 검색

AND 조건을 파라미터로 처리

@Test
void searchAndParam(){
	Member findMember = jpaQueryFactory
    	.selectFrom(member)
        .where(
        	member.username.eq("member1"),
            member.age.eq(10)
        )
        .fetchOne();
        
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

where()에 파라미터로 검색조건을 추가하면 AND 조건이 추가된다.
이 경우 NULL 값은 무시한다.

결과 조회

  • fetch: 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne: 단건 조회
    - 결과 없으면 null
    • 결과가 둘 이상이면: com.querydsl.core.NonUniqueResultException
  • fetchFirst: limit(1).fetchOne()

정렬

  • desc(), asc(): 일반 정렬
  • nullsLast(), nullsFirst(): null 데이터 순서 부여

집합

@Test
void aggregation(){
	List<Tuple> result = queryFactory
    	.select(member.count(),
        		member.age.sum(),
                member.age.avg(),
                member.age.max(),
                member.age.min())
        .from(member)
        .fetch();
        
    Tuple tuple = result.get(0);
    assertThat(tuple.get(member.age.sum())).isEqualTo(100);
    assertThat(tuple.get(member.age.avg())).isEqualTo(25);
    assertThat(tuple.get(member.age.max())).isEqualTo(40);
}

조인 기본 조인

  • 조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.

  • join(조인 대상, 별칭으로 사용할 Q타입)

@Test
void join(){
	List<Member> result = queryFactory
    	.selectFrom(member)
        .join(member.team, team)
        .where(team.name.eq("teamA"))
        .fetch();
        
    assertThat(result)
    	.extracting("username")
       	.containsExactly("member1", "member2");
}
  • join(), innerJoin(): 내부 조인(inner join)
  • leftJoin(): left 외부 조인(left outer join)
  • rightJoin(): right 외부 조인(right outer join)
  • JPQL의 ON 과 성능 최적화를 위한 fetch 조인 제공

세타 조인

  • 연관관계가 없는 필드로 조인한다.
@Test
void theta_join(){
	em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    
    List<Member> result = queryFactory
    	.select(member)
        .from(member, team)
        .where(member.username.eq(team.name))
        .fetch();
        
    assertThat(result)
    	.extracting("username")
        .containsExactly("teamA", "teamB");
}
  • from 절에 여러 엔티티를 선택해서 세타 조인
  • 외부 조인 불가능 다음에 설명할 조인 on을 사용하면 외부 조인 가능

조인 on 절

  • ON 절을 활용한 조인(JPA 2.1부터 지원)
    - 조인 대상 필터링
@Test
void join_on_filtering(){
	List<Tuple> result = queryFactory
    	.select(member, team)
        .from(member)
        .leftJoin(member.team, team).on(team.name.eq("teamA"))
        .fetch();
        
    for(Tuple tuple : result){
    	System.out.println("tuple = " + tuple);
    }
}
  • 연관관계 없는 엔티티 외부조인
@Test
void join_on_no_relation(){
	em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    em.persist(new Member("teamC"));
    
    List<Tuple> result = queryFactory
    	.select(member, team)
        .from(member)
        .leftJoin(team).on(member.username.eq(team.name))
        .fetch();
        
    for(Tuple tuple : result){
    	System.out.println("tuple = " + tuple);
    }
}
  • 하이버네이트 5.1 부터 on을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다.
  • 문법을 잘 봐야 한다. leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
    - 일반조인: leftJoin(member:team, team)
    • on 조인: from(member).leftJoin(team).on(xxx)

조인 페치 조인

  • SQL 조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능이다. 주로 성능 최적화에 사용하는 방법이다.
@Test
void fetchJoinUse(){
	em.flush();
    em.clear();
    
    Member findMember = queryFactory
    	.selectFrom(member)
        .join(member.team, team).fetchJoin()
        .where(membe.username.eq("member1"))
        .fetchOne();
        
    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("페치 조인 적용").isTrue();
}
  • 즉시로딩으로 Member, Team SQL 쿼리 조인으로 한번에 조회한다.
  • join(), leftJoin() 등 조인 기능 뒤에 fetchJoin() 이라고 추가하면 된다.

서브 쿼리

@Test
void subQuery(){
	QMember memberSub = new QMember("memberSub");
    
    List<Member> result = queryFactory
    	.selectFrom(member)
        .where(member.age.eq(
        	JPAExpressions
            	.select(memberSub.age.max())
                .from(memberSub)
        ))
        .fetch();
        
    assertThat(result)
    	.extracting("age")
        .containsExactly(40);
}
@Test
void subQueryGoe(){
	QMember memberSub = new QMember("memberSub");
    
    List<Member> result = queryFactory
    	.selectFrom(member)
        .where(member.age.goe(
        	JPAExpressions
            	.select(memberSub.age.avg())
                .from(memberSub)
        ))
        .fetch();
    assertThat(result)
    	.extracting("age")
        .containsExactly(30, 40);
}
@Test
void subQueryIn(){
	QMember memberSub = new QMember("memberSub");
    
    List<Member> result = queryFactory
    	.selectFrom(member)
        .where(member.age.in(
        	JPAExpressions
            	.select(memberSub.age)
                .from(memberSub)
                .where(memberSub.age.gt(10))
        ))
        .fetch();
    assertThat(result)
    	.extracting("age")
        .containsExactly(20, 30, 40);
}
@Test
void selectSubQuery(){
	QMember memberSub = new QMember("memberSub");
    
    List<Tuple> result = queryFactory
    	.select(member.username,
        	JPAExpressions
            	.select(memberSub.age.avg())
                .from(memberSub)
        )
        .from(member)
        .fetch();
    
    for(Tuple tuple : result){
    	System.out.println("tuple = " + tuple);
    }
}
  • JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl 도 지원하지 않는다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.

Case문

@Test
void basicCase(){
	List<String> result = queryFactory
    	.select(member.age
        	.when(10).then("10")
            .when(20).then("20")
            .otherwise("00")
        )
        .from(member)
        .fetch();
}
@Test
void complexCase(){
	List<String> result = queryFactory
    	.select(
        	new CaseBuilder()
            	.when(member.age.between(0, 20)).then("0~20")
                .when(member.age.between(21, 30)).tehn("20~30")
                .otherwise("00")
        )
        .from(member)
        .fetch();
}

상수, 문자 더하기

@Test
void constant(){
	List<Tuple> result = queryFactory
    	.select(member.username, Expressions.constant("A"))
        .from(member)
        .fetch();
}
@Test
void concat(){
	List<String> result = queryFactory
    	.select(member.username.concat("_").concat(member.age.stringValue()))
        .from(member)
        .where(member.username.eq("member1"))
        .fetch();
}
  • 문자가 아닌 다른 타입들은 stringValue()로 문자로 변환할 수 있다.
    이 방법은 ENUM을 처리할 때도 자주 사용한다.
profile
성장형 인간

0개의 댓글