Querydsl을 사용하는 이유
- 쿼리를 자바코드로 -> 컴파일시점에 오류
- 동적쿼리 쉬움
- 쿼리와 유사한 코드
build.gradle
- plugins 추가
- dependencied 추가
- 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
- spring-boot
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에서 지원하지 않으므로)
- 서브쿼리를 join으로 변경 (불가능할 수 있다.)
- 쿼리를 2번 분리하여 실행
- 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 활용
- JpaRepository를 상속받아 JPA 사용
- 사용자정의 레포지토리(RepositoryCustom / RepositoryImpl)사용으로 querydsl사용
- 혹은 별도의 @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);
}
}
'스프링 > 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 |
댓글