본문 바로가기
스프링/JPA

Querydsl

by 공부 안하고 싶은 사람 2021. 8. 24.
반응형

Querydsl을 사용하는 이유

  • 쿼리를 자바코드로 -> 컴파일시점에 오류
  • 동적쿼리 쉬움
  • 쿼리와 유사한 코드

 

 

build.gradle

  1. plugins 추가
  2. dependencied 추가
  3. Querydsl 빌드 설정 추가
plugins {
...
    //querydsl 추가
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
...
}

dependencies {
...
    //querydsl 추가
    implementation 'com.querydsl:querydsl-jpa'
...
}

//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
}
//querydsl 추가 끝

// 빌드 의존성 충돌로 인한 버전 명시 추가
configurations.all {
    resolutionStrategy {
        force 'com.google.guava:guava:26.0-jre', 'com.google.code.findbugs:jsr305:3.0.2', 'org.javassist:javassist:3.27.0-GA'
    }
}

gradle - compileQuerydsl 하면 Q파일생성

 

 

동작 테스트

@Entity
@Getter @Setter
public class Hello {

    @Id
    @GeneratedValue
    private Long id;
}
@SpringBootTest @Transactional
class QueryDslApplicationTests {
    @Autowired EntityManager em;

    @Test
    void contextLoads() {
        Hello hello = new Hello();
        em.persist(hello);
        JPAQueryFactory query = new JPAQueryFactory(em);
        QHello qHello = QHello.hello; //Querydsl Q타입 동작 확인
        Hello result = query
            .selectFrom(qHello)
            .fetchOne();
        Assertions.assertThat(result).isEqualTo(hello);
    }
}

 

 

라이브러리 살펴보기

  • com.querydsl:querydsl-jpa
    • querydsl-apt: Querydsl 관련 코드 생성 기능 제공
    • querydsl-jpa: querydsl 라이브러리
  • spring-boot-starter-web
    • spring-boot-starter-tomcat: 톰캣 (웹서버)
    • spring-webmvc: 스프링 웹 MVC
  • spring-boot-starter-data-jpa
    • spring-boot-starter-aop
    • spring-boot-starter-jdbc
      • HikariCP 커넥션 풀 (부트 2.0 기본)
      • hibernate + JPA: 하이버네이트 + JPA
      • spring-data-jpa: 스프링 데이터 JPA
  • spring-boot-starter(공통): 스프링 부트 + 스프링 코어 + 로깅
    • spring-boot
      • spring-core
    • spring-boot-starter-logging
      • logback, slf4j

 

 

application.yml

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/querydsl
    username: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show_sql: true
        format_sql: true

logging.level:
  org.hibernate.SQL: debug
# org.hibernate.type: trace

도메인

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@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);
        }
    }
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}
@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;

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

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

 

 

Jpql vs Querydsl

@SpringBootTest
@Transactional
public class QueryDslBasicTest {
    @PersistenceContext
    EntityManager em;

    @BeforeEach
    public void testEntity() {
        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);
    }

    @Test         //member1을 찾아라.
    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        //member1을 찾아라.
    public void startQuerydsl() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m = new QMember("m");

        Member findMember = queryFactory
            .select(m)
            .from(m)
            .where(m.username.eq("member1"))//파라미터 바인딩 처리
            .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
}
  • EntityManager 로 JPAQueryFactory 생성
  • Querydsl은 JPQL 빌더
  • JPQL: 문자(실행 시점 오류), Querydsl: 코드(컴파일 시점 오류) -> data-jpa 면 해결가능
  • JPQL: 파라미터 바인딩 직접, Querydsl: 파라미터 바인딩 자동 처리
// 필드로 선언하여도 문제없다
// 여러 쓰레드에서 동시에 EM에 접근하여도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에 동시성 문제는 걱정하지 않아도 된다.
JPAQueryFactory queryFactory;

@BeforeEach
public void before() {
    queryFactory = new JPAQueryFactory(em);
    //…
}

 

 

기본 Q-type활용

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

QMember qMember = new QMember("m"); //별칭(as) 직접 지정

QMember qMember = QMember.member; //기본 인스턴스 사용 
// -> QMember를 static import사용 권장

Querydsl에서 실행되는 쿼리 확인 가능

spring.jpa.properties.hibernate.use_sql_comments: true

검색조건

