JPQL을 자바코드로 작성하기 위해서는 Q타입(=Q타입 클래스)를 생성해야한다.
Q타입은 엔티티를 기반으로 생성된 쿼리 전용 클래스이다. Q타입은 쿼리를 자바코드로 작성할때 필요하다. 쿼리를 자바코드로 작성할때에는 Q타입 안에 미리 만들어져 있는 Q타입 객체를 사용한다.
즉, 엔티티를 기반으로 Q타입 클래스가 빌드 시점에 생성되고, 쿼리 작성 시에는 Q타입 클래스에 미리 정의된 Q타입 객체를 사용한다.
참고로 Q타입은 Gradle 빌드 과정 중 자바 컴파일 단계에서 생성된다.
그래서 터미널에서 ./gradlew build나 ./gradlew compileJava나 ./gradlew compileQuerydsl을 실행하면 Q타입이 생성된다.
Querydsl은 라이브러리이다.
Querydsl은 JPQL 빌더이다 = Querydsl은 JPQL을 문자열로 직접 작성하지 않고, 자바 코드로 JPQL을 조립(빌드)해주는것이다.
타입세이프하다. => JPQL 문자열에서 런타임에 터질 에러를 컴파일 시점에 잡아준다.
Querydsl에서 JPQL을 실행하려면 JPAQueryFactory가 있어야하고, EntityManager을 통해 JPAQueryFactory를 생성한다.
그리고 Querydsl은 fetchOne(), fetch(), 같은 fetch 계열 메서드가 호출되는 순간 JPQL을 실행한다.
import study.querydsl.entity.Team; // import
import static study.querydsl.entity.QMember.*; // static import
static import 사용전에는
Member findMember = queryFactory
.select(QMember.member)
.from(QMember.member)
.where(QMember.member.username.eq("member1"))
.fetchOne();
static import 사용후에는
import static study.querydsl.entity.QMember.*;
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
@BeforeEach
public void before() { // 각 @Test 실행전에 실행됨.
queryFactory = new JPAQueryFactory(em);
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
public void test() {
QueryResults<Member> results = queryFactory.selectFrom(member)
.offset(1)
.limit(2)
.fetchResults();
System.out.println("!!!!" + results.getTotal()); // 4 (전체 데이터 개수)
System.out.println("@@@@" + results.getLimit()); // 2
System.out.println("####" + results.getOffset()); // 1
List<Member> resultList = results.getResults();
for (Member member1 : resultList) {
System.out.println("member = " + member1);
}
} // 2번째(offset)부터 2개(limit)를 조회하는 쿼리
@Test
public void test1() {
Pageable pageable = PageRequest.of(0, 2); // (여기서 첫번째 파라미터는 페이지번호 두번째 파라미터는 페이지 별 사이즈)
QueryResults<Member> results = queryFactory.selectFrom(member)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
System.out.println("!!!!" + results.getTotal()); // 4 (전체 데이터 개수)
System.out.println("@@@@" + results.getLimit()); // 2 ( 페이지 별 사이즈)
System.out.println("####" + results.getOffset()); // 0 ( pageable.getOffset() 값은 페이지번호 x 페이지 별 사이즈이다 )
List<Member> resultList = results.getResults();
for (Member member1 : resultList) {
System.out.println("member = " + member1);
}
} // 첫번째페이지에 2개의 데이터를 조회하는 페이징 쿼리 ( offset이 0이므로, 첫번째부터 2개의 데이터를 조회하는 페이징 쿼리)
@Test
public void test2() {
Pageable pageable = PageRequest.of(1, 2); // (여기서 첫번째 파라미터는 페이지번호 두번째 파라미터는 페이지 별 사이즈)
QueryResults<Member> results = queryFactory.selectFrom(member)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
System.out.println("!!!!" + results.getTotal()); // 4 (전체 데이터 개수)
System.out.println("@@@@" + results.getLimit()); // 2 ( 페이지 별 사이즈)
System.out.println("####" + results.getOffset()); // 2 ( pageable.getOffset() 값은 페이지번호 x 페이지 별 사이즈이다 )
List<Member> resultList = results.getResults();
for (Member member1 : resultList) {
System.out.println("member = " + member1);
}
} // 두번째페이지에 2개의 데이터를 조회하는 페이징 쿼리 (offset이 2이므로, 세번째부터 2개의 데이터를 조회하는 페이징 쿼리)
inner join이든 left join이든
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.on(team.name.eq("teamA"))
.fetch();
select
member1
from
Member member1
inner join
member1.team as team
with
team.name = ?1
와 같은 jpql이 실행되고,
select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.id
and (
team1_.name=?
)
와 같이 SQL로 변환되서 실행된다.
List<Member> result = queryFactory
.selectFrom(member)
.leftJoin(member.team, team)
.on(team.name.eq("teamA"))
.fetch();
select
member1
from
Member member1
left join
member1.team as team
with
team.name = ?1
와 같은 jpql이 실행되고,
select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.id
and (
team1_.name=?
)
와 같이 SQL로 변환되서 실행된다.
List<Member> result = queryFactory
.selectFrom(member)
.join(team)
.on(team.name.eq("teamA"))
.fetch();
select
member1
from
Member member1
inner join
Team team
with
team.name = ?1
와 같은 jpql이 실행되고,
select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
inner join
team team1_
on (
team1_.name=?
)
와 같이 SQL로 변환되서 실행된다.
List<Member> result = queryFactory
.selectFrom(member)
.leftJoin(team)
.on(team.name.eq("teamA"))
.fetch();
select
member1
from
Member member1
left join
Team team
with
team.name = ?1
와 같은 jpql이 실행되고,
select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
left outer join
team team1_
on (
team1_.name=?
)
와 같이 SQL로 변환되서 실행된다.
참고로,
- EntityManagerFactory는 애플리케이션이 실행될때 애플리케이션 전역에서 단 하나만 생성되며, 실제 EntityManager를 생성하는 객체이고,
- EntityManager 프록시(싱글톤)는 애플리케이션이 실행될때 애플리케이션 전역에서 단 하나만 생성되며, 이때 생성되는 EntityManger 프록시는 실제 EntityManger가 아니다.
그래서 주입되는 스프링빈도 프록시객체이다.(@Autowired EntityManager em)
실제 EntityManger는 트랜잭션이 시작할때 트랜잭션 단위로 생성되며, 프록시가 실제 EntityManger에게 작업을 위임한다. 실제 EntityManger가 영속성 작업을 수행하는 객체이다.
즉, em.persist()를하면 프록시가 persist()하는것이지만 내부적으로 실제 EntityManager가 생성되서 이 실제 EntityManger에게 persist()를 위임해서 이 실제 EntityManger가 persist()를 하는것이다.- 트랜잭션 단위로 실제 EntityManager가 생성될때 이때 EntityManagerFactory에 의해 생성된다.
- @PersistenceContext에 의해 EntityManager를 자동으로 의존성주입 할 수 있고, @PersistenceUnit에 의해 EntityManagerFactory를 자동으로 의존성주입 할 수 있다.
그리고 둘다 @AutoWired로도 의존성 주입 할 수 있다.
@PersistenceUnit
EntityManagerFactory emf;
@Test
public void fetchJoin() {
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
.join(member.team)
.fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam()); // 해당 객체가 초기화 되었는지 확인
assertThat(loaded).isTrue();
}
select
member0_.member_id as member_i1_1_0_,
team1_.id as id1_2_1_,
member0_.age as age2_1_0_,
member0_.team_id as team_id4_1_0_,
member0_.username as username3_1_0_,
team1_.name as name2_2_1_
from
member member0_
inner join
team team1_
on
member0_.team_id=team1_.id
where
member0_.username=?
JPAExpressions를 활용해서 서브쿼리를 작성한다.
일반적인 SQL에서 서브쿼리 작성하는것 처럼, 쿼리에서 사용하고 있는 테이블의 alias와 겹치면 안되므로, 별도의 QMember를 만들어서 서브쿼리에서 활용한다.
public 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);
}
[JPQL]
select
member1
from
Member member1
where
member1.age = (
select
max(memberSub.age)
from
Member memberSub
)
[SQL]
select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
where
member0_.age=(
select
max(member1_.age)
from
member member1_
)
public 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);
}
[JPQL]
select
member1
from
Member member1
where
member1.age in (
select
memberSub.age
from
Member memberSub
where
memberSub.age > ?1
)
[SQL]
select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
where
member0_.age in (
select
member1_.age
from
member member1_
where
member1_.age>?
)
@Test
public void basicCase() {
List<String> result = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
@Test
public void complexCase() {
List<String> result = queryFactory
.select(new CaseBuilder()
.when(member.age.between(10, 20)).then("0~20살")
.when(member.age.between(21, 30)).then("21살~30살")
.otherwise("기타"))
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
.select(Projections.bean(MemberDto.class,
member.userName,
member.age
)
))
.from(member).select(Projections.fields(MemberDto.class,
member.userName,
member.age
)
))
.from(member).select(Projections.constructor(MemberDto.class,
member.userName,
member.age
)
))
.from(member)조회하려는 member의 "userName"과 MemberDto의 "userName"필드명은 동일해야한다. 다르다면 값이 들어오지않고, alias를 사용해서 동일하게 해야한다.
1.
.select(Projections.fields(MemberDto.class,
member.userName.as("memberName"),
member.age.as("memberAge")
)
))
.from(member)
2.
.select(Projections.fields(MemberDto.class,
member.userName.as("memberName"),
ExpressionUtils.as(
member.age.add(1),
"memberAge"
)
))
.from(member)
3.
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.userName.as("memberName"),
ExpressionUtils.as(
JPAExpressions
.select(subMember.age.max())
.from(subMember),
"memberAge"
)
))
.from(member)
.fetch();
참고로,
DTO 클래스의 생성자에 @QueryProjection을 달아주고 빌드나 ./gradlew compileQuerydsl을 하면 Q파일이 생긴다.List<MemberDto> result = queryFactory .select(new QMemberDto(member.userName, member.age)) .from(member) .fetch();그럼 이렇게 가능하다.
참고로,
DTO로 조회할때는 페치조인처럼 연관된 엔티티나 컬렉션을 SQL한번에 조회할 수 있다. 지연로딩 즉시로딩 상관없이 무조건 한번의 쿼리로 연관된 엔티티나 컬렉션을 조회할 수 있다.queryFactory .select(new QMemberTeamDto( member.id, member.username, team.id, team.name )) .from(member) .leftJoin(member.team, team) .fetch();
@Test
public void 동적쿼리_BooleanBuilder() throws Exception {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember1(usernameParam, ageParam);
Assertions.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
.selectFrom(member)
.where(builder)
.fetch();
}
builder.and(
member.age.goe(20)
.and(member.age.loe(30))
);
builder.and(
member.username.eq("kim")
.or(member.username.eq("lee"))
);
@Test
public void 동적쿼리_WhereParam() throws Exception {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember2(usernameParam, ageParam);
Assertions.assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(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(usernameEq(username), ageEq(age))와
.where(usernameEq(username).and(ageEq(age)))는 동일하다.
그래서 WHERE member.username = ? AND member.age = ? 실행되는 쿼리가 동일하다.
하지만, 컴마로 했을때에는 a 또는 b가 null 이면 자동 무시하고 NPE가 없음.
and로 했을때에는 usernameEq(username)가 null이면 null.and(...) 가 되어 NPE가 발생함.
REPLACE(원본문자열, 찾을문자열, 바꿀문자열)
List<String> result = queryFactory
.select(Expressions.stringTemplate("function('replace', {0}, {1}, {2})", member.username, "member", "M"))
.from(member)
.fetch();
LOWER(문자열)
List<String> result = queryFactory
.select(member.username)
.from(member)
.where(member.username.eq(Expressions.stringTemplate("function('lower', {0})", member.username)))
//.where(member.username.eq(member.username.lower())) // 이렇게 해도 동일하다.
.fetch();
- 참고로, Spring Data JPA는
public interface MemberRepository extends JpaRepository<Member, Long> { Page<Member> findByAge(int age, Pageable pageable); }개발자는 메서드 시그니처만 정의하고 구현은 따로 하지않는다.
Spring Data JPA는 Repository 인터페이스에 메서드 시그니처만 정의하면, 메서드가 호출될 때 내부적으로 필요한 작업을 수행하고, 선언된 반환 타입에 맞게 값을 리턴해준다.
- 참고로, Spring Data JPA는
Page<T>처럼 반환 타입이 인터페이스인 경우에는, 내부적으로 return new PageImpl<>(content, pageable, total); 처럼 Page인터페이스의 구현체인 PageImpl을 리턴한다.
- content : 현재 페이지 데이터
- pageable : 페이지 정보
- total : 전체 데이터 개수
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
}
public class MemberRepositoryImpl implements MemberRepositoryCustom{
.
.
.
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
QueryResults<MemberTeamDto> results = 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();
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
}
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();
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);
}
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();
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);
return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
}
case1. 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈(한 페이지에 보여줄 데이터 개수)보다 작을때(=전체 데이터가 첫 페이지안에 모두 있을때)
첫 페이지부터, 현재 페이지의 content.size() < pageable.getPageSize()인 경우에는 첫 페이지이자 마지막 페이지임을 알 수 있고, 전체 데이터의 개수를 알 수 있기 때문에, count쿼리가 필요없다.
=====> 첫 페이지이면서 마지막페이지에서는 전체 데이터의 개수(content.size())를 알 수 있기 때문에, count쿼리가 필요없다.
case2. 마지막 페이지 일때(마지막 페이지의 offset + 마지막 페이지의 컨텐츠 사이즈를 더해서 전체 사이즈 구함)
첫,중간 페이지들: 현재 페이지의 content.size() == pageable.getPageSize()인 경우에는 마지막 페이지인지 아닌지 모르기 때문에, 전체 데이터 개수와 다음 페이지 존재 여부를 알기 위해 count 쿼리 필요(현재 페이지의 offset + pageSize < total이면 다음 페이지 존재함.)
마지막 페이지: 현재 페이지의 content.size() < pageable.getPageSize()인 경우에는 마지막 페이지임을 알 수 있고, 현재 페이지의 pageable.getOffset + content.size()로 전체 데이터 개수 계산 가능하기 때문에 count 쿼리 불필요
=====> 마지막페이지에서는 pageable.getOffset() + content.size()로 전체 데이터의 개수를 알 수 있기 때문에, count쿼리가 필요없다.
@GetMapping("/v2/members")
public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
return memberRepository.searchPageSimple(condition, pageable);
}
여기서 Pageable의 파라미터의 값을 줄때,
http://localhost:8080/v2/members?page=0&size=5 처럼 주면 된다.
요청 파라미터에 page / size / sort가 있으면 PageRequest객체로 만들어서 파라미터에 넣어준다.
page=0&size=5 처럼 주면 PageRequest.of(0,5)와 같은 객체가 생성되서 파라미터에 들어간다.
참고로 sort까지 주면 ?page=0&size=5&sort=username,desc 처럼 하면 되고 PageRequest.of(0, 5, Sort.by("username").descending())와 같은 객체가 생성되서 파라미터에 들어간다.
Pageable의 파라미터로 아예 값을 /v2/members 처럼 page나 size를 안주면, 기본값은 page는 0이고 size는 20이다. 즉, PageRequest.of(0, 20).