Spring Boot/QueryDSL
Spring Boot QueryDSL 기본 문법
최-코드
2024. 8. 13. 10:26
...
//웬만하면 이 방식으로 QClass 만들자.
import static com.rosoa0475.querydsl.entity.QMember.member;
...
@Autowired
EntityManager em;
JPAQueryFactory queryFactory;
@BeforEach
public void befor(){
queryFactory = new JPAQueryFactory(em);
...
}
public void startQuerydsl(){
// QMember m1 = new QMember("m1") 같은 테이블이 서로 조인하려고 할 때는 이 방식으로 다른 값을 넣어야 한다. 테이블에 대한 별칭이 되기 때문이다.
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1")) // 파라미터 바인딩 처리까지 해준다.(?1와 같은 것에 대해)
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
JPAQueryFactory에 EntityManager를 넣어서 JPA와 같이 queryFactory가 내부적으로 em을 통해 CRUD를 진행한다.
검색 조건 함수
사용 예제
@Test
public void search(){
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"),
/*.and(member.age.eq(10))*/
member.age.eq(10))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
- and의 경우 .and 대신에 ,을 통해서도 이루어질 수 있다.
- ,로 and 처리할 시에 만약 null 값이 있을 경우 해당 null 부분은 무시해버린다. 즉, 쿼리에 아예 포함이 안 된다.
결과 조회
@Test
public void resultFetch(){
//리스트로 조회, 데이터 없으면 빈 리스트 반환한다.
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
//단 건 조회, 결과가 없으면 null이고, 둘 이상이면 NonUniqueResultException 발생한다.
Member fetchOne = queryFactory
.selectFrom(member)
.fetchOne();
Member fetchFirst = queryFactory
.selectFrom(member)
.fetchFirst();// limit(1).fetchOne()과 같다.
}
정렬
/*
1. 회원 나이 내림차순
2. 회원 이름 올림차순
단 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();
Member member5 = result.get(0);
Member member6 = result.get(1);
Member memberNull = result.get(2);
assertThat(member5.getUsername()).isEqualTo("member5");
assertThat(member6.getUsername()).isEqualTo("member6");
assertThat(memberNull.getUsername()).isNull();
}
.nullsLast()와 .nullsFirst()를 통해 null 데이터의 순서를 부여할 수 있다. .nullsLast()를 지정할 시에 null값은 정렬에서 맨 뒷 순서다.
페이징
@Test
public void paging(){
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) // 0번, 1번 데이터 스킵
.limit(2) // 2개 데이터 선택 시 바로 반환
.fetch();
assertThat(result.size()).isEqualTo(2);
}
집합
@Test
public void aggregation(){
//QueryDSL이 제공하는 Tuple 타입 - 여러 개의 타입을 저장할 수 있다.
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.sum())).isEqualTo(100);
assertThat(tuple.get(member.age.avg())).isEqualTo(25);
assertThat(tuple.get(member.age.max())).isEqualTo(40);
assertThat(tuple.get(member.age.min())).isEqualTo(10);
}
/**
* 팀의 이름과 각 팀의 평균 연령을 구해라.
*/
@Test
public void group() throws Exception {
List<Tuple> result = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.fetch();
Tuple teamA = result.get(0);
Tuple teamB = result.get(1);
assertThat(teamA.get(team.name)).isEqualTo("teamA");
assertThat(teamA.get(member.age.avg())).isEqualTo(15);
assertThat(teamB.get(team.name)).isEqualTo("teamB");
assertThat(teamB.get(member.age.avg())).isEqualTo(35);
}
having 절을 통해 그룹화된 결과를 제한할 수 있다.
조인
- .join()에서 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에는 별칭으로 사용할 Q타입을 지정하면 된다.
- .join(member.team, team)와 같이 하면 jpql은 member.team as team와 같이 설정 된다.
/**
* 팀 A에 소속된 모든 회원
*/
@Test
public void join() throws Exception {
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
//.leftJoin(member.team, team) = .join()
//.rightJoin(member.team, team)
//.innerJoin(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("member1", "member2");
}
/**
* 세타 조인, 카테시안 곱
* 회원의 이름이 팀 이름과 같은 회원 조회
*/
@Test
public void theta_join() throws Exception {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
em.persist(new Member("teamC"));
List<Member> result = queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("teamA", "teamB");
}
- on절 활용
- 조인 대상 필터링
- 외부 조인에 대해서만 on절에 썻을 때와 where절에서 썻을 때의 차이점이 존재한다.
- 해당 조건에서 만족하는 거만 조인하므로 조건을 만족하지 않은 것에 대해 null로 배치된다.
- 아래의 경우 teamB는 null값으로 나온다.
- 조인 대상 필터링
/**
* 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
* JPQL : select m, t from Member m left join m.team t on 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);
}
}
- on절 활용
- 연관관계가 없는 엔티티 외부조인
- 일반적으로 jpa에서 join할 때 obj1.obj2와 같이 한다. 이를 통해 sql에서 on절에 pk와 fk의 값을 비교하는 구문이 들어간다.
- 하지만 그냥 obj2만 사용하면 pk와 fk를 비교하지 않고 지정한 on절에 대해서만 조인을 진행한다.
- 연관관계가 없는 엔티티 외부조인
/**
* 연관관계가 없는 엔티티 외부조인
* 회원의 이름이 팀 이름과 같은 대상 외부 조인
*/
@Test
public void join_on_no_relation() throws Exception {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
em.persist(new Member("teamC"));
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name))
.where(member.username.eq(team.name))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
- 페치조인
@Test
public void fetchJoinUse() throws Exception {
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
//fetch join 시에는 별칭 X
.join(member.team).fetchJoin()
.where(member.username.eq("member1"))
//oneToMnay 조인시 jpql과 같이 distinct() 추가하기
.fetchOne();
}
서브쿼리
- 서브쿼리에 com.querydsl.jpa.JPAExpressions을 사용하면 된다. QClass와 마찬가자로 스태틱 임포트 가능하다.
- 같은 테이블에 대해서는 다른 별칭을 줘야 하므로 QClass를 다른 이름으로 만들어야 한다.
- select 절과 where절에서만 서브쿼리가 가능하다. from절에서는 서브쿼리가 안 된다.
- from절의 서브쿼리 해결방안, 위에서부터 1순위이다.
- 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
- 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
- nativeSQL을 사용한다
- from절의 서브쿼리 해결방안, 위에서부터 1순위이다.
...
//서브쿼리 스태틱 임포트
import static com.querydsl.jpa.JPAExpressions.select;
...
/**
* 나이가 가장 많은 회원 조회
*/
@Test
public void subQuery() throws Exception {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
select(memberSub.age.max())
.from(memberSub)
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(40);
}
/**
* 나이가 평균 이상인 회원 조회
*/
@Test
public void subQueryGoe() throws Exception {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(40);
}
/**
*
*/
@Test
public void subQueryIn() throws Exception {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.in(
select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(20, 30, 40);
}
@Test
public void selectSubQuery() throws Exception {
QMember memberSub = new QMember("memberSub");
List<Tuple> result = queryFactory
.select(member.username,
select(memberSub.age.avg())
.from(memberSub))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
case문
- 웬만하면 디비에 있는 로우데이터를 필터링하고 그룹화하는, 데이터를 줄이는 일만 하고 데이터를 변경하는 일은 지양해야한다. 따라서 애플리케이션에서 로직을 처리하는 것을 지향하자. 예외적으로 효율이 좋아지는 경우도 있긴 하다.
- select, where, orber by에서 사용 가능하다.
@Test
public void basicCase() throws Exception {
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(0, 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);
}
}
- querydsl은 자바코드로 작성하기 때문에 복잡한 조건을 변수로 선언해서 각 절에서 재사용할 수 있다.
@Test
public void orberByCase(){
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); }
}
상수 & 문자 더하기
- 상수의 경우 com.querydsl.core.types.dsl.Expressions의 constant() 메소드를 이용하면 된다.
@Test
public void constant(){
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = "+tuple);
}
}
- 문자 더하기의 경우 concat() 메소드를 이용하면 된다.
- 이 때 concat()은 string값만 인자로 받기 때문에 string값이 아닌 컬럼의 경우 stringValue()메소드를 이용해야 한다. 이 때 sql에서는 cast 함수가 실행된다.
@Test
public void concat() {
//{username}_{age}
List<String> result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}