beadss / jpa-study

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

10-2 정리중 #33

Open 2xel opened 5 years ago

2xel commented 5 years ago

10.4 QueryDSL

1. QueryDSL 설정

2. 시작

기본 Q 생성

// Member 쿼리 타입
public class QMember extends EntityPathBase<Member> {
    public static final QMember member new QMember("member1");
    ...
}

// 쿼리 타입 사용
QMember qMember = new QMember("m"); // 직접 지정
QMember qMember = QMember.member;   // 기본 인스턴스 사용

// import static 활용
import static jpabook.jpashop.domain.QMember.member;    // 기본 인스턴스

public void basic() {
    EntityManager em = emf.createEntityManager();

    JPAQuery query = new JPAQuery(em);
    List<Member> members = query.from(member)
                                .where(member.name.eq("회원1"))
                                .orderBy(member.name.desc())
                                .list(member);
}

3. 검색 조건 쿼리

// 기본 쿼리 기능
JPAQuery query = new JPAQuery(em);
QItem item = QItem.item;
List<Item> list = query.from(item)
                        .where(item.name.eq("좋은상품").and(item.price.gt(20000)))
                        .list(item);    // 조회할 프로젝션 지정
select item
from Item item
where item.name = ?1 and item.price >?2

4. 결과 조회

5. 페이징과 정렬

// 페이징과 정렬
QItem item = QItem.item;

query.from(item)
    .where(item.price.gt(20000))
    .orderBy(item.price,desc(), item.stockQuantity.asc())
    .offset(10).limit(20)
    .list(item);
// 페이징과 정렬 QueryModifiers 사용
QueryModifiers queryModifiers = new QueryModifiers(20L, 10L);
List<Item> list = query.from(item)
                        .restrict(queryModifiers)
                        .list(item);    
// 실제 페이징 처리를 하려면 검색된 전체 데이터 수를 알아야 한다.
// 이때는 list() 대신에 아래와 같이 listResults()를 사용한다.

// 페이징과 정렬 listResult() 사용
SearchResults<Item> result = query.from(item)
                                .where(item.price.gt(10000))
                                .offset(10).limit(20
                                .listResults(item);

long total = result.getTotal(); // 검색된 전체 데이터 수
long limit = result.getLimit();
long offset = result.getOffset();
List<Item> results = result.getResults();   // 조회된 데이터
// listResult()를 사용하면 전체 데이터 조회를 위한 count 쿼리를 한 번 더 실행한다.
// 그리고 SearchResults를 반환하는데 이 객체에서 전체 데이터 수를 조회 할 수 있다.

6. 그룹

// groupBy() 사용
query.from(item)
    .groupBy(item.price)
    .having(item.price.gt(1000))    // 그룹화된 결과를 제한하려면 having을 사용
    .list(item);

7. 조인

// 기본 조인
QOrder order = QOrder.order;
QMember member = QMember.member;
QOrderItem orderItem = QOrderItem.orderItem;

query.from(order)
    .join(order.member, member)
    .leftJoin(order.orderItems, orderItem)
    .list(order);

// 조인 on 사용
query.from(order)
    .leftJoin(order.orderItems, orderItem)
    .on(orderItem.count.gt(2))
    .list(order);

// 페치 조인 사용
query.from(order)
    .innerJoin(order.member, member).fetch()
    .leftJoin(order.orderItems, orderItem).fetch()
    .list(order);

// from 절에 여러 조건 사용 (세타 조인)
query.from(order, member)
    .where(order.member.eq(member))
    .list(order);

8. 서브 쿼리

// 서브 쿼리 예제 - 한건
QItem item = QItem.item;
QItem itemSub = new QItem("itemSub");

query.from(item)
    .where(item.price.eq(new JPASubQuery().from(itemSub).unique(itemSub.price.max())))
    .list(item);

// 서브 쿼리 예제 - 여러건
QItem item = Qitem.item;
QItem itemSub = new QItem("itemSub");

query.from(item)
    .where(item.in( 
        new JPASubQuery().from(itemSub)
        .where(item.name.eq(itemSub.name))
        .list(itemSub)
     ))
     .list(item);

9. 프로젝션과 결과 반환

// 프로젝션 대상이 하나
QItem item = QItem.item;
List<String> resut = query.from(item).list(item.name);

for(String name : result) {
    System.out.println("name = " + name);
}

여러 컬럼 반환과 튜플

// 튜플 사용 예제
QItem item = QItem.item;

List<Tuple> result = query.from(item).list(item.name, item.price);
                                    //list(new QTuple(item.name, item.price));

for(Tuple tuple : result) {
    System.out.println("name = " + tuple.get(item.name));
    System.out.println("price = " + tuple.get(item.price));
}

빈 생성

// 예제 ItemDTO
public class ItemDTO {
    private String username;
    private int price;

    public ItemDTO() {}

    public ItemDTO(String username, int price) {
        this.username = username;
        this.price = price;
    }

    // Getter, Setter ...
}

// 프로퍼티 접근(Setter)
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
    Projections.bean(ItemDTO.class, item.name.as("username"), item.price));