member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'
member.username.isNotNull() //이름이 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.username.contains("member") // like ‘%member%’ 검색
member.username.startsWith("member") //like ‘member%’ 검색
  • 검색조건을 .and() .or()으로 연결
  • Select 와 from을 selectfrom으로 합칠 수 있다.
  • where에 검색조건을 다른 파라미터로 연결하면 and조건으로 추가
    • 이 경우 null값은 무시 -> 메서드 추출을 활용하여 동적쿼리 깔금하게

결과조회

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단 건 조회
    • 결과가 없으면 : null
    • 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
  • fetchFirst() : limit(1).fetchOne()
  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
  • fetchCount() : count 쿼리로 변경해서 count 수 조회

정렬

List<Member> result = queryFactory
 .selectFrom(member)
 .where(member.age.eq(100))
 .orderBy(member.age.desc(), member.username.asc().nullsLast())
 .fetch();
  • Desc(), asc() : 정렬
  • nullsLast(), nullsFirst() : null 데이터 순서 부여

페이징

@Test
public void paging1() {
    List<Member> result = queryFactory
      .selectFrom(member)
      .orderBy(member.username.desc())
      .offset(1) //0부터 시작(zero index)
      .limit(2) //최대 2건 조회
      .fetch();
    assertThat(result.size()).isEqualTo(2);
}

@Test // 전체 조회 (count쿼리도 포함)
public void paging2() {
    QueryResults<Member> queryResults = queryFactory
      .selectFrom(member)
      .orderBy(member.username.desc())
      .offset(1)
      .limit(2)
      .fetchResults();
    assertThat(queryResults.getTotal()).isEqualTo(4);
    assertThat(queryResults.getLimit()).isEqualTo(2);
    assertThat(queryResults.getOffset()).isEqualTo(1);
    assertThat(queryResults.getResults().size()).isEqualTo(2);
}

조인

조인의 기본 문법은

첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정

@Test
public void join() throws Exception {
    QMember member = QMember.member;
    QTeam team = QTeam.team;
    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() : rigth 외부 조인(rigth outer join)

세타조인

연관관계가 없어도 from절에 여러 엔티티, where에 조건을 설정하여 조인가능

(단, 외부 조인 불가능 -> on으로 가능)

@Test
public void theta_join() throws Exception {
     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");
}

조인 - on절

조인 대상 필터링

내부조인의 경우 where절에서 필터링 하는것과 동일하므로, where를 사용하도록 하자.

외부조인의 경우에만 사용하도록 하자.

@Test
public void join_on_filtering() throws Exception {
     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);
     }
}
t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]

연관관계 없는 엔티티 외부 조인

@Test
public void join_on_no_relation() throws Exception {
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    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("t=" + tuple);
    }
}
t=[Member(id=3, username=member1, age=10), null]
t=[Member(id=4, username=member2, age=20), null]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]
t=[Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)]
t=[Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]
  • 일반조인: leftJoin(member.team, team)
  • on조인: from(member).leftJoin(team).on(xxx)

페치 조인

join(), leftJoin() 등 조인 기능 뒤에 fetchJoin() 이라고 추가

@PersistenceUnit
EntityManagerFactory emf;

@Test // 일반
public void fetchJoinNo() throws Exception {
     em.flush();
     em.clear();
     Member findMember = queryFactory
         .selectFrom(member)
         .where(member.username.eq("member1"))
         .fetchOne();

     boolean loaded =
    emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
     assertThat(loaded).as("페치 조인 미적용").isFalse();
}

@Test // 패치조인
public void fetchJoinUse() throws Exception {
     em.flush();
     em.clear();
     Member findMember = queryFactory
         .selectFrom(member)
         .join(member.team, team).fetchJoin()
         .where(member.username.eq("member1"))
         .fetchOne();

     boolean loaded =
    emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
     assertThat(loaded).as("페치 조인 적용").isTrue();
}

서브쿼리

com.querydsl.jpa.JPAExpressions 사용

where절

@Test
public void subQuery() throws Exception {
     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);
}

select절

@Test
public void subQuery_select() throws Exception {
  QMember memberSub = new QMember("memberSub");

  List<Tuple> fetch = queryFactory
    .select(member.username,
            JPAExpressions
            .select(memberSub.age.avg())
            .from(memberSub)
           ).from(member)
    .fetch();

  for (Tuple tuple : fetch) {
    System.out.println("username = " + tuple.get(member.username));
    System.out.println("age = " +
                       tuple.get(JPAExpressions.select(memberSub.age.avg())
                                 .from(memberSub)));
  }
}
  • Static import를 통해 보기 쉽게 만들 수도 있다.
  • from절의 서브쿼리는 지원하지 않는다.(Jpql에서 지원하지 않으므로)
    1. 서브쿼리를 join으로 변경 (불가능할 수 있다.)
    2. 쿼리를 2번 분리하여 실행
    3. NativeSQL

