[JPA BASIC] 5. Query

Query 언어를 알아보자!

JPA는 데이터를 관리하고 검색하는 데 있어서 강력한 기능을 제공하지만, 복잡한 쿼리를 처리하는 데에는 한계가 있을 수 있음. 이러한 한계를 극복하기 위해 JPA는 다양한 쿼리 방법을 지원하고 있다.

종류

  • JPQL
  • JPA Criteria
  • QueryDSL
  • 네이티브 SQL
  • JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용

JPQL

  • 테이블이 아닌 엔티티 객체를 대상으로 검색
  • SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
  • 동적 쿼리의 어려움: JPQL은 문자열 기반으로 쿼리를 작성하기 때문에, 동적 쿼리를 생성하는 과정에서 오류가 발생하기 쉽다. 이 때문에 복잡한 비즈니스 로직에 따라 쿼리를 조건적으로 변경해야 하는 상황에서는 코드가 복잡해지고 오류를 발견하기 어려울 수 있다.
  • 별칭은 필수(m) (as는 생략가능)
//검색
String jpql = "select m From Member m where m.name like ‘%hello%'"; 
List<Member> result = em.createQuery(jpql, Member.class)
          .getResultList();

Criteria

  • 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
  • JPQL 빌더 역할
  • JPA 공식 기능
  • 단점: 너무 복잡하고 실용성이 없다.
  • Criteria 대신에 QueryDSL 사용 권장
//Criteria 사용  준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class); 

//루트  클래스 (조회를  시작할  클래스)
Root<Member> m = query.from(Member.class); 

//쿼리  생성 
CriteriaQuery<Member> cq =
query.select(m).where(cb.equal(m.get("username"), kim)); 
List<Member> resultList = em.createQuery(cq).getResultList();

QueryDSL

  • 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
  • JPQL 빌더 역할
  • 컴파일 시점에 문법 오류를 찾을 수 있음
  • 동적쿼리 작성 편리함
  • 단순하고 쉬움
  • 실무 사용 권장
//JPQL
//select m from Member m where m.age > 18 
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;

List<Member> list =
      query.selectFrom(m) 
           .where(m.age.gt(18)) 
           .orderBy(m.name.desc()) 
           .fetch();

네이티브 SQL 소개

  • JPA가 제공하는 SQL을 직접 사용하는 기능
  • JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능
  • 예) 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트
String sql =
    "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'"; 

List<Member> resultList =
            em.createNativeQuery(sql, Member.class).getResultList();

JDBC 직접 사용, SpringJdbcTemplate 등

  • JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스등을 함께 사용 가능
  • 단 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요
  • 예) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시

JPQL - 기능

JPQL - 문법

  • select m from Member as m where m.age > 18
  • 엔티티와 속성은 대소문자 구분O (Member, age)
  • JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
  • 엔티티 이름 사용, 테이블 이름이 아님(Member)
  • 별칭은 필수(m) (as는 생략가능)
  • COUNT(m), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age), GROUP BY, HAVING, ORDER BY 사용 가능

TypeQuery, Query

  • TypeQuery: 반환 타입이 명확할 때 사용
  • Query: 반환 타입이 명확하지 않을 때 사용
// TypedQuery
TypedQuery<Member> query =
    em.createQuery("SELECT m FROM Member m", Member.class);

// Query
Query query =
    em.createQuery("SELECT m.username, m.age from Member m");

결과 조회

  • query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
    • 결과가 없으면 빈 리스트 반환
  • query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환(되도록 query.getResultList()을 쓰자)
    • 결과가 없으면: javax.persistence.NoResultException
    • 둘 이상이면: javax.persistence.NonUniqueResultException

바인딩

// 방법 1
Query query = entityManager.createQuery("SELECT m FROM Member m where m.username=:username");
query.setParameter("username", usernameParam);

// 방법 2
Query query = entityManager.createQuery("SELECT m FROM Member m WHERE m.username = ?1");
query.setParameter(1, usernameParam);

