Querydsl
@Test // 기존 JPQL
public void startJPQL() {
//member 1 find
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 // Querydsl
public void startQuerydsl() {
// QMember m = new QMember("m");
// QMember m = QMember.member;
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
JPQL 빌더. 기존 JPQL은 파라미터 바인딩을 직접 하고 런타임 시점에서 오류를 확인할 수 있었지만, Querydsl은 파라미터 바인딩이 자동으로 처리되고 컴파일 시점에 오류를 확인할 수 있다.
JPAQueryFactory
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@Autowired
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void before() {
queryFactory = new JPAQueryFactory(em);
}
직접 필드로 JPAQueryFactory를 사용하는 위의 코드
@SpringBootApplication
public class QuerydslApplication {
public static void main(String[] args) {
SpringApplication.run(QuerydslApplication.class, args);
}
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom{
private final JPAQueryFactory queryFactory;
}
기존 설정에서 해당 JPAQueryFactory를 애플리케이션 로딩 시점에 빈으로 등록하여 사용할 수도 있다.
Q-Type
Querydsl은 엔티티를 Q엔티티명이라는 클래스로 추가적으로 만들어서 해당 Q-type 인스턴스를 이용한다.
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
같은 테이블을 조인하거나 서브쿼리로 쓰게 된다면 alias를 다르게 사용해야하므로 별칭 직접 지정 방법을 사용해야하고 그게 아니라면 기본 인스턴스 방법을 사용하여 static import해주는 것이 더 편리하다.
결과 조회
1. fetch() - 리스트 조회
반환 타입 List<Entity or Dto>
2. fetchOne() - 단 건 조회 ( 결과가 없으면 null, 결과가 둘 이상이면 NonUniqueResultException)
반환타입 Entity or Dto
3. fetchFirst() - limit(1).fetchOne()
반환타입 Entity or Dto
Deprecated
4. fetchResults() - 페이징 정보 포함, total count 쿼리 추가 실행
반환타입 QueryResult<Entity or Dto>
5. fetchCount() - count 쿼리
반환타입 Long,long
정렬
/**
* 1.회원 나이 내림차순
* 2.회원 이름 올림차순
* 3.2에서 회원이름이 없다면 마지막에 출력(nulls last)
*/
@Test
public void sort() {
em.persist(new Member(null, 100));
em.persist(new Member("member5",100));
em.persist(new Member("member6",100));
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(
member.age.desc(),
member.username.asc().nullsLast())
.fetch();
assertThat(result.size()).isEqualTo(3);
assertThat(result.get(1).getUsername()).isEqualTo("member6");
}
nullsLast(), nullsFirst() - null 데이터에 순서 부여
페이징
조회건수 제한 시,
@Test
public void paging() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetch();
assertThat(result.size()).isEqualTo(2);
}
전체 조회건수 필요 시,
@Test
public 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.count())).isEqualTo(4);
assertThat(tuple.get(member.age.avg())).isEqualTo(25);
}
다만 이 때 count 쿼리는 필요없는 left join을 한 채로 실행될 수 있기 때문에 해당 count 쿼리는 별도로 작성하는 것이 성능적인 이점을 보여줄 수 있다.
기본 join
@Test
public 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");
}
첫 번째 파라미터에 조인 대상을 지정, 두 번째 파라미터에 별칭으로 사용할 Q 타입 지정
위의 경우 QTeam.team을 static import하여 team이라는 별칭으로 QTeam 타입을 지정하여 조인을 사용하였다.
join - on절
@Test
public void join_on_filtering() {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team)
// .on(team.name.eq("teamA"))
.where(team.name.eq("teamA"))
.fetch();
}
조인 대상 필터링 - 해당 조건에 해당하는 데이터를 모두 가지고 오고 싶다면 left join을 사용하면 되지만, 단순히 inner join을 이용하여 조인 대상을 필터링하고 싶다면 보다 익숙한 where를 사용하는 게 더 권장된다.
@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)]
연관관계 없는 엔티티 외부 조인 - 기본 조인과는 다르게 left join() 부분에 엔티티 하나만 들어간다.
기본 조인 - .leftJoin(member.team, team)
on 조인 - .leftJoin(team).on(member.username.eq(team.name))
Fetch join
@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();
}
join(), left join() 기능 뒤에 fetchJoin()을 추가
서브쿼리
JPAExpressions 사용
/**
*서브쿼리 eq 사용
*/
@Test
public void subQuery() {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
select(memberSub.age.max())
.from(memberSub)
))
.fetch();
}
/**
*서브쿼리 goe 사용
*/
@Test
public void subQueryGoe() {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
}
/**
*select절 서브쿼리, where절 서브쿼리 in 사용
*/
@Test
public void subQueryInSelect() {
QMember memberSub = new QMember("memberSub");
List<Tuple> result = queryFactory
.select(member.username,
select(memberSub.age.avg())
.from(memberSub))
.from(member)
.where(member.age.in(
select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
}
from 절의 서브쿼리는 지원하지 않음. 이를 위한 해결방안으로는 서브쿼리를 join으로 변경 가능한 상황이면 변경하거나, 애플리케이션 차원에서 쿼리를 두 번 분리하여 실행하거나, nativeSQL을 사용하는 법들이 있다.
엔티티가 아닌 프로젝션 결과 반환(Tuple, Dto)
@Test
public void simpleProjection() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
프로젝션 대상이 하나라면 단순히 타입을 지정해서 반환할 수 있음. 그러나 프로젝션에 여러 타입의 데이터가 들어가게 된다면 타입지정은 불가. 이때 사용할 수 있는 것이 Tuple과 Dto
Tuple - 프로그래밍에 사용되는 데이터 구조로 여러 개의 요소를 담을 수 있는 순서가 있는 집합. 변경할 수 없는 시퀀스 형태의 객체로 한 번 생성되면 그 값을 수정하거나 삭제할 수 없음. 괄호() 를 사용하며 각 요소는 쉼표로 구분됨
// Tuple은 쿼리Dsl 패키지의 하부기술이기 때문에 Repository에서 사용은 괜찮지만 바깥 계층에서 해당 Tuple을 사용하는 것은 지양
@Test
public void tupleProjection() {
List<Tuple> result = queryFactory
.select(member.username, member.age)
.from(member)
.fetch();
for (Tuple tuple : result) {
// System.out.println("tuple = " + tuple);
String username = tuple.get(member.username);
Integer age = tuple.get(member.age);
System.out.println("username = " + username);
System.out.println("age = " + age);
}
}
// Tuple 사용
----------------------------------------------------------------------------------
// 결과를 DTO로 반환, 프로퍼티 접근번, 필드 직접 접근, 생성자 사용의 방식 지원
@Test // 프로퍼티 접근 - setter 사용(Projections.bean(Dto.class, 별칭.필드명...)
public void findDtoBySetter(){
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
@Test // 필드 직접 접근 - (Projections.fields(Dtol.class, 별칭.필드명...)
public void findDtoByField(){
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
@Test // 생성자 사용 - (Projections.constructor(Dto.class, 별칭.필드명...)
public void findDtoByConstructor(){
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
@Test // Dto의 필드명과 Entity의 필드명이 다를 때는 as 사용, 만약 생성자 방식으로 사용한다면 as 사용 불필요
public void findUserDto(){
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
ExpressionUtils.as(JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")
))
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
// QueryProjection은 DTO 클래소 Q-Type으로 생성 후 사용
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
// 매우 편리하고 Dto가 Querydsl에 의존하면서 컴파일 오류를 잡아내는 이점을 가져갈지,
// Querydsl에 의존하지 않으면서 여러 레이어를 돌아다닐 수 있는 Dto를 순수하게 유지하여 의존성에 대한 이점을 가져갈지 고민
동적 쿼리 Builder
@Test
public void dynamicQuery_BooleanBuilder() throws Exception {
String usernameParam = "member1";
Integer ageParam = null;
List<Member> result = searchMember1(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember1(String usernameCond, Integer ageCond) {
BooleanBuilder builder = new BooleanBuilder();
if (usernameCond != null) {
builder.and(member.username.eq(usernameCond));
}
if (ageCond != null) {
builder.and(member.age.eq(ageCond));
}
return queryFactory
.select(member)
.from(member)
.where(builder)
.fetch();
}
BooleanBuilder의 사용은 해당 쿼리가 한 눈에 들어오지 않는다는 단점이 존재.
동적 쿼리 Where 다중 파라미터
@Test
public void dynamicQuery_WhereParams() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember2(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
.select(member)
.from(member)
// .where(usernameEq(usernameCond), ageEq(ageCond))
.where(allEq(usernameCond, ageCond))
.fetch();
}
private BooleanExpression usernameEq(String usernameCond) {
return usernameCond != null ? member.username.eq(usernameCond) : null;
}
private BooleanExpression ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}
// 조건 간의 조립이 가능, 재사용성 높음
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
Where 다중 파라미터는 해당 searchMemberV2에서 실행하고자 하는 쿼리를 한 눈에 알아볼 수 있고 BooleanExpressions로 반환 타입을 설정하고 해당 동적 조건들을 만들어주면 allEq 처럼 각각의 조건들을 조립하여 새로운 조건을 만들어낼 수 있음(추가적인 null 체크 필요)
벌크 연산
@Test
public void bulkUpdate() {
long count = queryFactory
.update(member)
.set(member.username, "청년층")
.where(member.age.lt(29))
.execute();
em.flush();
em.clear(); // 벌크 연산 후, 영속성 컨텍스트 초기화
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
assertThat(members.get(0).getUsername()).isEqualTo("청년층");
}
@Test
public void bulkAdd() {
queryFactory
.update(member)
.set(member.age, member.age.add(1))
.execute();
}
@Test
public void bulkDelete() {
queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
}
벌크 연산 후 영속성 컨텍스트 초기화는 필수
스프링 데이터 JPA와 Querydsl
스프링 데이터 JPA는 쿼리메서드를 이용하여 간단한 정적 쿼리는 쉽게 구현할 수 있음. 반면 동적 쿼리는 구현에 있어 고민이 될 수 있지만 사용자 정의 Repository를 구현하고 이를 해당 interface "Entity"Repository에 상속받게 하는 구조로 Querydsl 전용 Repository를 만들 수 있음
public interface MemberRepository extends JpaRepository<Member, Long>,MemberRepositoryCustom {
}
JpaRepository를 상속받는 기존 MemberRepsotiroty
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
사용자 정의 Repository interface
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom{
private final JPAQueryFactory queryFactory;
@Override // @QueryProjections 를 이용한 Dto 반환, Where 다중 파라미터를 이용하여 동적쿼리 생성 해당 ~Eq는 밑에 위치
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return 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()))
.fetch();
}
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
}
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
}
private BooleanExpression usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return hasText(teamName) ? team.name.eq(teamName) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
}
해당 사용자 정의 Repository interface를 구현하는 구현 Repository
searchPageSimple (Content와 Count를 같이 조회)
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
QueryResults<MemberTeamDto> results = queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.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();
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
.fetchResults()를 이용하면 content와 totalcount를 한 번에 조회할 수 있다.( 검색 쿼리 한 번, 카운트 쿼리 한 번으로 총 쿼리 횟수는 2회). 이 때 fetchResults는 카운트 쿼리 실행 시에 필요없는 부분들(left join, order by 등)은 제외하고 실행된다
/* select
count(member1)
from
Member member1
left join
member1.team as team // 원래 쿼리
*/ select
count(m1_0.member_id)
from
member m1_0 // 실제 쿼리. 성능저하를 유발할 수 있기 때문에 필요없는 left join 제거
searchPageComplex (Content와 Count를 별개로 조회)
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> content = queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.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();
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 new PageImpl<>(content, pageable, total);
}
전체 카운트 조회에서 최적화가 필요할 경우(limit, offset 제거, left join의 경우에는 where 다중 파라미터에 teamName을 가져오기 때문에 필요) 하지만 해당 contQuery도 더 최적화가 가능하다. 이를테면
1.페이지의 시작이면서 컨텐츠가 페이지 사이즈보다 작아 추가적인 페이지가 필요하지 않을 경우
2.마지막 페이지이면서 해당 페이지의 컨텐츠 사이즈가 페이지 사이즈보다 작을 경우
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);
위의 두 경우는 countQuery가 발생하지 않게 메서드 참조 방식으로 함수를 작성해서 countQuery를 생략할 수 있다.
해당 기능을 제공하는 PageableExecutionUtils를 살펴보면,
public abstract class PageableExecutionUtils {
private PageableExecutionUtils() {}
public static <T> Page<T> getPage(List<T> content, Pageable pageable, LongSupplier totalSupplier) {
Assert.notNull(content, "Content must not be null");
Assert.notNull(pageable, "Pageable must not be null");
Assert.notNull(totalSupplier, "TotalSupplier must not be null");
if (pageable.isUnpaged() || pageable.getOffset() == 0) {
if (pageable.isUnpaged() || pageable.getPageSize() > content.size()) {
return new PageImpl<>(content, pageable, content.size());
}
return new PageImpl<>(content, pageable, totalSupplier.getAsLong());
}
if (content.size() != 0 && pageable.getPageSize() > content.size()) {
return new PageImpl<>(content, pageable, pageable.getOffset() + content.size());
}
return new PageImpl<>(content, pageable, totalSupplier.getAsLong());
}
}
위의 두 가지 경우에 대해서 조건식을 설정한 코드들을 살펴볼 수 있다.
해당 코드들을 작성하면서 이상하다고 느낀 점은 .fetchResuls() 와 .fetchCount()가 Deprecated 표시가 뜬다는 점이었다. 해당 부분에 대해서 의문을 가지고 있었는데 마지막 부분에 그 이유가 있었다.
Querydsl 5.0 부터는 .fetchResuls() 와 .fetchCount()가 향후 지원되지 않는다. 지금 당장은 아니겠지만 앞으로 Querydsl에서 해당 기능들을 사용하는 것은 자제해야지 싶다.
@Test
public void count() {
Long totalCount = queryFactory
// .select(Wildcard.count) //select count(*)
.select(member.count()) //select count(member.id)
.from(member)
.fetchOne();
System.out.println("totalCount = " + totalCount);
}
기존 카운트 쿼리에서 Wildcard.count, member.count(), count(member.id)로 바꾸고 fetchone()을 이용해 반환 받는다.
또, 위의 작성된 serachPageComplex 코드의 카운트 쿼리를 새롭게 바꾸면
JPAQuery<Long> countQuery = queryFactory
.select(member.count())
.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::fetchOne);
반환타입이 Member 에서 Long으로, fetchCount가 아닌 fetchOne으로 바뀌어 있는 모습을 확인할 수 있다.
'Spring > JPA' 카테고리의 다른 글
JPA 7 - 스프링 데이터 JPA (0) | 2023.07.10 |
---|---|
JPA 6 - 성능 최적화 (0) | 2023.07.08 |
JPA 5 - 패치 조인 (0) | 2023.07.03 |
JPA 4 - 값 타입, JPQL (1) | 2023.07.03 |
JPA 3 - 영속성 컨텍스트, 연관관계 매핑, 즉시로딩/지연로딩 (0) | 2023.07.01 |