case문

단순

List<String> result = queryFactory
   .select(member.age
       .when(10).then("열살")
       .when(20).then("스무살")
       .otherwise("기타"))
   .from(member)
   .fetch();

복잡

CaseBuilder 이용

List<String> result = queryFactory
   .select(new CaseBuilder()
       .when(member.age.between(0, 20)).then("0~20살")
       .when(member.age.between(21, 30)).then("21~30살")
       .otherwise("기타"))
   .from(member)
   .fetch();

orderby에서 case문으로 커스텀 정렬

NumberExpression<Integer> rankPath = new CaseBuilder()
 .when(member.age.between(0, 20)).then(2)
 .when(member.age.between(21, 30)).then(1)
 .otherwise(3);

위 조건을 select문 케이스문으로 사용한 다음, orderby에서 해당 컬럼에 대하여 정렬할 수도 있다.


상수, 문자 더하기

Expressions.constant(xxx) 사용

문자 추가

Tuple result = queryFactory
   .select(member.username, Expressions.constant("A"))
   .from(member)
   .fetchFirst();

문자열 concat (.concat()사용)

String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
 .from(member)
 .where(member.username.eq("member1"))
 .fetchOne();

member.age.stringValue()

문자가 아닌 다른 타입들은 stringValue() 로 문자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때도 자주 사용


distinct

List<String> result = queryFactory
 .select(member.username).distinct()
 .from(member)
 .fetch();

프로젝션과 결과 반환

튜플

  • 대상이 하나이면 타입을 명확히 지정할 수 있음
  • 대상이 둘 이상이면 튜플 / DTO로 조회
// 하나
List<String> result = queryFactory
 .select(member.username)
 .from(member)
 .fetch();

// 둘 이상
List<Tuple> result = queryFactory
 .select(member.username, member.age)
 .from(member)
 .fetch();

for (Tuple tuple : result) {
 String username = tuple.get(member.username);
 Integer age = tuple.get(member.age);
 System.out.println("username=" + username);
 System.out.println("age=" + age);
}

DTO

  • 순수JPA의 jpql으로 DTO를 반환할 때 생성자 방식으로 package이름을 다 적으므로 지저분
  • QueryDsl
    • 프로퍼티 접근
    • 필드 직접 접근
    • 생성자 사용
    • 별칭이 다를 때
// 프로퍼티
List<MemberDto> result = queryFactory
 .select(Projections.bean(MemberDto.class,
 member.username,
 member.age))
 .from(member)
 .fetch();

// 필드
List<MemberDto> result = queryFactory
 .select(Projections.fields(MemberDto.class,
 member.username,
 member.age))
 .from(member)
 .fetch();

// 생성자
List<MemberDto> result = queryFactory
            .select(Projections.constructor(MemberDto.class,
                member.username,
                member.age))
            .from(member)
            .fetch();

// 별칭이 다를 떄
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
            .select(Projections.fields(MemberDto.class,
                member.username.as("name"),
                ExpressionUtils.as(
                    JPAExpressions
                    .select(memberSub.age.max())
                    .from(memberSub), "age")
                )
            ).from(member)
            .fetch();
  • 별칭이 다를 때
    • ExpressionUtils.as(source,alias) : 필드나, 서브 쿼리에 별칭 적용
    • username.as("memberName") : 필드에 별칭 적용

@QueryProjection

@QueryProjection // 생성자에 어노테이션 추가
public MemberDto(String username, int age) {
  this.username = username;
  this.age = age;
}

// DTO를 Q파일로 생성하여 사용할 수 있다.
List<MemberDto> result = queryFactory
 .select(new QMemberDto(member.username, member.age))
 .from(member)
 .fetch();

동적쿼리 해결하는 법

BooleanBuilder

@Test
public void BooleanBuilder() throws Exception {
  String usernameParam = "member1";
  Integer ageParam = 10;

  List<Member> result = serchMember1(usernameParam, ageParam);
  Assertions.assertThat(result.size()).isEqualTo(1);
}