프로젝션 방법 - SELECT 절에 조회할 대상(컬럼)을 지정하는 것

  1. Query 타입으로 조회
  2. Object[] 타입으로 조회
  3. new 명령어로 조회
    • 단순 값을 DTO로 바로 조회
      • (SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m)
    • 패키지 명을 포함한 전체 클래스 명 입력
    • 순서와 타입이 일치하는 생성자 필요

페이징 API

  • JPA는 페이징을 다음 두 API로 추상화
  • setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
//페이징  쿼리
String jpql = "select m from Member m order by m.name desc"; 
List<Member> resultList = em.createQuery(jpql, Member.class)
        .setFirstResult(10) 
        .setMaxResults(20) 
        .getResultList();

조인

// 내부 조인:
SELECT m FROM Member m [INNER] JOIN m.team t
// 외부 조인:
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
// 세타 조인:
select count(m) from Member m, Team t where m.username = t.name
// ON절을 활용한 조인(JPA 2.1부터 지원)
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A' 
// 서브 쿼리
select m from Member m where m.age > (select avg(m2.age) from Member m2) 

서브 쿼리

  • [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
    • {ALL | ANY | SOME} (subquery)
    • ALL 모두 만족하면 참
    • ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
  • [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
// 팀A 소속인 회원
select m from Member m where exists (select t from m.team t where t.name = '팀A')
// 전체 상품 각각의 재고보다 주문량이 많은 주문들 
select o from Order o where o.orderAmount > ALL (select p.stockAmount from Product p)
// 어떤 팀이든 팀에 소속된 회원 
select m from Member m where m.team = ANY (select t from Team t)

서브 쿼리 한계

  • JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
  • SELECT 절도 가능(하이버네이트에서 지원)
  • FROM 절의 서브 쿼리는 JPQL에서 불가능
    • 조인으로 풀 수 있으면 풀어서 해결
  • 하이버네이트6 부터는 FROM 절의 서브쿼리를 지원한다

JPQL

  • EXISTS, IN
  • AND, OR, NOT
  • =, >, >=, <, <=, <>
  • BETWEEN, LIKE, IS NULL

case

  • 기본 case 식 사용 가능
  • COALESCE: 하나씩 조회해서 null이 아니면 반환
  • NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
// 사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username,'이름 없는 회원') from Member m 
// 사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m

기본 함수

  • CONCAT
  • SUBSTRING
  • TRIM
  • LOWER, UPPER
  • LENGTH
  • LOCATE
  • ABS, SQRT, MOD
  • SIZE, INDEX(JPA 용도)

사용자 정의 함수 호출

  • 하이버네이트는 사용전 방언에 추가해야 한다.
    • 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한 다.
select function('group_concat', i.name) from Item i

단일 값 연관 경로

묵시적 내부 조인(inner join) 발생, 탐색O (묵시적 조인 : join 키워드를 사용 안함 )

// JPQL: 
select o.member from Order o
// SQL:
select m.*
  from Orders o 
  inner join Member m on o.member_id = m.id

경로 표현식

select o.member.team from Order o // -> 성공
select t.members from Team // -> 성공
select t.members.username from Team t // -> 실패
select m.username from Team t join t.members m // -> 성공

경로 탐색을 사용한 묵시적 조인 시 주의사항

  • 항상 내부 조인
  • 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야함
  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시 적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 줌

실무 조언

  • 가급적 묵시적 조인 대신에 명시적 조인 사용
  • 조인은 SQL 튜닝에 중요 포인트
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

JPQL - 페치 조인

  • SQL 조인 종류X
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • join fetch 명령어 사용
  • 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
// 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)
// SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT
// [JPQL]
select m from Member m join fetch m.team
// [SQL]
SELECT M.*, T.* FROM MEMBER M 
INNER JOIN TEAM T ON M.TEAM_ID=T.ID 

페치 조인

String jpql = "select m from Member m join fetch m.team"; 
List<Member> members = em.createQuery(jpql, Member.class) 
                         .getResultList();
for (Member member : members) {
    //페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
    System.out.println("username = " + member.getUsername() + ", " + 
                       "teamName = " + member.getTeam().name()); 
}

컬렉션 페치 조인

// 일대다 관계, 컬렉션 페치 조인
// [JPQL] 
select t
from Team t join fetch t.members 
where t.name = '팀A'
// [SQL]
SELECT T.*, M.* 
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID 
WHERE T.NAME = '팀A'
String jpql = "select t from Team t join fetch t.members where t.name = '팀A'" 
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
for(Team team : teams) {
    System.out.println("teamname = " + team.getName() + ", team = " + team); 
    for (Member member : team.getMembers()) {
        //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
        System.out.println(-> username = " + member.getUsername()+ ", member = " + member); 
  }
}

페치 조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령
  • JPQL의 DISTINCT 2가지 기능 제공
    • 1. SQL에 DISTINCT를 추가
    • 2. 애플리케이션에서 엔티티 중복 제거(뻥튀기 데이터를 지워준다)
  • 하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용된다.
select distinct t  
from Team t join fetch t.members  
where t.name = 팀A

alt text

페치 조인과 일반 조인의 차이

  • JPQL은 SELECT 절에 지정한 엔티티만 조회 한다.
  • 페치 조인을 사용할 때는 연관된 엔티티도 함께 조회 한다. (즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

페치 조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
    • 하이버네이트는 가능, 가급적 사용X
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
    • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
  • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
    • @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
  • 최적화가 필요한 곳은 페치 조인 적용

일대다 관계 팁!

지연 로딩이나 일반 조인으로 데이터를 가져올때 한번에 in절을 사용하여 가져오는 방법
배치 사이즈란? 한 번에 로드할 수 있는 연관 데이터의 개수를 의미한다.

// 엔티티에서 직접 설정
@OneToMany
@BatchSize(size = 10)
private List<Order> orders = new ArrayList<>();

// properties
spring.jpa.properties.hibernate.default_batch_fetch_size=10

// yml
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 10
// 사용 예제
@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    @ManyToOne(fetch = FetchType.LAZY)
    private Author author;
    // getter와 setter 생략
}

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    // getter와 setter 생략
}

public List<Book> findAllBooksWithAuthors() {
    return entityManager.createQuery(
        "SELECT b FROM Book b JOIN FETCH b.author", Book.class)
    .getResultList();
}

Named 쿼리

애플리케이션 로딩 시점에 초기화 및 검증
한 마디로 미리 정의해 두고 쓰기

// 어노테이션
@Entity
@NamedQuery(
        name = "Member.findByUsername",
        query="select m from Member m where m.username = :username") 
public class Member {
    ... 
}

List<Member> resultList =
  em.createNamedQuery("Member.findByUsername", Member.class) 
        .setParameter("username", "회원1")
        .getResultList();
<!-- xml방법 -->

<!-- [META-INF/persistence.xml] -->
<persistence-unit name="jpabook" >
  <mapping-file>META-INF/ormMember.xml</mapping-file>

<!-- [META-INF/ormMember.xml] -->
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
  <named-query name="Member.findByUsername"> 
  <query><![CDATA[
              select m 
              from Member m
              where m.username = :username 
          ]]></query>
  </named-query>
  <named-query name="Member.count">
  <query>select count(m) from Member m</query> 
  </named-query>
</entity-mappings>

벌크 연산

  • 쿼리 한 번으로 여러 테이블 변경하는 방법 지원(ex : update … where p.age > 20)
  • 벌크 연산은 데이터베이스에서 직접 진행하기 때문에 영속성 컨텍스트와 싱크가 안맞을 수 있다.
    • 싱크 방법 두가지
      • 1) 벌크 연산을 먼저 실행
      • 2) 벌크 연산 후 clear() 후 다시 불러와 사용한다.
// 벌크 연산
int resultCount = em.createQuery("update Member m set m.age = 20").executeUpdate();

// clear
em.clear();

// clear를 하지 않으면 반영 안된다.
Member findMember = em.find(Member.class, "id");

© 2023 Lee. All rights reserved.