build.gradle에 별도의 설정이 필요하다.
plugins {
id 'org.springframework.boot' version '2.5.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
//querydsl 추가
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
id 'java'
}
group = 'study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
//querydsl 추가
implementation 'com.querydsl:querydsl-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
//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 추가 끝
위와 같이 설정하면
gradle에 있는 Tasks > other에 compileQuerydsl이 추가가 되는데 이를 더블클릭 한다.
위의 compileQuerydsl을 실행하면 아래와 같이 build.gradle에 설정한 경로에
main에 있는 Hello와 비슷한 경로로 build>generated 하위에 QHello가 생성된다.
※ 참 고
git에 프로젝트 커밋시 빌드된 Q파일은 올리면 안된다. 라이브러리 버전에 따라 달라지기 때문
따라서, 커밋ignore로 무시하고 해야하나 위의 경로는 build 하위로 설정했기 때문에 무시해도 된다.
//Hello.java
@Entity
@Getter @Setter
public class Hello {
@Id @GeneratedValue
private Long id;
}
//QHello.java
/**
* QHello is a Querydsl query type for Hello
*/
@Generated("com.querydsl.codegen.EntitySerializer")
public class QHello extends EntityPathBase<Hello> {
private static final long serialVersionUID = 1910216155L;
public static final QHello hello = new QHello("hello");
public final NumberPath<Long> id = createNumber("id", Long.class);
public QHello(String variable) {
super(Hello.class, forVariable(variable));
}
public QHello(Path<? extends Hello> path) {
super(path.getType(), path.getMetadata());
}
public QHello(PathMetadata metadata) {
super(Hello.class, metadata);
}
}
Querydsl로 생성된 QHello 테스트 코드
@SpringBootTest
@Transactional
class QuerydslApplicationTests {
// @PersistenceContext
@Autowired
EntityManager em;
@Test
void contextLoads() {
Hello hello = new Hello();
em.persist(hello);
JPAQueryFactory query = new JPAQueryFactory(em);
//QHello qHello = new QHello("h");
QHello qHello = QHello.hello;
Hello result = query
.selectFrom(qHello)
.fetchOne();
//같은 트랜잭션 안에서 em.persist와 조회가 이루어 지므로 동일한 값이 나온다.
//동일성 보장
assertThat(result).isEqualTo(hello);
assertThat(result.getId()).isEqualTo(hello.getId());
}
}
사용 코드
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@Autowired
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void before() {
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 startJQPL() {
//member1을 찾아라
String sqlString =
"select m from Member m " +
"where m.username = :username";
Member findMember = em.createQuery(sqlString, Member.class)
.setParameter("username", "member1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
@Test
public void startQuerydsl() {
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
//QMember m = new QMember("m"); //별칭 직접 입력
QMember m = QMember.member; //위의 주석과 동일 QMember안에 정의되어 있다.(기본 인스턴스 사용)
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1")) //파라미터 바인딩 처리
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
}
자동으로 PreparedStatement로 파라미터 바인딩 (SQL 인젝션 문제 없다.)
큰 이점은 JPQL을 이용하면 쿼리문을 문자열 방식으로 작성하기 때문에 오타시 런타임 시점에 오류가 발생하고,
querydsl은 자바 코드로 사용되어 컴파일 시점이 오류를 캐치할 수 있다.
- EntityManager로 JPAQueryFactory 생성
- Querydsl은 JPQL빌더
- JPQL : 문자(실행 시점 오류), Querydsl : 코드(컴파일 시점 오류)
- JPQL : 파라미터 바인딩 직접, Querydsl : 파라미터 바인딩 자동 처리
되도록이면 Querydsl에서 Q타입 선언할 때, 기본 인스턴스를 static import 사용해서 코드 간결하게 할 것.
사용 코드
//QMember m = new QMember("m"); //별칭 직접 지정
QMember m = QMember.member; //위의 주석과 동일 QMember안에 정의되어 있다. (기본 인스턴스 사용)
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1")) //파라미터 바인딩 처리
.fetchOne();
위의 부분을 아래와 같이 간결하게 변경 가능
import study.querydsl.entity.QMember;
Member findMember = queryFactory
.select(member) //QMember.member에서 QMember을 import해서 간결하게 변경
.from(member)
.where(member.username.eq("member1")) //파라미터 바인딩 처리
.fetchOne();
@Test
public void searchAdnParam() {
Member findMember = queryFactory
.selectFrom(member) //select와 from 합친것
.where(
member.username.eq("member1"), // ,로 구분하면 and로 조립됨
member.age.eq(10)
)
.fetchOne();
//where 절을 아래와 같이도 가능
/* .where(
member.username.eq("member1")
.and(member.age.eq(10))
) */
assertThat(findMember.getUsername()).isEqualTo("member1");
}
select와 from을 selectFrom으로 사용할 수 있다.
where절 안의 조건들이 and일 경우 , 로 조립할 수 있다.(선호)
이경우 null 값은 무시된다.
JPLQ이 제공하는 모든 검색 조건을 제공한다.
- fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
- fetchOne() : 단 건 조회
- fetchFirst() : limit(1).fetchOne()와 동일 (1개의 값만 조회)
- fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
- fetchCount() : count 쿼리로 변경해서 count 수 조회
사용 코드
@Test
public void resultFetch() {
//List
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
//단 건
Member member1 = queryFactory
.selectFrom(member)
.fetchOne();
//처음 한 건 조회
Member member2 = queryFactory
.selectFrom(member)
.fetchFirst();
//페이징에서 사용(조회와 카운트 쿼리 2개 동작)
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults();
//count 쿼리
long count = queryFactory
.selectFrom(member)
.fetchCount();
}
- desc(), asc() : 일반 정렬
- nullsLast(), nullsFirst() : null데이터 순서 부여
사용 코드
/**
* 회원 정렬 순서
* 1. 회원 나이 내림차순(desc)
* 2. 회원 이름 오름차순(asc)
* 단, 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();
}
사용 코드
@Test
public void paging() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) //0부터 시작
.limit(2) //최대 2건 조회
.fetch();
assertThat(result.size()).isEqualTo(2);
for (Member member : result) {
System.out.println("member = " + member.getUsername()); //member3,member2 출력
}
//전체 조회 수 필요시 fetchResults 이용(카운트 쿼리도 나가기 때문)
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);
}
주의: count 쿼리가 실행되어 성능상 불이익이 있을 수 있다.
count쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 만들것.
사용 코드
/**
* JPQL
* select
* COUNT(m), //회원수
* SUM(m.age), //나이 합
* AVG(m.age), //평균 나이
* MAX(m.age), //최대 나이
* MIN(m.age) //최소 나이
* from Member m
*/
@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);
}
JPQL이 제공하는 모든 집합 함수를 제공한다.
Tuple는 querydsl이 여러개 조회할 수 있게 만들어 놓은 타입
Group by 사용 코드
/**
* 팀의 이름과 각 팀의 평균 연령을 구해라.
*/
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
@Test
public void group() {
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);
//member과 team은 각각 QMember.member, QTeam.team 이다.
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);
}
//실행되는 쿼리문
select
team1_.name as col_0_0_,
avg(cast(member0_.age as double)) as col_1_0_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.team_id
group by
team1_.name
사용 코드
/**
* 팀 A에 소속된 모든 회원
*/
@Test
public void join() {
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team) //join(조인 대상, 사용할 Q타입)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result)
.extracting("username") //username 필드 추출
.containsExactly("member1", "member2");
}
- join(), innerJoin() : 내부 조인(inner join)
- leftJoin() : left 외부 조인(left outer join)
- rightJoin() : right 외부 조인(right outer join)
사용 코드
/**
* 조인 - on절 (조인 대상 필터링)
* 회원과 팀을 조인하면서, 팀 이름이 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 OUTER JOIN Team t On m.TEAM_ID = t.id and t.name='teamA'
*/
@Test
public void join_on_filtering() {
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);
}
}
//결과
tuple = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
tuple = [Member(id=5, username=member3, age=30), null]
tuple = [Member(id=6, username=member4, age=40), null]
※ 참 고
on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where절에서 필터링 하는 것과 기능이 동일. 따라서 on절을 활용한 조인 대상 필터링 사용할 때, 내부조인이면 where절로 해결하고, 정말 외부조인이 필요한 경우에만 on절을 사용.
사용 코드
/**
* 조인 - on절 (연관관계 없는 엔티티 외부 조인)
* 회원의 이름과 팀의 이름이 같은 대상 외부 조인
*/
@Test
public void join_on_no_relation() {
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("tuple = " + tuple);
}
}
//결과
tuple = [Member(id=3, username=member1, age=10), null]
tuple = [Member(id=4, username=member2, age=20), null]
tuple = [Member(id=5, username=member3, age=30), null]
tuple = [Member(id=6, username=member4, age=40), null]
tuple = [Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)]
tuple = [Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]
※주의
일반 조인과 문법이 다르다. leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
-일반 조인 : leftJoin(member.team, team)
- on 조인 : from(member) leftJoin(team).on(xxx)
SQL조인을 활용해서 연관된 엔티티를 SQL한번에 조회하는 기능. 주로 성능 최적화에 사용
사용 코드
//조인 - 페치 조인
@PersistenceUnit
EntityManagerFactory emf;
@Test
public void fetchJoinUse() {
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();
}
// 참 고
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"))
.fetchOne();
위와 같은 쿼리를 실행했을 때, member엔티티 안에있는 team은 쿼리문이 날라가지 않고
findMember을 통해 team을 조회할때 team에 대한 조회 쿼리가 날라간다(지연로딩).
하지만, fetchJoin()을 사용하면 처음부터 조인되는 엔티티들을 모두 조회한다.
사용방법
join(), leftJoin()등 조인 기능 뒤에 .fetchJoin()이라고 추가만 하면 된다.
사용 코드
@Test
public void subQuery() {
//기존의 QMember.member과 다른 별칭위해 생성
QMember memberSub = new QMember("memberSub");
//나이가 가장 많은 회원 조회
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)
))
.fetch();
//나이가 평균 나이 이상인 회원 조회
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe( //goe: 크거나 같다 >=
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
//서브쿼리 여러 건 처리, in절 사용
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.in(
JPAExpressions
.select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
//SELECT 절에 SubQuery
List<Tuple> result = queryFactory
.select(member.username,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
).from(member)
.fetch();
}
//추가
JPAExpressions를 static import로 사용 할 수 있다.
import static com.querydsl.jpa.JPAExpressions.*;
위와 같이 선언하여 코드상 JPAExpressions를 작성하지 않아도 서브쿼리 사용 가능
//in절 사용 서브쿼리 실행한 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>?
)
※ 참 고
JPA JPQL서브쿼리의 한계점으론 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.
따라서 Querydsl도 from 절의 서브쿼리를 지원하지 않는다.
from 절의 서브쿼리 해결방안으로는
1. 서브쿼리를 join으로 변경한다.(가능한 상황도 있고, 불가능한 상황도 있다.)
2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
3. nativeSQL을 사용한다.
select, 조건절(where), order by에서 사용 가능.
※ 참 고
되도록이면 sql문에서 조건을 처리하는 것 보다는 db에서는 데이터만 가져오고
애플리케이션 딴에서 조건을 처리하는게 좋다.
사용 코드
@Test
public void case() {
//단순한 조건
List<String> result = queryFactory
.select(member.age
.when(10).then("10살")
.when(20).then("20살")
.otherwise("기타"))
.from(member)
.fetch();
//복잡한 조건(new 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();
}
//복잡한 조건 쿼리 실행 sql문
select
case
when member0_.age between ? and ? then ?
when member0_.age between ? and ? then ?
else '기타'
end as col_0_0_
from
member member0_
orderBy에서 Case문 함께 사용
사용 코드
/**
* orderBy에서 Case문 함께 사용
* 0~30살이 아닌 회원을 가장 먼저 출력
* 0~20살 회원 출력
* 21~30살 회원 출력
*/
@Test
public void orderByCase() {
//순서 정의
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();
}
//쿼리 실행 sql문
select
member0_.username as col_0_0_,
member0_.age as col_1_0_,
case
when member0_.age between ? and ? then ?
when member0_.age between ? and ? then ?
else 3
end as col_2_0_
from
member member0_
order by
case
when member0_.age between ? and ? then ?
when member0_.age between ? and ? then ?
else 3
end desc
사용 코드
//상수
@Test
public void constant() {
Tuple result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetchFirst();
}
//문자 더하기
@Test
public void concat() {
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
}
문자가 아닌 다른 타입들은 stringValue()를 이용해 문자로 변환할 수 있다.
이 방법은 ENUM을 처리할 때도 자주 사용
//문자 더하기 쿼리 실행 sql문
select
((member0_.username||?)||cast(member0_.age as char)) as col_0_0_
from
member member0_
where
member0_.username=?
member.age.stringValue()의 stringValue()는 문자가 아닌 다른 타입을 문자로 변환하는 것.
(ENUM처리시에 자주 사용)
프로젝션: SELECT절의 대상이 되는 것.
ex) SELECT NAME, AGE FROM MEMBER 일 경우 프로젝션은 NAME, AGE
예제를 위한 MemberDto.java와 UserDto.java
//MemberDto.java
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
//UserDto.java
@Data
@NoArgsConstructor
public class UserDto {
private String name;
private int age;
}
사용 코드
@Test
public void findDtoByJPQL() {
List<MemberDto> result = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age)" +
"from Member m", MemberDto.class)
.getResultList();
}
순수 JPA에서 DTO직접 조회의 경우 new 오퍼레이션을 통해 패키지명을
직접 입력해야하는 번거로움이 있다.
Querydsl에서 결과를 DTO로 반환할 때 3가지의 방법을 지원한다.
- 프로퍼티 접근(setter)
- 필드 직접 접근
- 생성자 사용
사용 코드
@Test
public void findDto() {
//1. 프로퍼티 접근 - Setter Projections.bean
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
//2. 필드 접근 Projections.fields
// 2.1. Member엔티티의 필드명과 MemberDto의 필드명이 같을때
List<MemberDto> result1 = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
// 2.2. Member엔티티의 필드명과 별칭이 다를 때와 서브쿼리 사용 별칭
//프로퍼티나, 필드 접근 생성 방식에서 이름이 다를 때 해결 방안
//ExpressionUtils.as(source, alias) : 필드나, 서브 쿼리에 별칭 적용
//username.as("memberName"): 필드에 별칭 적용
QMember memberSub = new QMember("memberSub");
List<UserDto> result2 = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
ExpressionUtils.as(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")
)
).from(member)
.fetch();
//3. 생성자 접근 Projections.constructor
//생성자 사용의 경우 타입을 맞춰줘야 한다.
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
}
반환할 Dto의 생성자에 @QueryProjection을 추가하고
gradle에서 compileQuerydsl을 빌드해서 Q파일을 별도로 생성한다.
사용 코드
//MemberDto.java
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
//@QueryProjection 추가 후 compileQuerydsl을 통해 Q파일 생성
//테스트 코드
@Test
public void findDtoQP() {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
}
이 방법은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법.
다만 DTO에 QueryDSL 어노테이션을 유지해야 하는 점과 DTO까지 Q파일을 생성해야 하는 단점이 있다.
동적 쿼리를 해결하는 두가지 방식
- BooleanBuilder
- Where 다중 파라미터 사용(선호하는 방식)
BooleanBuilder 사용 코드
@Test
public void 동적쿼리_BooleanBuilder() {
String usernameParam = "member1";
Integer ageParam = null;
List<Member> result = searchMember1(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember1(String usernameParam, Integer ageParam) {
BooleanBuilder builder = new BooleanBuilder();
//builder에 들어갈 파라미터 값이 null이면 해당 조건은 where절에 포함되지 않는다.
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();
}
Where 다중 파라미터 사용 코드
@Test
public void 동적쿼리_whereParam() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember2(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember2(String usernameParam, Integer ageParam) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameParam), ageEq(ageParam))
.fetch();
}
private BooleanExpression usernameEq(String usernameParam) {
return usernameParam != null ? member.username.eq(usernameParam) : null;
}
private BooleanExpression ageEq(Integer ageParam) {
return ageParam != null ? member.age.eq(ageParam) : null;
}
위 방식의 장점
- where 조건에 null 값은 무시된다.(BooleanBuilder 방식도 마찬가지)
- 메서드를 다른 쿼리에서도 재활용 할 수 있다.
- 쿼리 자체의 가독성이 높아진다.
JPA의 변경 감지는 영속성 컨텍스트에 있는 엔티티에 변화가 있을때 트랜잭션 커밋 시점에 수정을 해주는 것으로 하나하나 수정되는 번거로움이 있지만, 벌크 연산은 한번에 여러 데이터들을 수정할 수 있다.
사용 코드
@Test
public void bulkUpdate() {
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
//벌크 연산 후 다시 조회할땐 영속성 컨텍스트 초기화 작업 필히 수행!
em.flush();
em.clear();
//영속성 컨텍스트를 clear해주지 않으면 db에서 가져오는 것이아니라
//영속성 컨텍스트에 남아있던 member1, member2의 데이터를 가져온다.
List<Member> result = queryFactory
.selectFrom(member)
.fetch();
for (Member member : result) {
System.out.println("member = " + member.getUsername());
}
}
@Test
public void bulkAdd() {
long count = queryFactory
.update(member)
.set(member.age, member.age.add(1))
.execute();
}
@Test
public void bulkDelete() {
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
}
SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있다.
사용 코드
@Test
public void sqlFunction1() {
//member -> M으로 변경하는 replace 함수 사용
List<String> result = queryFactory
.select(Expressions.stringTemplate("function('replace', {0}, " +
"{1}, {2})", member.username, "member", "M"))
.from(member)
.fetch();
//소문자로 변경해서 비교
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();
}
참고
lower같은 ansi 표준 함수들은 querydsl이 상당부분 내장하고 있어서
.where(member.username.eq(member.username.lower()))로 사용 가능
[7] QueryDSL 실무 활용 [2/2] (0) | 2021.07.23 |
---|---|
[5] 스프링 데이터 JPA (0) | 2021.07.08 |
[4] JPA 활용2 강의 내용 [2/2] (0) | 2021.06.17 |
[2] 자바 ORM 표준 JPA 프로그래밍 - 기본편(객체지향 쿼리) [2/2] (0) | 2021.05.20 |
[1] 자바 ORM 표준 JPA 프로그래밍 - 기본편 [1/2] (0) | 2021.05.04 |