JPA ORM(Object-Relational Mapping) -1

안녕하세요. 오늘은 JPA를 사용하면서 공부했던 내용을 적어보고자 합니다.

Named 엔티티 그래프

Named 엔티티 그래프는 JPA(Java Persistence API)에서 사용되는 기능 중 하나로, 엔티티와 그와 연관된 속성들을 명시적으로 로딩하는 방법을 지정하는 기능입니다. 이를 통해 성능 최적화와 불필요한 데이터 로딩을 방지할 수 있습니다.

보통 JPA를 사용하여 엔티티를 조회하면, 모든 연관 관계(Eager 또는 Lazy)가 로딩됩니다. 하지만 모든 경우에 모든 연관 관계를 로딩하는 것은 비효율적일 수 있습니다. Named 엔티티 그래프는 이러한 상황에서 원하는 연관 관계만 로딩할 수 있도록 지정하는 방법을 제공합니다.

Named 엔티티 그래프를 사용하는 방법은 다음과 같습니다:

  1. 엔티티 클래스에 @NamedEntityGraph 애노테이션을 추가하여 Named 엔티티 그래프를 정의합니다.
  2. @NamedEntityGraph 애노테이션의 속성으로 name을 지정하여 Named 엔티티 그래프의 이름을 지정합니다.
  3. @NamedEntityGraph 애노테이션 내부에 attributeNodes 속성을 사용하여 로딩할 연관 관계의 속성을 지정합니다.

예를 들어, 다음은 Order 엔티티 클래스에 Named 엔티티 그래프를 정의한 예시입니다:

@NamedEntityGraph(name = "Order.withMember" attributeNodes = {
    @NamedAttributeNode("member") //name :엔티티 그래프는 @NamedAttributeNode를 사용하고 그값으로 
                                    //함께 조회할 속성을 선택하면 된다. 
                                //attributeNodes 함께 조회할 속성을 선택한다. 이때 @NamedAttributeNode를 
                                //사용하 그 값으로 함께 조회할 속성을 선택하면 된다.
})
@Entity
@Table(name = "ORDERS")
public class Order {
    @Id @GeneratedValue
    @Column(name = "Order_ID")
    private Long id;
 
    @ManytoOne(fetch = FetchType.Lazy, optional = false)
    @JoinColumn(name = "Member_ID")
    private Member member; //주문회원
}

Order.member는 지연로딩으로 설정되어 있지만 엔티티 그래프에서 함께 조회할 속성으로 member를 선택했으므로 사용하면 Order를 조회할 때 연관된 member도 함께 조회를 할 수 있다.

둘 이상을 정의하고 싶을 때는 @NamedEntityGraphs를 사용하면 된다.

@NamedEntityGraph(name = "Order.withMember" attributeNodes = {
    @NamedAttributeNode("member") //name :엔티티 그래프는 @NamedAttributeNode를 사용하고 그값으로 
                                    //함께 조회할 속성을 선택하면 된다. 
                                //attributeNodes 함께 조회할 속성을 선택한다. 이때 @NamedAttributeNode를 
                                //사용하 그 값으로 함께 조회할 속성을 선택하면 된다.
})
@Entity
@Table(name = "ORDERS")
public class Order {
    @Id @GeneratedValue
    @Column(name = "Order_ID")
    private Long id;
 
    @ManytoOne(fetch = FetchType.Lazy, optional = false)
    @JoinColumn(name = "Member_ID")
    private Member member; //주문회원
}

실행된 SQL를 확인해 보면

select o.*, m.*
from 
      ORDERS o

inner join 

      Member m

             on o.MEMBER_ID = m.MEMBER_ID
where
      o.ORDER_ID = ?

 

QueryDSL

QueryDSL은 자바 기반의 오픈 소스 SQL 쿼리 빌더이자 JPA(Java Persistence API)와 JDO(Java Data Objects)를 지원하는 프레임워크입니다. QueryDSL을 사용하면 SQL 쿼리를 문자열로 작성하는 대신 자바 코드로 쿼리를 작성할 수 있으며, 이로 인해 타입 안정성과 코드 가독성을 높일 수 있습니다.

주요 기능과 특징:

  1. 타입 안정성: QueryDSL은 자바 코드로 쿼리를 작성하기 때문에 컴파일 시점에서 오류를 검출할 수 있으며, 오타나 잘못된 필드명 등의 문제를 방지합니다.
  2. ORM과의 통합: QueryDSL은 JPA와 JDO와 같은 ORM 프레임워크와의 통합을 제공하여 엔티티와 연관된 쿼리를 자연스럽게 작성할 수 있습니다.
  3. 다양한 데이터베이스 지원: QueryDSL은 다양한 데이터베이스를 지원하며, 데이터베이스 종속적인 기능도 지원합니다.
  4. Fluent API: QueryDSL은 메서드 체인을 사용하여 쿼리를 작성할 수 있으며, 이로 인해 쿼리의 가독성을 높여줍니다.
  5. 집합 연산 지원: 집합 연산과 서브쿼리도 지원하므로 복잡한 쿼리도 유연하게 작성할 수 있습니다.

