beadss / jpa-study

jpa슽터디입니다
1 stars 2 forks source link

10-2 정리중 #32

Open joont92 opened 5 years ago

joont92 commented 5 years ago

JPQL을 편하게, 동적으로 작성할 수 있도록 JPA에서 공식 지원하는 Creteria 라는것이 있다.
하지만 큰 단점이 있는데, 너무 불편하다는 것이다.

그에 반해 JPA에서 공식 지원하지는 않지만
쿼리를 문자가 아닌 코드로 작성해도 쉽고 간결하며, 모양도 쿼리와 비슷하게 개발할 수 있는 QueryDSL 이라는 것이 있다.
QueryDSL은 오픈소스 프로젝트이며, 이름 그대로 데이터를 조회하는데 기능이 특화되어 있다.

최범균님이 번역한 공식 한국어 문서를 제공한다.
http://www.querydsl.com/static/querydsl/4.0.1/reference/ko-KR/html_single/

설정

사용(4.1.3 버전 기준)

기본 사용법

동적으로 생성할 쿼리는 JPAQuery를 사용하여 만들 수 있는데, 이것보단 JPAQueryFactory를 사용하는게 권장된다고 한다.

JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember member = QMember.member;

Member foundMember = 
    queryFactory.selectFrom(member) // select + from
    .where(customer.username.eq("joont"))
    .fetchOne();

대충 위의 형태로 사용할 수 있다.

결과반환

프로젝션

프로젝션을 지정한다.

List<Member> foundMembers = 
    queryFactory.select(member)
    .from(member, order)
    .fetch();

(아직 나오진 않았지만 from 절에 위처럼 쿼리 타입을 연속으로 줄 경우, 두 엔티티가 조인된다.)

member와 order가 조인된 상태에서 member 엔티티의 속성만 가져온다.
(select를 생략하면 기본적으로 from의 첫번째 엔티티를 프로젝션 대상으로 쓴다)

from

쿼리할 대상을 지정한다.

List<Member> foundMembers = 
    queryFactory.from(member)
    .fetch();

member 테이블을 전체 조회하게 된다. 프로젝션 지정(select)가 빠졌지만 위와 동일하게 from의 첫번째 엔티티를 사용한다.

from과 select를 나누기 보단 selectFrom 절을 쓰는것이 더 낫다.

조인

join, innerJoin, leftJoin, rightJoin 을 지원한다.
개인적으로 from절에 multiple arguments를 주는것보다 이게 더 좋다.(SQL에서도...)

QTeam team = QTeam.team;

List<Member> foundMembers = 
    queryFactory.selectFrom(member)
    .innerJoin(member.team, team)
    .fetch();

join의 첫번쨰 인자로는 join할 대상, 두번쨰 인자로는 join할 대상의 쿼리 타입을 주면 된다. on 절은 자동으로 붙는다.

조건

List<Member> foundMembers = 
    queryFactory.selectFrom(member)
    .where(member.username.eq("joont")) // 1. 단일 조건  
    .where(member.username.eq("joont"), member.homeAddress.city.eq("seoul")) // 2. 복수 조건. and로 묶임  
    .where(member.username.eq("joont").or.member.homeAddress.city.eq("seoul")) // 3. 복수 조건. and나 or를 직접 명시할 수 있음  
    .fetch();

그룹핑

group by도 가능하다.

List<String> foundCities = 
    queryFactory.from(member)
    .select(member.homeAddress.city)
    .groupBy(member.homeAddress.city)
    .fetch();

city로 group by 한 뒤 city만 출력하게 된다.

정렬

List<Member> foundMembers = 
    queryFactory.selectFrom(member)
    .orderBy(member.id.asc(), member.username.desc())
    .fetch();

페이징

시작 인덱스를 지정하는 offset,
조회할 개수를 지정하는 limit,
두개를 인수로 받는 QueryModifiers를 사용하는 restrict를 지원한다.

근데 실제로 페이징 처리를 하려면 전체 데이터 개수를 알고 있어야하므로, fetchResults()를 사용해야 한다.

QueryResults<Member> result = 
    queryFactory.selectFrom(member)
    .offset(10)
    .limit(10)
    .fetchResults();

List<Member> foundMembers = result.getResults(); // 조회된 member
long total = result.getTotal(); // 전체 개수  
long offset = result.getOffset(); // offset
long limit = result.getLimit(); // limit

다중 결과 반환

다중 프로젝션 할 경우 Tuple 클래스로 받을 수 있다.

List<Tuple> foundMembers = 
    queryFactory.select(member.username, member.homeAddress.city)
    .from(member)
    .fetch();

System.out.println(founeMembers.get(0));
System.out.println(founeMembers.get(1));

class com.querydsl.core.types.QTuple$TupleImpl 클래스가 리턴되는데 이것보단 아래 bean population을 쓰는게 더 나아보인다.

List<MemberDTO> foundMembers = 
    queryFactory.select(Projections.fields(UserDTO.class, member.username, member.homeAddress.city))
    .from(member)
    .fetch();

서브쿼리

동적 조건

수정, 삭제, 배치 쿼리

벌크 연산(UPDATE, DELETE)

JPQL로 여러 건을 한번에 수정하거나 삭제할 떄 사용한다.
아래는 UPDATE 벌크 연산이다.

String sql = "UPDATE Product p " +
    "SET p.prce = p.price * 1.1 " +
    "WHERE p.stockAmount < :stockAmount";

