JPA 스터디 (10장)

soon world·2021년 8월 18일
0

JPA

목록 보기
8/8

10장 객체지향 쿼리 언어

  • JPA를 다루는 개발자라면 JPQL 을 필수로 학습 해야 한다!

10.1 객체지향 쿼리 언어 소개

  • JPQL 소개
    • 엔티티 객체를 조회하는 객체지향 쿼리
    • SQL 을 추상화 해서 특정 데이터베이스에 의존하지 않음
    • SQL 보다 간결
  • Criteria 소개
    • JPQL을 생성하는 빌더 클래스이다.
    • 장점 중 하나로 문자가 아닌 query.select(m).where(...) 처럼 프로그래밍 코드로 JPQL을 작성 할 수 있음
    • 컴파일 시점에 오류를 발견할 수 있다.
    • 단점으로는 복잡하고 장황하기에 사용하기 불편하며 Criteria로 작성한 코드는 한눈에 들어오지 않는다는 점이다.
  • QueryDSL 소개
    • Criteria 와 마찬가지로 JPQL을 생성하는 빌더 역할을 한다.
  • 네이티브 SQL
    • SQL을 직접 사용할 수 있는 기능

10.2 JPQL

  • 기본 문법과 쿼리 API

    • SELECT 문
      • 대소문자 구분, 반면 SELECT, FROM 등 JPQL 키워드는 대소문자 구분 안함
      • 엔티티 이름을 사용(기본값인 클래스 명을 엔티티 명으로 사용하길 권함)
      • 별칭 필수, 별칭 없을 시 오류 발생
    • TypeQuery , Query
      • 조회 대상 타입이 명확할 경우 TypeQuery, 그렇지 않을 경우 Query 사용
        • 'SELECT m FROM Member m' 의 경우 반환타입이 Member로 명확하기 때문에 TypeQuery를 사용 한다.
        • 'SELECT m.username, m.age from Member m' 일 경우 조회 타입이 String 과 Integer 두개로 명확하지 않으므로 이럴 때 Query 를 사용 한다.
    • 결과 조회
      • query.getResultList() : 결과를 예제로 반환, 없을 시 빈 컬렉션 반환
      • query.getSingleResult() : 결과가 정확히 하나 일때 사용, 하나 아닐 시 예외 발생
  • 파라미터 바인딩

    • 이름 기준 파라미터
      • 앞에 : 를 사용 한다.
    • 위치 기준 파라미터
      • ? 다음에 위치 값을 줘서 사용 함
      • 위치 기준 파라미터 방식 보단 이름 기준 파라미터 방식이 더 명확함
  • 프로젝션

    • SELECT 절에 조회할 대상을 지정하는 것

    • 엔티티 프로젝션

      • SELECT m FROM Member m // SELECT m.username FROM Member m
      • 조회한 엔티티는 영속성 컨텍스트에서 관리
    • 임베디드 프로젝션

      • 임베디드 타입은 값 타입이므로 영속성 컨텍스트에서 관리되지 않음
    • 여러 값 조회

      • 프로젝션에 여러 값 선택 시 TypeQuery를 사용할 수 없고 Query 를 사용
    • NEW 명령어

      TypedQuery<UserDTO> query = em.createQuery("SELECT new UserDTO(m.username, m.age)
      FROM Member m", UserDTO.class);
      
      //SELECT 다음 NEW 명령어 사용 시 반환받을 클래스를 지정 할 수 있어서
      // 이 클래스의 생성자에 JPQL 조회 결과를 넘겨 줄 수 있다.
    • 페이징 API

      • setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
      • setMaxPesults(int maxResult) : 조회할 데이터 수
    • 집합

      • COUNT,MAX,MIN,AVG,SUM
      • GROUP BY, HAVING
        • GROUP BY : 그룹별 통계 구할 때 사용
        • HAVING : GROUP BY 로 그룹화 된 데이터를 기준으로 필터링
    • 정렬(ORDER BY)

      • ASC : 오름차순(default)
      • DESC : 내림차순
  • JPQL 조인

    • 내부 조인

      String query = "SELECT m FROM Member m JOIN m.team t WHERE t.name = :teamName";
      
      //JPQL 조인을 SQL 조인 처럼 사용 시 문법 오류 발생
      //ex) FROM Member m JOIN Team t -> 문법 오류
    • 외부 조인

      SELECT m ,t
      FROM Member m LEFT JOIN m.team t on t.name = 'A'
    • 컬렉션 조인

      • 일대다 관계나 다대다 관계처럼 컬렉션을 사용 하는 곳에서 조인 하는것을 컬렉션 조인이라 함
  • 페치 조인

    • JPQL 에서 성능 최적화를 위해 제공하는 기능
    • join fetch
    • 엔티티 페치 조인
      • select m from Member m join fetch m.team
      • JPQL 조인과는 다르게 m.team 다음에 별칭이 없다.
      • seelct m 으로 회원 엔티티만 조회 했지만 실제로는 회원과 연관된 팀도 조회 됨
    • 컬렉션 페치 조인
      • 엔티티 페치조인과 거의 비슷함, 컬렉션을 페치 조인했다는 내용
    • 페치 조인과 일반 조인의 차이
      • JPQL은 결과를 반환 시 연관관계 까지 고려햐진 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할뿐!, 페치조인을 사용 시 연관된 엔티티도 함께 조회한다.
    • 페치 조인의 특징 및 한계
      • 페치 조인을 사용 시 한번의 SQL로 연관된 엔티티들을 함께 조회할 수 있어 SQL 호출 횟수를 줄여 성능을 최적화 할 수 있다. (글로벌 로딩 전략은 지연로딩 으로 가고 최적화 필요 시 페치 조인 적용)
      • 페치 조인 대상에는 별칭을 줄 수 없음
      • 둘 이상의 컬렉션 페치 불가
      • 컬렉션 페치 조인 시 페이징 API 사용 불가
  • 경로 표현식

    • 상태 필드 : 단순히 값을 저장하기 위한 필드
    • 연관 필드 : 연관관계를 위한 필드, 임베디드 타입 포함
      • 단일 값 연관 필드 : @ManyToOne , @OneToOne, 대상이 엔티티
      • 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션
  • 서브 쿼리

    • EXISTS
      • 서브쿼리에 결과가 존재하면 참, NOT 은 반대
    • ALL | ANY |SOME
      • 비교 연산자와 같이 사용 { = | > | ≥ | ≤ | <> }
      • ALL : 조건을 모두 만족 시 참
      • ANY 혹은 SOME : 둘은 같은 의미, 조건이 하나라도 만족 시 참
    • IN
      • 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
  • ...

  • 동적 쿼리

    • em.create("select...") 처럼 JPQL을 문자로 완성하여 직접 넘기는 것
    • 런타임에 특정 조건에 따라 JPQL 을 동적으로 구성 할 수 있다.
  • 정적 쿼리

    • 미리 정의한 쿼리에 이름을 부여하여 필요할 때 사용할 수 있는데 이것을 Name쿼리 (정적쿼리) 라고 한다. 한번 정의시 변경 불가

10.3 Criteria

  • JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API !.. (어려워서 QueryDSL 에 밀리고 있는 상태..)

  • 맛만 봐보자!

    • Criteria API 는 javax.persistence.criteria 패키지에 존재

      //JPQL : select m from Member m
      
      CriteriaBuilder cb = em.getCriteriaBuilder(); // Criteria 쿼리 빌더
      
      //Criteria 생성, 반환 타입 지정
      CriteriaQuery<Member> cq = cb.createQuery(Member.class);
      
      Root<Member> m = cq.from(Member.class); //FROM 절
      cq.select(m); //SELECT 절
      
      TypedQuery<Member> query = em.createQuery(cq);
      List<Member> members = query.getResultList();
  • 위 예제 처럼 Criteria 문법만 보고서 어떠한 JPQL이 나올지 예측하기가 QueryDSL 에 비해 까다롭고 직관적이지 않다..! 나같아도 Criteria 보단 QueryDSL 을 선호..할 수 밖에 없을 거같다.

10.4 QueryDSL

  • Criteria 의 가장 큰 단점인 너무 복잡하고 어렵다는 걸 대체 할 수 있도록 쿼리를 문자가 아닌 코드로 작성해도 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발 할 수 있는 프로젝트

  • 시작

    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);
    }
  • 기본 Q 생성

    • 쿼리 타입은 아래와 같이 기본 인스턴스를 보관 하고 있음, 하지만 같은 엔티티를 조인 하거나 같은 엔티티를 서브쿼리에 사용하면 같은 별칭이 사용되므로 이 때는 별칭을 직접 지정하여 사용 해야 함

      public class QMember extends EntityPathBase<Member> {
      	public static final QMember member = new QMember("member1");
        ...
      }	
      //쿼리 타입 사용
      QMember qMember = new QMember("m"); //직접 지정
      QMember qMember = QMember.member; // 기본 인스턴스 사용
  • 검색 조건 쿼리

    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 //위치 기준 파라미터 바인딩
    • QueryDSL 의 where 절에는 and 나 or 사용 가능

      .where(item.name.eq("좋은상품") , item.price.gt(20000)) // 이때는 and 연산이 됨
      
      item.price.between(10000,20000); // 10000~20000 사이
      item.name.contains("상품1"); // 상품1 이라는 이름을 포함한 상품
      item.name.startswith("고급"); // 이름이 고급으로 시작 하는 상품
  • 결과 조회

    • uniqueResult() : 조회 결과가 한 건일 때 사용, 조회 결과 없을 시 null 반환, 결과 하나 이상이면 NonUniqueResultException 예외 발생
    • singleResult() : uniqueResult()와 같지만 결과가 하나 이상이면 처음 데이터 반환
    • 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);
    
    ---
    
    QueryModifiers queryModifiers = new QueryModifiers(20L, 10L); //limit, offset
    
    List<Item> list = 
    	query.from(item)
    	.restrict(queryModifiers) 
    	.list(item);
    
    ---
    //실제 페이징 처리 시 검색된 전체 데이터 수를 알아야 한다. 이 때 listResults() 사용
    
    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(); // 조회된 데이터
    
    //listResults() 사용 시 전체 데이터 조회를 위한 count 쿼리를 한번 더 실행 한다.
    // 그리고 SearchResults 를 반환하는데 이 객체에서 전체 데이터 수를 조회할 수 있음
  • 그룹

    query.from(item)
    	.groupBy(item.price)
    	.having(item.price.gt(10000)) //조건
    	.list(item);
  • 조인

    • innerJoin, leftJoin, rightJoin, fullJoin 사용 및 JPQL 의 성능최적화를 위한 fetch 조인도 사용할 수 있다.

    • join(조인대상, 별칭으로 사용할 쿼리 타입)

      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);
      
      ---
      //fetch 조인
      query.from(order)
      	.innerJoin(order.member, member).fetch()
      	.leftJoin(order.orderItems, orderItem).fetch()
      	.list(order);
      
      ---
      //from 절에 여러 조건 사용
      qQOrder order = QOrder.order;
      QMember member = QMember.member;
      
      query.from(order, member)
      	.where(order.member.eq(member))
      	.list(order);
  • 서브 쿼리

    //com.mysema.query.jpa.JPASubQuery를 생성 하여 사용 
    //서브쿼리 결과가 하나면 unique() , 여러 건이면 list() 사용
    
    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);
  • 프로젝션 결과 반환

    • select 절에 조회 대상을 지정 하는 것( 저번 시간 내용)

    • 프로젝션 대상이 하나

      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);
        
        for(Tuple tuple : result) {
        	System.out.println("name = " + tuple.get(item.name));
        	System.out.println("price = " + tuple.get(item.price)); 
        }
  • 빈 생성 (com.mysema.query.types.Projections 사용)

    • 특정 객체(ex. DTO) 로 받고 싶을 때 사용

    • 프로퍼티 접근

    • 필드 직접 접근

    • 생성자 사용

      QItem item = QItem.item;
      List<ItemDTO> result = query.from(item).list(
      	projections.bean(ItemDTO.class, item.name.as("username"), item.price); //Setter 사용
      
      query.from(item).list(
      	projections.fileds(ItemDTO.class, item.name.as("username"), item.price); //필드 직접 접근
      
      query.from(item).list(
      	projections.constructor(ItemDTO.class, item.name, item.price) // 생성자 사용
      
      ----
      DISTINCT
      -> query.distinct().from(item)... 으로 사용
  • 수정,삭제,배치 쿼리

    • JPQL 배치 처럼 영속성 컨텍스트를 무시하고! 데이터베이스를 직접 쿼리한다.

      //수정 배치 쿼리
      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();
      
      //수정 시 JPAUpdateClause 사용
      //삭제 시 JPADeleteClause 사용
      
  • 동적 쿼리 : com.mysema.query.BooleanBuilder 사용 시 특정 조건에 따른 동적 쿼리 생성

  • 메소드 위임 : @QueryDelegate(쿼리타입,필요한 파라미터..) 사용 시 쿼리 타입에 검색 조건을 직접 정의 가능

    public class ItemExpression {
    
    	@QueryDelegate(Item.class)
    	public static BooleanExpression isExpensive(QItem item, Integer price) {
    		return item.price.gt(price);
    	}
    
    }

10.5 네이티브 SQL

  • 특정 데이터베이스만 지원 하는 함수, 문법 등
  • 인라인 뷰 ( From 절에서 사용하는 서브쿼리), UNION, INTERSECT
  • 스토어드 프로시저
  • 실제 네이티브 SQL 이라고 생각하면 된다.

10.6 객체지향 쿼리 심화

10.7 정리

  • JPQL은 SQL을 추상화 했으므로 특정 데이터베이스 기술에 의존하지 않음
  • Criteria 나 QueryDSL은 JPQL을 만들어주는 빌더역할을 할뿐! 핵심은 JPQL을 알아야 한다.
  • Criteria , QueryDSL 을 사용 시 동적쿼리를 편하게 작성 할 수 있다.
  • 최대한 JPQL을 사용하고, 그래도 방법이 없을 때 네이티브 SQL 을 사용하자
  • JPQL은 대량의 데이터 수정,삭제 하는 벌크 연산을 지원 한다.
profile
Hello SoonWorld

0개의 댓글