// 필드 직접 접근
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
    Projections.filed(ItemDTO.class, item.name.as("username"), item.price));
// Projections.fields() 메소드를 사용하면 필드에 직접 접근해서 값을 채워준다. private로 설정해도 동작

// 생성자 사용
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
    Projections.constructor(ItemDTO.class, item.name, item.price));
// Projections.constructor() 메소드는 생성자를 사용, 지정한 프로젝션과 파라미터 순서가 같은 생성자가 필요

DISTINCT

10. 수정, 삭제 배치 쿼리

QItem item = QItem.item;
// 수정 배치 쿼리
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
long count = updateClause.where(item.name.eq("시골개발자의 JPA 책"))
                        .set(item.price, item,price,add(100))
                        .execute(); // 상품의 가격을 100원 증가시킨다.

// 삭제 배치 쿼리
JPADeleteClause deleteClause = new JPADeleteClause(em, item);
long count = deleteClause.where(item.name.eq("시골개발자의 JPA 책"))
                        .execute(); // 이름이 같은 상품을 삭제한다.

11. 동적 쿼리

// 동적 쿼리 예제
SearchParam param = new SearchParam();
param.setName("시골개발자");
param.setPrice(10000);

QItem item = QItem.item;

BooleanBuilder builder = new BooleanBuilder();
if(StringUtils.hasText(param.getName())) {
    builder.and(item.name.contains(param.getName()));
}
if(param.getPrice() != null) {
    builder.and(item.price.gt(param.getPrice()));
}
List<Item> result = query.from(item)
                        .where(builder)
                        .list(item);
// 상품 이름과 가격 유무에 따라 동적 쿼리 생성

12. 메소드 위임

// 검색 조건 정의
public class ItemExpression {
    @QueryDelegate(Item.class)
    public static BooleanExpression isExpensive(QItem item, Integer price) {
        return item.price.gt(price);
    }
}
// 메소드 위임 기능을 사용하려면 static 메소드를 만들고 @QueryDelegate 어노테이션에 속성으로 기능을 적용할 엔티티를 지정
// 정적 메소드의 첫번쨰 파라미터에는 대상 엔티티의 쿼리 타입을 지정하고 나머지는 필요한 파라미터 정의

// 쿼리 타입에 생성된 결과
public class QItem extends EntityPathBase<Item> {
    ...
    public BooleanExpression isExpensive(Integer price) {
        isExpensive(Integer price) {
            return ItemExpression.isExpensive(this, price);
        }
    }
}

// 메소드 위임 기능 사용
query.from(item).where(item.isExpensive(30000)).list(item);

// String, Date 같은 자바 기본 내장 타입에도 메소드 위임 기능을 사용할 수 있다.
@QueryDelegate(String.class)
public static BooleanExpression isHelloStart(StringPath stringPath) {
    return stringPath.startsWith("Hello");
}

5. 네이티브 SQL

1. 네이티브 SQL 사용

엔티티 조회

// 엔티티 조회 코드
String sql = "SLECT ID, AGE, NAME, TEAM_ID " +
             "FROM MEMBER " +
             "WHERE AGE > ?";

Query nativeQuery = em.createNativeQuery(sql, Member.class)
                        .setParameter(1, 20);
// 첫번째 파라미터는 네이티브 SQL을 입력하고 두번째 파라미터는 조회할 엔티티 클래스의 타입을 입력
// JPQL를 사용할 때와 거의 비슷하지만 실제 데이터베이스 SQL을 사용한다는 것과 위치기반 파라미터만 지원한다는 차이가 있다.

값 조회

// 값 조회
String sql = "SELECT ID, AGE, NAME, TEAM_ID " +
             "FROM MEMBER " +
             "WHERE AGE > ?";

Query nativeQuery = em.createNativeQuery(sql)
                    .setParameter(1, 10);