private List<Member> serchMember1(String usernameParam, Integer ageParam) {
  BooleanBuilder builder = new BooleanBuilder();
  if (usernameParam != null)
    builder.and(member.username.eq(usernameParam));
  if (ageParam != null)
    builder.and(member.age.eq(ageParam));

  return queryFactory
            .selectFrom(member)
            .where(builder)
            .fetch();
}
  • BooleanBuilder(member.name.eq("test1") 과 같이 기본 조건 추가 가능

Where 다중 파라미터

@Test
public List<Member> serchMember2(String usernameParam, Integer ageParam) throws Exception {
  return queryFactory
    .selectFrom(member)
    .where(usernameEq(usernameParam), ageEq(ageParam))
    .fetch();
}

private Predicate ageEq(Integer ageParam) {
  return ageParam != null ? member.age.eq(ageParam) : null;
}

private Predicate usernameEq(String usernameParam) {
  return usernameParam != null ? member.username.eq(usernameParam) : null;
}
  • where조건의 null은 무시된다
  • 메서드를 재활용/조합 할 수 있다
  • 가독성이 높다

벌크 연산

@Test
public void bulkUpdate() throws Exception {
  queryFactory
    .update(member)
    .set(member.username, "비회원")
    .where(member.age.lt(28))
    .execute();

  queryFactory
    .update(member)
    .set(member.age, member.age.add(1))
    .execute();

  queryFactory
    .delete(member)
    .where(member.age.gt(10))
    .execute();
}

JPQL 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전하다.


SQL function 호출

Dialect에 등록된 내용만 호출가능

@Test
public void sqlFuction() throws Exception {
  queryFactory
    .select(Expressions.stringTemplate(
      "function('replace', {0}, {1}, {2})", 
      member.username, "member", "M"))
    .from(member)
    .fetch();
} 

실무 활용

동적쿼리는 기본조건 혹은 페이징이 있는게 안전하다

BooleanBuilder / Where절 파라미터로 조건을 추가할 수 있다.

(재사용성과 가독성을 위해 Where 절 사용이 좋아 보인다)

Spring-data-jpa 활용

  1. JpaRepository를 상속받아 JPA 사용
  2. 사용자정의 레포지토리(RepositoryCustom / RepositoryImpl)사용으로 querydsl사용
  3. 혹은 별도의 @Repository로 분리하여 사용

페이징(MemberRepositoryImpl.java)

@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
  QueryResults<MemberTeamDto> memberTeamDtoQueryResults = queryFactory
    .select(new QMemberTeamDto(
      member.id,
      member.username,
      member.age,
      team.id,
      team.name))
    .from(member)
    .leftJoin(member.team, team)
    .where(usernameEq(condition.getUsername()),
           teamNameEq(condition.getTeamName()),
           ageGoe(condition.getAgeGoe()),
           ageLoe(condition.getAgeLoe()))
    .offset(pageable.getOffset())
    .limit(pageable.getPageSize())
    .fetchResults();

  // QueryResults를 반환 후 3가지 정보로 return
  List<MemberTeamDto> content = memberTeamDtoQueryResults.getResults();
  long total = memberTeamDtoQueryResults.getTotal();
  return new PageImpl<>(content, pageable, total);
}

@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
  List<MemberTeamDto> content = queryFactory
    .select(new QMemberTeamDto(
      member.id,
      member.username,
      member.age,
      team.id,
      team.name))
    .from(member)
    .leftJoin(member.team, team)
    .where(usernameEq(condition.getUsername()),
           teamNameEq(condition.getTeamName()),
           ageGoe(condition.getAgeGoe()),
           ageLoe(condition.getAgeLoe()))
    .offset(pageable.getOffset())
    .limit(pageable.getPageSize())
    .fetch();

  // fetch 후 직접 count query 
  long total = queryFactory
    .select(member)
    .from(member)
    .leftJoin(member.team, team)
    .where(usernameEq(condition.getUsername()),
           teamNameEq(condition.getTeamName()),
           ageGoe(condition.getAgeGoe()),
           ageLoe(condition.getAgeLoe()))
    .fetchCount();
  return new PageImpl<>(content, pageable, total);
}
  • 전체 카운트를 조회 하는 방법을 최적화 할 수 있으면 이렇게 분리하면 된다. (예를 들어서 전체 카운트를 조회할 때 조인 쿼리를 줄일 수 있다면 상당한 효과가 있다.)
  • 코드를 리펙토링해서 내용 쿼리과 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다.
  • count 쿼리가 생략 가능한 경우 생략해서 처리 ( 아래의 최적화 )
    • 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
    • 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)

