Querydsl 수업을 듣고 정리한 내용입니다.
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@Autowired
EntityManager em;
@BeforeEach
public void before() {
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, teamA);
Member member4 = new Member("member4", 40, teamA);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
@Test
public void startJPQL() {
// member1를 찾아라
Member findMember = em.createQuery("select m from Member m where m.username = :username", 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"); // 어떤 qmember인지 이름을 준 것이다.
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1")) // 파라미터 바인딩 처리
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
EntityManager
로 JPAQueryFactory
생성Querydsl
은 JPQL 빌더JPQL
: 문자 (실행 시점 오류) vs Querydsl
: 코드 (컴파일 시점 오류)JPQL
: 파라미터 바인딩 직접 vs Querydsl
: 파라미터 바인딩 자동 처리Querydsl
은 문법을 틀렸을 경우, 컴파일 타임에 오류를 바로 잡을 수 있다.
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void before() {
queryFactory = new JPAQueryFactory(em);
//…
}
@Test
public void startQuerydsl2() {
//member1을 찾아라.
QMember m = new QMember("m");
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
}
JPAQueryFactory
를 필드로 제공하면 동시성 문제는 JPAQueryFactory
를 생성할 때 제공하는EntityManager(em)
에 달려있다.
스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager
에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다.
실행 결과
✔️ Q클래스 인스턴스를 사용하는 2가지 방법
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
이전에는 Q클래스 인스턴스를 new
를 사용하여 별칭을 직접 지정한 방법을 사용했는데, Querydsl
가 기본적으로 제공하는 기본 인스턴스를 사용하는 것이 더 간편하다!
✔️ 기본 인스턴스를 static import와 함께 사용
동시성 문제를 걱정하지 않아도 된다.
이전에 실행되는 @Test
before
에서 queryFactory = new JPAQueryFactory(em);
와 같이 구현하면된다. 구현할 시, 실행하기전 @BeforeEach
애노테이션이 적힌 메서드가 실행된다.
@Test
public void startQuerydsl3() {
//member1을 찾아라.
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
이렇게 쓰는 방법을 권장한다.
static import
를 사용하면 간편하게 사용할 수 있다.
위에 작성한 소스 실행시 (member1
일 때)
JPQL
, 두 번째 Querydsl
@Test
public void startQuerydsl3() {
//member1을 찾아라.
// 같은 테이블을 조인하는 경우 이와 같이 사용한다.
QMember m1 = new QMember("m1");
Member findMember = queryFactory
.select(m1)
.from(m1)
.where(m1.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
m1
이 보인다.
✔️ 기본 검색 쿼리
@Test
public void search() {
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
.and()
, .or()
를 메서드 체인으로 연결할 수 있다.
실행 결과
💡 참고
select
,from
을selectFrom
으로 합칠 수 있다.
✔️ JPQL이 제공하는 모든 검색 조건 제공
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%’ 검색 ...
✔️ AND 조건을 파라미터로 처리
@Test
public void searchAndParam() {
List<Member> result1 = queryFactory
.selectFrom(member)
.where(
member.username.eq("member1"),
member.age.eq(10))
.fetch();
assertThat(result1.size()).isEqualTo(1);
}
where()
에 파라미터로 검색조건을 추가하면 AND
조건이 추가된다.null
값은 무시한다. → 메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있다.
fetch()
: 리스트 조회, 데이터 없으면 빈 리스트 반환fetchOne()
: 단건 조회null
com.querydsl.core.NonUniqueResultException
fetchFirst()
: limit(1).fetchOne()
와 같다.fetchResults()
: 페이징 정보 포함, total count 쿼리 추가 실행fetchCount()
: count 쿼리로 변경해서 count 수 조회
//List
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
//단건
Member findMember1 = queryFactory
.selectFrom(member)
.fetchOne();
//처음 한 건 조회
Member findMember2 = queryFactory
.selectFrom(member)
.fetchFirst();
//페이징에서 사용
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults();
//count 쿼리로 변경
long count = queryFactory
.selectFrom(member)
.fetchCount();
✔️ 페이징에서 사용
@Test
public void resultFetch() {
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults();
results.getTotal();
List<Member> content = results.getResults();
✔️ count 쿼리로 변경
@Test
public void resultFetch() {
long total = queryFactory
.selectFrom(member)
.fetchCount();
}
/**
* 회원 정렬 순서
* 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();
}
desc()
, asc()
: 일반 정렬desc()
: 내림차순asc()
: 오름차순nullsLast()
, nullsFirst()
: null 데이터 순서 부여
실행 결과
✔️ 조회 건수 제한
@Test
public void paging1() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetch();
assertThat(result.size()).isEqualTo(2);
}
}
✔️ 전체 조회 수가 필요한 경우
@Test
public void paging2() {
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 전용 쿼리를 별도로 작성해야 한다.