QueryDSL의 사용법은 다음과 같습니다:

  1. 라이브러리 추가: QueryDSL을 사용하기 위해 해당 프로젝트의 의존성에 QueryDSL 라이브러리를 추가합니다.
  2. Q클래스 생성: QueryDSL은 엔티티 클래스를 기반으로 Q클래스를 생성하여 사용합니다. Q클래스는 엔티티의 필드와 테이블 정보를 담고 있으며, 쿼리 작성에 사용됩니다. Q클래스는 빌드 시점에 자동으로 생성됩니다.
  3. QueryDSL 쿼리 작성: Q클래스를 이용하여 QueryDSL 쿼리를 작성합니다. QueryDSL은 JPQL을 기반으로 하며, Criteria API보다 간결하고 가독성이 좋은 쿼리를 작성할 수 있도록 도와줍니다.
  4. 쿼리 실행: QueryDSL로 작성한 쿼리를 실행하고 결과를 받아옵니다. JPA를 사용하는 경우 JPA의 EntityManager를 이용하여 쿼리를 실행합니다.

예를 들어, JPA를 사용하는 상황에서 QueryDSL을 이용하여 쿼리를 작성하고 실행하는 예시는 다음과 같습니다.

public void queryDSL() {
    
    EntityManager em = emf.createEntityManager();
 
    JPAQuery query = new JPAQuery(em);
    QMember qMember = new QMember("m"); //생성되는 JPQL의 별칭이 m
    List<Member> members = 
        query.from (qMember)
            .where(qMember.name.eq("회원1"))
            .orderBy(qMember.name.desc())
            .list(qMember);
}

QueryDSL을 사용하려면 우선 com.mysema.query.jpa.impl.JPAQuery 객체를 생성해야 하는데

이때 엔티티 매니저를 생성자에 넘겨준다.

쿼리 타입(Q)은 사용하기 편리하도록 아래와 같이 기본 인스턴스를 보관하고 있다.

같은 엔티티를 조인하거나 같은 엔티티를 서브쿼리에 사용하면 같은 별칭을 사용하므로 별칭은 직접 지정해서 사용한다.

  • 쿼리 타입(Q)
public class QMember extends EntityPathBase<Member>{
 
    public static final QMember member = new QMember("member1");
    
    //or You can use QClass's Nickname as ...
    QMember qMember = new QMember("m"); //직접 지정
    QMember qMember = QMember.member; //기본 인스턴스 사용
  • import해서 사용하는 방법도 있다.
import static 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);
}

검색 조건 쿼리

QueryDSL의 where 절에는 and 나 or을 사용할 수 있다.

.where(item.name.eq("좋은상품"), item.price.gt(20000))
//item.price between(10000,20000) 가격이 10000원~ 20000원
//item.price.contains("상품1"); SQL에서 like '%상품1%' 검색
//item.price.startsWith("고급"); SQL에서 like '고급%' 검색

결과 조회

list()나 uniqueResult()를 사용하여 파라미터로 넘겨준다.

uniqueResult()는 조회 결과가 한 건일 때 사용한다.

singleResult()는 조회 결과가 한 건일 때 사용하지만 하나 이상이면 처음 데이터를 반환한다

list()는 결과가 하나 이상일 때 사용한다. 결과가 없으면 빈 컬렉션을 반환한다.

  • 페이징과 정렬
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)
//정렬은 orderBy를 사용하는데 쿼리 타입(Q)이 제공하는 asc(), desc()를 사용한다
//페이징은 offset과 limit을 적절히 조합해서 
  • 페이징과 정렬 QueryModifiers 사용
QueryModifiers queryModifiers = new QueryModifiers(20L,10L); //limit, offset
List<Item> list = 
    query.from(item)
    .restrict(queryModifiers)
    .list(item);
 
  • 페이징 처리시 검색되는 전체 데이터 수를 알아야 한다.
SearchResult<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(); //조회된 데이터
//listResults()를 사용하면 전체 데이터 조회를 위한 count쿼리를 한번 더 
//실행한다. 그리고 searchResults를 반환하는데 이 객체에서 전체 데이터 수를 
//조회 할 수 있다.
  • groupBy를 사용하고 그룹화된 결과를 제한하려면 having을 사용하면 된다.
query.from(item)
    .groupBy(item.price)
    .having(item.price.gt(1000))
    .list(item);

조인은 innerJoin(join), leftJoin, rightJoin, fullJoin을 사용할 수 있다.

  • JPQL의 on과 성능 최적화를 위한 fetch 조인도 사용할 수 있다.
///////////기본조인///////////////////////
QOrder order = QOrder.order;
QMember member = QMember.member;
QOrderItem orderItem = QOrderItem.orderItem;
 
query.from(order)
    .join(order.member, member) //join(조인 대상, 별칭으로 사용할 쿼리 타입)
    .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 절에 여러 조건 사용///////////////
QOrder order = QOrder.order;
QMember member = QMember.member;
 
query.from(order, member)
    .where(order.member.eq(member))
    .list(order);
 
/////서브 쿼리 사용//////////////
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);
  • 프로덕션과 결과 반환
/////프로덕션 대상이 하나면 해당 타입으로 반환한다
QItem item = QItem.item;
List<String> result = 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<Tuple> result = query.from(item.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));
}
  • 쿼리 결과를 엔티티가 아닌 특정 객체로 받고 싶을 때 프로퍼티, 필드 , 생성자 3가지로 접근할 수 있다.
/////프로퍼티 접근(Setter)
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
    Projections.bean(ItemDTO.class, item.name.as("username"), item.price));
//Projections.bean() 메소드는 setter를 사용해서 값을 채운다.
 
 
//필드로 직접 접근
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
    Projection.fields(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() 메소드는 생성자를 사용한다. 물론 지정한 프로젝션과 파라미터 ㅜㄴ서가 
//같은 생성자가 필요하다.
  • 동적 쿼리

com.mysema.query.BooleanBuilder를 사용하면 특정 조건에 따른 동적 쿼리를 편리하게 생성할 수 있다.

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);

 

Leave a Comment