최적화

// fetch 후 직접 count query 
long total = queryFactory
  .select(member)
  .from(member)
  .leftJoin(member.team, team)
  .where(usernameEq(condition.getUsername()),
         teamNameEq(condition.getTeamName()),
         ageGoe(condition.getAgeGoe()),
         ageLoe(condition.getAgeLoe()))
  .fetchCount();
return new PageImpl<>(content, pageable, total);

////////////////////////////////////////////////////
// 위 기능 최적화
JPAQuery<Member> countQuery = queryFactory
  .select(member)
  .from(member)
  .leftJoin(member.team, team)
  .where(usernameEq(condition.getUsername()),
         teamNameEq(condition.getTeamName()),
         ageGoe(condition.getAgeGoe()),
         ageLoe(condition.getAgeLoe())
        );

return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);

정렬

  • 스프링 데이터 JPA는 자신의 정렬(Sort)을 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는 기능을 제공한다
  • 조건이 조금만 복잡해져도 Pageable 의 Sort 기능을 사용하기 어렵다. 루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 Sort 를 사용하기 보다는 파라미터를 받아서 직접 처리하는 것을 권장
JPAQuery<Member> query = queryFactory
  .selectFrom(member);
for (Sort.Order o : pageable.getSort()) {
     PathBuilder pathBuilder = new athBuilder(member.getType(),member.getMetadata());
     query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC, pathBuilder.get(o.getProperty())));
}
List<Member> result = query.fetch();

인터페이스 (비추)

interface MemberRepository extends JpaRepository<User, Long>,
QuerydslPredicateExecutor<User> {
}
Iterable result = memberRepository.findAll(
 member.age.between(10, 40)
 .and(member.username.eq("member1"))
);

한계점

  • 조인X (묵시적 조인은 가능하지만 left join이 불가능하다.)
  • 클라이언트가 Querydsl에 의존해야 한다. 서비스 클래스가 Querydsl이라는 구현 기술에 의존
  • 복잡한 실무환경에서 사용하기에는 한계가 명확

Querydsl Web 지원 (비추)

API파라미터로 Predicate값을 전달받을 수 있다.

@RequestMapping(value = "/", method = RequestMethod.GET)
  String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,    
          Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
    model.addAttribute("users", repository.findAll(predicate, pageable));
    return "index";
  }

한계점

  • 단순한 조건만 가능
  • 조건을 커스텀하는 기능이 복잡하고 명시적이지 않음
  • 컨트롤러가 Querydsl에 의존
  • 복잡한 실무환경에서 사용하기에는 한계가 명확

Repository 지원

QuerydslRepositorySupport

장점

  • getQuerydsl().applyPagination() 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환 가능(단! Sort는 오류발생)
  • EntityManager 제공

한계

  • Querydsl 3.x 버전을 대상으로 만듬
  • QueryFactory 를 제공하지 않음
  • 스프링 데이터 Sort 기능이 정상 동작하지 않음

직접 만들어 한계점 극복하기

장점

  • 페이징 편리
  • 페이징 / 카운트 분리
  • Sort 지원
  • select(), selectFrom() 시작
  • EntityManager, QueryFactory 제공

CustomSupport 추상 클래스

import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import
    org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.jpa.repository.support.Querydsl;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.repository.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;
import org.springframework.util.Assert;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.function.Function;

/**
 * Querydsl 4.x 버전에 맞춘 Querydsl 지원 라이브러리
 *
 * @author Younghan Kim
 * @see
org.springframework.data.jpa.repository.support.QuerydslRepositorySupport
 */