int resultCount = em.createQuery(sql)
        .setParameter("stockAmount", 10)
        .executeUpdate();

executeUpdate 메서드를 사용한다. 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.
아래는 DELETE 벌크 연산이다.

String sql = "DELETE FROM Product p " +
    "WHERE p.price < :price";

int resultCount = em.createQuery(sql)
        .setParameter("price", 100)
        .executeUpdate();

벌크 연산시 주의사항

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 특징이 있으므로 주의해야 한다.
아래는 발생가능한 문제 상황이다.

Product product = em.find(Product.class, 1);
assertThat(product.getPrice(), is(1000));

String sql = "UPDATE Product p " +
    "SET p.prce = p.price * 1.1";
em.createQuery(sql).executeUpdate();

assertThat(product.getPrice(), is(1100)); // FAIL!!

벌크 연산은 영속성 컨텍스트와 2차 캐시를 무시하고 데이터베이스에 직접 쿼리한다.
따라서 위와 같이 영속성 컨텍스트와 데이터베이스 간에 데이터 차이가 발생할 수 있는 것이다.
이를 해결하기 위한 방법은 아래와 같다.

영속성 컨텍스트와 JPQL

영속성 컨텍스트에 이미 있는 엔티티를 JPQL로 다시 조회해올 경우 어떻게 처리될까?

Member member1 = em.find(Member.class, 1);

List<Member> list = 
    em.createQuery("SELECT m FROM Member m", Member.class)
    .getResultLst();

이미 영속성 컨텍스트에 들어있는 1번 member가 JPQL에 의해 다시 한번 조회되는 상황이다.
결과부터 말하자면 JPQL 쿼리는 쿼리대로 다 날라가고, 조회한 엔티티를 영속성 컨텍스트에 다 저장한다.
여기서 중요한 점은 1번 member의 경우 영속성 컨텍스트에 이미 들어있으므로, JPQL로 조회해온 1번 member는 그냥 버려진다는 것이다.

JPQL 조회시 영속성 컨텍스트

보다시피 조회해온 member들 중 1번 member는 영속성 컨텍스트에 이미 있으므로 그 결과가 버려진다.
영속성 컨텍스트에 없는 2번 member의 경우 영속성 컨텍스트에 저장된다.

새로 조회해온 결과를 기존 영속성 컨텍스트에 덮어쓰지 않는 이유는 영속 상태인 엔티티의 동일성을 보장해야하기 때문이다.

find로 들고오든, JPQL로 들고오든 동일한 엔티티를 반환해야 한다.

Member member1 = em.find(Member.class, 1);
Member member2 = 
    em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
    .setParameter("id", 1)
    .getSingleResult();

assertSame(member1, member2); // SUCCESS

JPQL 실행 시 플로우

보다시피 영속성 컨텍스트에 1번 member 엔티티가 있더라도 무조건 SQL을 실행해서 조회해온다.
(JPQL을 분석해서 영속성 컨텍스트를 조회하는 것은 너무 힘들기 때문이다.)
그리고 조회해온 엔티티를 영속성 컨텍스트에 넣을 때, 이미 있는 엔티티일 경우 결과를 버린다.

JPQL과 플러시 모드

플러시 모드는 FlushMode.AUTO(Default), FlushMode.COMMIT이 있다.
이때까지 FlushMode.AUTO 는 트랜잭션이 끝날때나 커밋될 때만 플러시를 호출하는 것으로 알고 있었으나, 사실은 시점이 하나 더 있다. JPQL 쿼리를 실행하기 직전이다.

Member member1 = em.find(Member.class, 1);
member1.setName("modified name");

Member member2 = 
    em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
    .setParameter("id", 1)
    .getSingleResult();

assertThat(member1.getName(), member2.getName());

변경감지는 플러시 될때 발생하므로, JPQL에서 아직 변경되지 않은 name 값을 가진 1번 member를 가져올 것이라고 생각할 수 있지만,
FlushMode.AUTO는 영속 상태인 엔티티의 동일성을 보장하기 위해 JPQL 실행 전에 플러시를 수행한다.
그러므로 위의 테스트는 성공한다.

어떻게 동작하는지 정확히는 모르겠으나, 영속성 컨텍스트에 있는 엔티티에 대해 JPQL을 실행할 떄만 플러시를 수행한다.
즉, 위의 상황에서 JPQL로 Team을 조회해올 경우 플러시가 발생하지 않는다.

하지만 이 상황에서 FlushMode.COMMIT으로 설정하면 쿼리전에 플러시를 수행하지 않으므로 위의 테스트가 실패하게 된다.
이때는 직접 em.flush를 호출해주거나, Query 객체에 직접 플러시 모드를 설정해주면 된다.

em.setFlushMode(FlushMode.COMMIT); // 커밋시에만 플러시

Member member1 = em.find(Member.class, 1);
member1.setName("modified name");

em.flush(); // 1. em.flush 직접 호출

Member member2 = 
    em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
    .setParameter("id", 1)
    .setFlushMode(FlushMode.AUTO) // 2. setFlushMode 설정
    .getSingleResult();

assertThat(member1.getName(), member2.getName());

FlushMode.COMMIT은 너무 잦은 플러시가 일어나는 경우, 플러시 횟수를 줄여서 성능을 최적화하고자 할 때 사용할 수 있다.