QueryDSL 란?
QeuryDsl -> 복잡한 쿼리, 동적쿼리 를 위한 라이브러리 이다. 또한 Q 클래스를 생성한다.
장점은
ㄴ 쿼리를 자바 코드로 작성하기 때문에 문법 오류를 컴파일 시점에서 해결.
ㄴ 동적 쿼리 문제 해결
ㄴ 쉬운 SQL 스타일 문법
ㄴ 단순 반복 X
ㄴ 개발자가 비즈니스 로직에만 집중할수 있도록 도움.
단점은
ㄴ 초기 설정이 불편하다..
주의사항
Q 클래스들은 버전 up 이 될시 세부적으로 바뀔수 있으므로 git 에 올리지 말것 - 쓰는데 상관 없음.
JPQL vs QueryDSL
JPA 에서 지원해주는 JPQL 과 QueryDSL 의 차이점은 JPQL 은 직접 쳐야하지만
QueryDsl 은 매서드가 존재함으로 실수 할 가능성이 낮다.
즉 컴파일 오류를 잡아주니 QueryDsl 이 더 좋다.
파라미터 바인딩을 자동으로 해준다.
그러므로 QueryDSL 이 더 좋다고 한다.
또한, 내가 쓸때도 기본적으로 동적 쿼리를 작성할때 JPQL 로 쓰는것보다 QueryDSL 로 커스텀해서 쓰는것이 더 편리했었다.
JPQL
@Test
void startJPQL() {
Member findByJQL = em.createQuery("select m from Member m where m.username= :username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
Assertions.assertThat(findByJQL.getUsername())
.isEqualTo("member1");
}
QueryDsl
@Test
void StartQueryDsl1() {
QMember m = new QMember("m");
Member member = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1"))
.fetchOne();
assertThat(member.getUsername()).isEqualTo("member1");
}
위와 같이 두개의 차이점이 분명하다 쿼리문을 매서드를 통한 방식이 아닌 문자열로 sql 문을 작성하니 당연히 오류가 있어도 잡을수가 없다. 하지만 QueryDSL 는 이런점을 해결해준다. ( 너무 좋았다. )
QueryDSL 초기 설정 ( gradle ) - Spring boot 3.0 이상 버전
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.1'
id 'io.spring.dependency-management' version '1.1.0'
//querydsl 추가
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
dependencies {
// Querydsl 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
}
//querydsl 추가 시작
//위치
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
//위치 IDE 애들이 자동으로 import 해줌.
sourceSets {
main.java.srcDir querydslDir
}
//컴파일 되면서 Q 클래스 생성
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
//querydsl 추가 끝
위의 설정을 그대로 복사하지말것. QueryDSL 만 사용하는 구문만 붙여놓기 하였다.
해당 gradle 에 쓴 코드에 의하면 나는 build.querydsl.generated 의 경로에 Q 클래스들을 생성하기로 하였다.
JPAQueryFactory
기본적으로 QueryDSL 를 사용할려면 EntityManager 를 JPAQueryFactory 생성자에 넣어 만들어서
JPAQueryFactory 를 가지고 Q 클래스들로 쓰게 된다. 아래의 예제를 보자.
@Autowired
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void before() {
queryFactory = new JPAQueryFactory(em);
}
@Test
void StartQueryDsl1() {
QMember m = new QMember("m");
Member member = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1"))
.fetchOne();
assertThat(member.getUsername()).isEqualTo("member1");
}
EntityManager 를 받아와서 JPAQueryFactory 에 넣고 JPAQueryFactory 를 가지고 select 나 from, where 를 쓰게 된다.
하지만 EntityManager 를 @Bean 으로 등록시키고 의존성 주입 받는 방식도 있다. 아래에 코드를 쓰겠다.
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em){
return new JPAQueryFactory(em);
}
------------------------------------------------------------------------------------------
@Repository
public class MemberJpaRepository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
public MemberJpaRepository(EntityManager em, JPAQueryFactory jpaQueryFactory) {
this.em = em;
this.queryFactory = jpaQueryFactory;
}
}
위와 같이 @Bean 으로 등록시키고 JPAQueryFactory 를 바로 쓸수 있게 하였다. 또한 EntityManager 를 받은 이유는
EntityManager 를 통한 작업을 할수 있기 때문에 미리 받아 두었다.
where()
@Test
void search() {
QMember m = QMember.member;
Member findMember = queryFactory
.selectFrom(m)
.where(m.username.eq("member1")
.and(m.age.eq(10)))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
이런식으로 Q 클래스를 받아 안에 eq 로 통해 걸르게 된다.
아래는 이러한 방식이 있다는 것을 알려준다.
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%’ 검색
딱 보면 SQL 문에 쓰는 함수들을 전부 사용할수 있게 된다.
fetch() 종류
fetch 의 종류는 5개정도로 나뉠수 있다.
fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
fetchOne() : 단 건 조회
결과가 없으면 : null
결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
fetchFirst() : limit(1).fetchOne()
fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
fetchCount() : count 쿼리로 변경해서 count 수 조회
QMember m = QMember.member;
//List
List<Member> member = queryFactory
.selectFrom(m)
.fetch();
//단 건 -> 값이 두개 이상이면 안됨.
Member memberOne = queryFactory
.selectFrom(m)
.where(QMember.member.age.eq(10))
.fetchOne();
//처음 한 건 조회
Member memberFirst = queryFactory
.selectFrom(m)
.fetchFirst();
//페이징에서 사용
QueryResults<Member> results = queryFactory
.selectFrom(m)
.fetchResults();
results.getTotal(); //쿼리
List<Member> members = results.getResults(); //쿼리
//즉 두번 쿼리문 날림.
//count 쿼리로 변경
long count = queryFactory
.selectFrom(m)
.fetchCount();
마지막에서 두번째인 QueryResults 는 쿼리를 날리기전에 저장해놓고 해당 들어있는 매서드들을 쓰게되면 그때 쿼리문으로 가져오는 형태인것 같다. 다른것을 전부 쓰면 바로 쿼리문을 날려 데이터를 갖고 오는데.
정렬기능도 지원해준다.
@Test
public void sort() {
QMember member = QMember.member;
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();
/**
* desc() , asc() : 일반 정렬
* nullsLast() , nullsFirst() : null 데이터 순서 부여
*/
}
위와 같이 member 데이터를 넣어주고 where 을 통해 100살들만 걸러서 oerderBy 를 통해 나이순으로 정렬하고 null 일때는 마지막에 정렬해줘서 넣어주게 되는 쿼리 이다.
offset 을 통해 페이지처리를 가능하게 한다.
@Test
public void paging2() {
QMember member = QMember.member;
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 쿼리는 원본 쿼리와 같이 모두
* 조인을 해버리기 때문에 성능이 안나올 수 있다. count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면,
* count 전용 쿼리를 별도로 작성해야 한다.
*/
}
주의: count 쿼리가 실행되니 성능상 주의! * > 참고: 실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, * count 쿼리는 조인이 필요 없는 경우도 있다. 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두 * 조인을 해버리기 때문에 성능이 안나올 수 있다. count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, * count 전용 쿼리를 별도로 작성해야 한다.
SQL 함수들을 쓰는 법
/**
* COUNT(m), //회원수
* SUM(m.age), //나이 합
* AVG(m.age), //평균 나이
* MAX(m.age), //최대 나이
* MIN(m.age) //최소 나이
* from Member m
*/
@Test
public void aggregation() throws Exception {
QMember member = QMember.member;
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);
}
join 방법
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 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 {
QMember member = QMember.member;
QTeam team = QTeam.team;
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);
}
}
/**
* 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 {
QMember member = QMember.member;
QTeam team = QTeam.team;
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);
}
}
여기서의 튜플은 프로젝션 대상이 둘 이상일때 사용하게 된다.
가져오는 데이터에 값 변경을 한다. ( Case )
List<String> result2 = 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();
위와 같이 0~20 이 걸리면 0~20살로 변환되어 값이 저장된다.
'Spring' 카테고리의 다른 글
테스트 도구 Apache JMeter (0) | 2023.08.11 |
---|---|
Spring interface 의존성 주입 (0) | 2023.07.31 |
Filter (0) | 2023.06.26 |
Spring Security (3) (0) | 2023.06.26 |
JWT (0) | 2023.06.26 |