@Repository
public abstract class Querydsl4RepositorySupport {
    private final Class domainClass;
    private Querydsl querydsl;
    private EntityManager entityManager;
    private JPAQueryFactory queryFactory;
    public Querydsl4RepositorySupport(Class<?> domainClass) {
        Assert.notNull(domainClass, "Domain class must not be null!");
        this.domainClass = domainClass;
    }
    @Autowired
    public void setEntityManager(EntityManager entityManager) {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        JpaEntityInformation entityInformation =
            JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
        SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE;
        EntityPath path = resolver.createPath(entityInformation.getJavaType());
        this.entityManager = entityManager;
        this.querydsl = new Querydsl(entityManager, new
            PathBuilder<>(path.getType(), path.getMetadata()));
        this.queryFactory = new JPAQueryFactory(entityManager);
    }
    @PostConstruct
    public void validate() {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        Assert.notNull(querydsl, "Querydsl must not be null!");
        Assert.notNull(queryFactory, "QueryFactory must not be null!");
    }
    protected JPAQueryFactory getQueryFactory() {
        return queryFactory;
    }
    protected Querydsl getQuerydsl() {
        return querydsl;
    }
    protected EntityManager getEntityManager() {
        return entityManager;
    }
    protected <T> JPAQuery<T> select(Expression<T> expr) {
        return getQueryFactory().select(expr);
    }
    protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {
        return getQueryFactory().selectFrom(from);
    }
    protected <T> Page<T> applyPagination(Pageable pageable,
        Function<JPAQueryFactory, JPAQuery> contentQuery) {
        JPAQuery jpaQuery = contentQuery.apply(getQueryFactory());
        List<T> content = getQuerydsl().applyPagination(pageable,
            jpaQuery).fetch();
        return PageableExecutionUtils.getPage(content, pageable,
            jpaQuery::fetchCount);
    }
    protected <T> Page<T> applyPagination(Pageable pageable,
        Function<JPAQueryFactory, JPAQuery> contentQuery, Function<JPAQueryFactory,
        JPAQuery> countQuery) {
        JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory());
        List<T> content = getQuerydsl().applyPagination(pageable,
            jpaContentQuery).fetch();
        JPAQuery countResult = countQuery.apply(getQueryFactory());
        return PageableExecutionUtils.getPage(content, pageable,
            countResult::fetchCount);
    }
}

적용 예시

import static org.springframework.util.StringUtils.isEmpty;
import static study.querydsl.entity.QMember.member;
import static study.querydsl.entity.QTeam.team;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.entity.Member;
import study.querydsl.repository.support.Querydsl4RepositorySupport;

@Repository
public class MemberTestRepository extends Querydsl4RepositorySupport {

    public MemberTestRepository() {
        super(Member.class);
    }
    public List<Member> basicSelect() {
        return select(member)
            .from(member)
            .fetch();
    }
    public List<Member> basicSelectFrom() {
        return selectFrom(member)
            .fetch();
    }

    // QuerydslRepositorySupport
    public Page<Member> searchPageByApplyPage(MemberSearchCondition condition, Pageable pageable) {
        JPAQuery<Member> query = selectFrom(member)
            .leftJoin(member.team, team)
            .where(usernameEq(condition.getUsername()),
                teamNameEq(condition.getTeamName()),
                ageGoe(condition.getAgeGoe()),
                ageLoe(condition.getAgeLoe()));
        List<Member> content = getQuerydsl().applyPagination(pageable, query)
            .fetch();

        return PageableExecutionUtils.getPage(content, pageable, query::fetchCount);
    }

    // 추상화 1단계
    public Page<Member> applyPagination(MemberSearchCondition condition,
        Pageable pageable) {
        return applyPagination(pageable, contentQuery -> contentQuery
            .selectFrom(member)
            .leftJoin(member.team, team)
            .where(usernameEq(condition.getUsername()),
                teamNameEq(condition.getTeamName()),
                ageGoe(condition.getAgeGoe()),
                ageLoe(condition.getAgeLoe())));
    }

    // 카운트 쿼리와, 페이징 쿼리 분리
    public Page<Member> applyPagination2(MemberSearchCondition condition,
        Pageable pageable) {
        return applyPagination(pageable,
            contentQuery -> contentQuery
                .selectFrom(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())),
            countQuery -> countQuery
                .selectFrom(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe()))
        );
    }

    private BooleanExpression usernameEq(String username) {
        return isEmpty(username) ? null : member.username.eq(username);
    }
    private BooleanExpression teamNameEq(String teamName) {
        return isEmpty(teamName) ? null : team.name.eq(teamName);
    }
    private BooleanExpression ageGoe(Integer ageGoe) {
        return ageGoe == null ? null : member.age.goe(ageGoe);
    }
    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe == null ? null : member.age.loe(ageLoe);
    }
}
728x90
반응형

'스프링 > JPA' 카테고리의 다른 글

Spring-Data-Jpa  (0) 2021.08.06
Envers / spring-data-envers  (0) 2021.08.03
JPA 최적화, Hint  (0) 2021.06.04
JPA 변경/삭제 @Modifying  (0) 2021.06.04
FK가 PK가 아닌 다른 컬럼과 연관관계가 있을 때  (0) 2021.05.31

댓글