List<Object[]> resultList = nativeQuery.getResultList();
for(Object[] row : resultList) {
    System.out.println("id = " + row[0]);
    System.out.println("age = " + row[1]);
    System.out.println("name = " + row[2]);
    System.out.println("team_id = " + row[3]);
}
// em.createNativeQuery(SQL)의 두번쨰 파라미터를 사용하지 않으면
// JPA는 조회한 값들을 Object[]에 담아서 반환한다.
// 여기서는 스칼라 값들을 조회했을 뿐이므로 결과를 영속성 컨텍스트가 관리하지 않는다.
// JDBC로 데이터를 조회한 것과 비슷하다.

결과 매핑 사용

// 결과 매핑 사용
String sql = "SELECT M.ID, AGE, NAME, TEAM_ID, I.ORDER_COUNT " +
             "FROM MEMBER M " +
             "LEFT JOIN " +
             "  (SELECT IM.ID, COUNT(*) AS ORDER_COUNT " +
             "  FROM ORDERS O, MEMBER IM " +
             "  WHERE O.MEMBER_ID = IM.ID) I " +
             "ON M.ID = I.ID";

Query nativeQuery = em.createNativeQuery(sql, "memberWithOrderCount");
List<Object[]> resultList = nativeQuery.getResultList();
for(Object[] row : resultList) {
    Member member = (Member) row[0];
    BigInteger orderCount = (BigInteger) row[1];

    System.out.println("member = " + member);
    System.out.println("orderCount = " + orderCount);
}
// 두번째 파라미터에 결과 매핑 정보의 이름이 사용되었다.

// 결과 매핑을 정의
@Entity
@SqlResultSetMapping(name = "memberWithOrderCount",
                    entities = {@EntityResult(entityClass = Member.class)},
                    column = {@ColumnResult(name = "ORDER_COUNT")})
public class Member {...}
// memberWithOrderCount의 결과 매핑을 잘 보면 회원 엔티티와 ORDER_COUNT 컬럼을 매핑했다.
// ID, AGE, NAME, TEAM_ID는 Member 엔티티와 매핑하고
// ORDER_COUNT는 단순히 값으로 매핑한다.

// 표준 명세 예제 - SQL
Query q = em.createNativeQuery(
    "SELECT o.id AS order_id, " +
    "   o.quantity AS order_quantity, " +
    "   o.item AS order_item, " +
    "   i.name AS item_name, " +
    "FROM Order o, Item i " +
    "WHERE (order_quantity > 25) AND (order_item = i.id)", "OrderResults");

// 표준 명세 예제 - 매핑 정보
@SqlResultSetMapping(name = "OrderResults",
                        entities = {
                            @EntityResult(entityClass=com.acme.Order.class,
                                fields={
                                    @FieldResult(name="id", column="order_id"),
                                    @FieldResult(name="quantity", column="order_quantity", column="order_quantity"),
                                    @FieldResult(name="item", column="order_item")
                                })},
                                columns={
                                    @ColumnResult(name="item_name")}
                    )
// @FieldResult를 사용해서 컬럼명과 필드명을 직접 매핑한다.
// 이 설저은 엔티티의 필드에 정의한 @Column보다 앞선다.
// @FieldResult를 한번이라도 사용하면 전체 필드를 @FieldResult를 사용해야 한다.
// 두 엔티티를 조회하는데 컬럼명이 중복될 때도 @FieldResult를 사용해야 한다.

결과 매핑 어노테이션

