장점
@Test
public void startJPQL() {
//member1을 찾아라.
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
public void startQuerydsl() {
//member1을 찾아라.
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");
}
Tip. JPAQueryFactory를 필드로 제공하면 동시성 문제는 어떻게 될까? 동시성 문제는 JPAQueryFactory를 생성할 때 제공하는 EntityManager(em)에 달려있다. 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다.
QMember qMember = new QMember("m"); //별칭 직접 지정
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%’ 검색
...
참고: 실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만,
count 쿼리는 조인이 필요 없는 경우도 있다. 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두
조인을 해버리기 때문에 성능이 안나올 수 있다. count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면,
count 전용 쿼리를 별도로 작성해야 한다.
List<Tuple> result = queryFactory
.select(member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min())
.from(member)
.fetch();
// 아이템의 가격으로 그룹을 짓고 1000원이 넘는 것만 뽑아라
.
.
.groupBy(item.price)
.having(item.price.gt(1000))
.
.
// 예시
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
( https://nathanh.tistory.com/134 조인에 대하여 다시 복습하려면 여기 블로그 참고 )
// 예시 (카디션 프로덕트로 나타남)
List<Member> result = queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
* JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'teamA'
* SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and
t.name='teamA'
*/
@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);
}
}
========= 결과 출력 ===========
// leftJoin() 으로 사용 했을 떄
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]
// join() 으로 사용 했을 때
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)]
Tip. 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다.
따라서, on 절을 활용한 조인 대상 필터링을 사용할 때 내부조인 이면 익숙한 where 절로 해결하고
정말 외부조인이 필요한 경우에만 이 기능을 사용하자.
일반조인: leftJoin(member.team, team)
on조인: from(member).leftJoin(team).on(xxx)
/**
* 2. 연관관계 없는 엔티티 외부 조인
* 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
* JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
* SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
*/
@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)]
.join(member.team, team).fetchJoin()
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
import static com.querydsl.jpa.JPAExpressions.select;
JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl
도 지원하지 않는다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도
하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.
(DB는 간단하게 데이터를 저장하고 퍼오는 것에만 집중하자 복잡하게 막 만들지마라)
// 예를 들어서 다음과 같은 임의의 순서로 회원을 출력하고 싶다면?
// 1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력
// 2. 0 ~ 20살 회원 출력
// 3. 21 ~ 30살 회원 출력
NumberExpression<Integer> rankPath = new CaseBuilder()
.when(member.age.between(0, 20)).then(2)
.when(member.age.between(21, 30)).then(1)
.otherwise(3);
List<Tuple> result = queryFactory
.select(member.username, member.age, rankPath)
.from(member)
.orderBy(rankPath.desc())
.fetch();
for (Tuple tuple : result) {
String username = tuple.get(member.username);
Integer age = tuple.get(member.age);
Integer rank = tuple.get(rankPath);
System.out.println("username = " + username + " age = " + age + " rank = "
+ rank);
}
=======결과========
결과
username = member4 age = 40 rank = 3
username = member1 age = 10 rank = 2
username = member2 age = 20 rank = 2
username = member3 age = 30 rank = 1
Tuple result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetchFirst();
for(Tuple tuple : result){
sout("tupe = " + tuple)
}
=========결과 ========
tuple = [member1, A]
tuple = [member2, A]
tuple = [member3, A]
tuple = [member4, A]
위와 같이 constant가 없어도 최적화가 가능한 SQL에는 constant 값을 넘기지 않는다.
상수를 더해야하는 어려운 상황에서의 최적화가 필요할때만 constant 값을 넘겨준다.
// {username}_{age}
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
===== 결과 ====
member1_10
: member.age.stringValue() 부분이 중요한데, 문자가 아닌 다른 타입들은 stringValue()를 통해 문자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때도 자주 사용한다.
List<Tuple> result = queryFactory
// MemberDTO
@Data
public class MemberDto {
private String username;
private int age;
public MemberDto() {
}
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
// JPA에서 DTO 조회 코드
List<MemberDto> result = em.createQuery(
"select new study.querydsl.dto.MemberDto(m.username, m.age) " +
"from Member m", MemberDto.class)
.getResultList();
1) 프로퍼티 접근
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
2) 필드 직접 접근
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
3) 생성자 사용
List<MemberDto> result = queryFactory
.select(Projections.contructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
@Data
public class UserDto {
private String name;
private int age;
}
// 1) username.as("memberName") : 필드에 별칭 적용
// 2) ExpressionUtils.as(source,alias) : 필드나, 서브 쿼리에 별칭 적용 (복잡한 방법)
List<UserDto> fetch = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
ExpressionUtils.as(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")
)
).from(member)
.fetch();
@Data
public class MemberDto {
private String username;
private int age;
public MemberDto() {
}
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.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();
}
조합 예제
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
@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))
.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;
}
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
-----
기존 숫자에 곱하기: multiply(x)
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
//member M으로 변경하는 replace 함수 사용
String result = queryFactory
.select(Expressions.stringTemplate("function('replace', {0}, {1},
{2})", member.username, "member", "M"))
.from(member)
.fetchFirst();
//소문자로 변경해서 비교해라.
select(member.username)
.from(member)
.where(member.username.eq(Expressions.stringTemplate("function('lower', {0})",
member.username)))
.where(member.username.eq(member.username.lower()))