@SqlResultSetMapping 속성 속성 기능
name 결과 매핑 이름
entities @EntityResult를 사용해서 엔티티를 결과로 매핑한다.
columns @ColumnResult를 사용해서 컬럼을 결과로 매핑한다.
@EntityResult 속성 속성 기능
entityClass 결과로 사용할 엔티티 클래스를 지정한다.
fields `@FieldResult을 사용해서 결과 컬럼을 필드와 매핑한다.
discriminatorColumn 엔티티의 인스턴스 타입을 구분하는 필드(상속에서 사용됨)
@FieldResult 속성 속성 기능
name 결과를 받을 필드명
column 결과 컬럼명
@ColumnResult 속성 속성 기능
name 결과 컬럼명

2. Named 네이티브 SQL

// 엔티티 조회
@Entity
@NamedNativeQuery(
    name = "Member.memberSQL",
    query = "SELECT ID, AGE, NAME, TEAM_ID " +
            "FROM MEMBER " +
            "WHERE AGE > ?",
    resultClass = Member.class)
public class Member {...}
// @NamedNativeQuery로 Named 네이티브 SQL을 등록했다.

// 사용예제
TypedQuery<Member> nativeQuery = em.createNamedQuery("Member.memberSQL", Member.class)
                                    .setParameter(1, 20);
// JPQL Named 쿼리와 같은 createNamedQuery 메소드를 사용하는 것이다.
// TypedQuery를 사용할 수 있다.

// 결과 매핑 사용
@Entity
@SqlResultSetMapping( name = "memberWithOrderCount",
    entities = {@EntityResult(entityClass = Member.class)},
    column = {@ColumnResult(name = "ORDER_COUNT")}
)
@NamedNativeQuery(
    name = "Member.memberWithOrderCount",
    query = "SELECT M.ID, AGE, NAME, TEAM_ID, I.ORDER_COUNT " +
            "FROM MEMBER M " +
            "LEFT JOIN " +
            "   (SELECT IM.ID, COUNT(*) AS ORDER_COUNT " +
            "   FROM ORDERS O, MEMBER IM " +
            "   WHERE O.MEMBER_ID = IM.ID) I " +
            "ON M.ID = I.ID",
    resultSetMapping = "memberWithOrderCount"
)
public class Member {...}
// Named 네이티브 쿼리에서 resultSetMapping = "memberWithOrderCount"로 조회 결과를 매핑할 대상까지 지정

// Named 네이티브 쿼리를 사용하는 코드
List<Object[]> resultList = em.createNamedQuery("Member.memberWIthOrderCount")
                            .getResultList();

@NamedNativeQuery

속성 기능
name 네임드 쿼리 이름(필수)
query SQL 쿼리(필수)
hints 벤더 종속적인 힌트
resultClass 결과 클래스
resultSetMapping 결과 매핑 사용
@NamedNativeQueries({
    @NamedNativeQuery(...),
    @NamedNativeQuery(...)
})

4. 네이티브 SQL 정리

6. 객체지향 쿼리 심화

1. 벌크 연산

// UPDATE 벌크 연산
String qlString = "update Product p " +
                  "set p.price = p.price * 1.1 " +
                  "where p.stockAmount < :stockAmount";

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

// DELETE 벌크 연산
String qlString = "delete from Product p " +
                  "where p.price < :price";

int resultcount = em.createQuery(qlString)
                    .setParameter("price", 1000)
                    .executeUpdate();

// JPA 표준은 아니지만 하이버네이트 INSERT 벌크 연산
String qlString = "insert into ProductTemp(id, name, price, stockAmount) " +
                  "select p.id, p.name, p.price, p.stockAmount from Product p " +
                  "where p.price < :price";

int resultCount = em.createQuery(qlString)
                    .setParamter("price", 100)
                    .executeUpdate();

벌크 연산 주의점

// 벌크 연산 시 주의점 예쩨
// 상품A 조회(상품A의 가격은 1000원이다)
Product productA = em.createQuery("select p from Product p where p.name = :name", Product.class)
                    .setParameter("name", "productA")
                    .getSingleResult();

// 출력 결과: 1000
System.out.println("ProductA 수정 전 = " + productA.getPrice());

// 벌크 연산 수행으로 모든 상품 가격 10% 상승
em.createQuery("update Product p set p.price = p.price * 1.1")
  .executeUpdate();

// 출력 결과: 1000
System.out.println("ProductA 수정 후 = " + productA.getPrice());

2. 영속성 컨텍스트와 JPQL

쿼리 후 영속 상태인 것과 아닌 것

JPQL로 조회한 엔티티와 영속성 컨텍스트

find() vs JPQL

3. JPQL과 플러시 모드

쿼리와 플러시 모드

// 쿼리와 플러시 모드 예제
product.setPrice(2000); // 가격을 1000 -> 2000원으로 변경

Product product2 = em.createQUery("select p from Product p where p.price = 2000", Product.class)
                    .getSingleResult();

// 플러시 모드 설정
em.setFlushMode(FlushModeType.COMMIT);  // 커밋 시에만 플러시

// 가격을 1000 -> 2000원으로 변경
product.setPrice(2000);

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

// 가격이 2000원인 상품 조회
Product product2 = em.createQuery("select p from Product p where p.price = 2000", Product.class)
                    .setFlushMode(FlushModeType.AUTO)   // 2. setFlushMode() 설정
                    .getSingleResult();

플러시 모드와 최적화