[JPA 기본] 객체지향 쿼리 언어 (JPQL)
포스트
취소

[JPA 기본] 객체지향 쿼리 언어 (JPQL)

객체지향 쿼리 언어 (JPQL)

JPA는 다양한 쿼리 방법을 지원한다.

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

JPQL

  • JPA가 제공하는 SQL을 추상화한 객체 지향 쿼리 언어
  • JPA를 사용하면 엔티티 객체를 중심으로 개발한다.
    • JPQL은 엔티티 객체를 대상으로 쿼리를 생성한다.
  • DB의 모든 데이터를 객체로 변환해서 검색하는 것은 불가능하다.
    • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다.
  • SQL과 문법 유사하며 SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등을 지원한다.
  • 예시 (기본)
Member newMember = new Member();
newMember.setName("newMember");
newMember.setAge(21);
em.persist(newMember);

String query = "SELECT m FROM Member m";
List<Member> members = em.createQuery(query, Member.class).getResultList();
SELECT
    m 
FROM
    Member m */ select
        m1_0.id,
        m1_0.age,
        m1_0.username,
        m1_0.team_id,
        m1_0.type 
    from
        Member m1_0
  • 예시 (조건 검색)
Member newMember = new Member();
newMember.setName("newMember");
newMember.setAge(21);
em.persist(newMember);

String query = "select m from Member m where m.age > 18";
List<Member> members = em.createQuery(query, Member.class).getResultList();
select
    m 
from
    Member m 
where
    m.age > 18 */ select
        m1_0.id,
        m1_0.age,
        m1_0.username,
        m1_0.team_id,
        m1_0.type 
    from
        Member m1_0 
    where
        m1_0.age>18

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("name"), "kim"));
List<Member> Members = em.createQuery(cq).getResultList();
select
    m1_0.id,
    m1_0.age,
    m1_0.username,
    m1_0.team_id,
    m1_0.type 
from
    Member m1_0 
where
    m1_0.username=?

QueryDSL

  • 문자가 아닌 자바코드로 JPQL을 작성할 수 있다.
  • JPQL 빌더 역할을 한다.
  • 컴파일 시점에 문법 오류를 찾을 수 있다.
  • 동적 쿼리 작성하기에 편리하다.
  • 단순하고 쉽다.
  • 실무 사용 권장 (★)
  • 예시
JPAQueryFactory 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, USERNAME FROM MEMBER WHERE USERNAME = 'kim'"; 
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();

JDBC 직접 사용, SpringJdbcTemplate 등

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

기본 문법과 쿼리 API

  • JPQL은 객체지향 쿼리 언어다.
    • 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
  • JPQL은 SQL을 추상화해서 특정데이터베이스 SQL에 의존하지 않는다.
  • JPQL은 결국 SQL로 변환된다.

기본 문법

  • 엔티티명과 속성명은 대소문자를 구분한다.
    • 만약 엔티티명이 Member일 때 member라고 명시하면 오류가 발생한다.
  • JPQL 키워드는 대소문자를 구분하지 않는다.
    • SELECT, FROM, where 모두 사용가능한 키워드들이다.
  • 테이블명이 아닌 엔티티명을 사용한다.
  • 별칭은 필수로 설정해야 한다.
    • AS는 생략해도 된다.
  • 예시
    • select m from Member as m where m.age > 18
    • select m.id, m.age, m.username from Member as m where m.age > 18

TypeQuery과 Query

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

결과 조회 API

  • query.getResultList()
    • 결과가 하나 이상일 때는 리스트를 반환한다.
    • 결과가 없으면 빈 리스트를 반환한다.
  • query.getSingleResult()
    • 결과가 정확히 하나일 때 단일 객체를 반환한다.
    • 결과가 없는 경우
      • javax.persistence.NoResultException 발생
    • 둘 이상인 경우
      • javax.persistence.NonUniqueResultException 발생

파라미터 바인딩

이름 기준
  • 변수명을 기준으로 파라미터를 바인딩한다.
  • 예시
TypedQuery<Member> query2 = em.createQuery("select m from Member m where m.age > :age", Member.class);
query2.setParameter("age", 18);
List<Member> members2 = query2.getResultList();
위치 기준
  • 위치를 기준으로 파라미터를 바인딩한다.
  • 위치는 절대적인 것이 아니기 때문에 절대 사용하면 안된다.
  • 예시
TypedQuery<Member> query2 = em.createQuery("select m from Member m where m.age > ?1", Member.class);
query2.setParameter(1, 18);
List<Member> members2 = query2.getResultList();

프로젝션 (SELECT)

  • 프로젝션이란?
    • SELECT 절에 조회할 대상을 지정하는 것
  • 프로젝션 대상
    • 엔티티
      • 예시
        • Member는 회원 엔티티다.
        • Team은 팀 엔티티다.
        • SELECT m FROM Member m
        • SELECT m.team FROM Member m
    • 임베디드 타입
      • 예시
        • address는 주소에 대한 임베디드 타입이다.
        • SELECT m.address FROM Member m
    • 스칼라 타입(숫자, 문자등 기본 데이터 타입)
      • 예시
        • username은 회원명을 가리키는 String이다.
        • age는 회원의 나이를 가리키는 int다.
        • SELECT m.username, m.age FROM Member m
  • DISTINCT 키워드로 중복을 제거할 수 있다.
  • 여러 가지 값을 조회할 때의 처리 방법은 3가지가 있다.
    • Query 타입으로 조회
    • Object[] 타입으로 조회
    • new 명령어로 조회
      • 단순 값을 VO(DTO)로 바로 조회
        • 예시
          • SELECT new com.example.demo.vo.step9.UserVO(m.username, m.age) FROM Member m
      • 패키지 명을 포함한 전체 클래스명을 입력해야 한다.
      • 순서와 타입이 일치하는 생성자가 필요하다.

페이징

  • JPA는 페이징을 다음 두 API로 추상화한다.
  • setFirstResult(int startPosition)
    • 조회 시작 위치
    • 0부터 시작한다.
  • setMaxResults(int maxResult)
    • 조회할 데이터 수
  • 예시
//회원 15명 추가
for (int i = 1; i <= 15; i++) {
    Member member = new Member();
    member.setName("member" + i);
    member.setAge(20 + i);
    em.persist(member);
}

em.flush();
em.clear();

List<Member> members =
    em.createQuery("SELECT m FROM Member m order by m.age desc", Member.class)
    .setFirstResult(0)
    .setMaxResults(10)
    .getResultList();

for (Member data : members) {
    System.out.println("member : " + data);
}

tx.commit();

조인

조인 종류

내부 조인
  • 예시
    • 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부터 지원한다.
  • 종류
    • 조인 대상 필터링
    • 연관관계 없는 엔티티 외부 조인
      • 하이버네이트 5.1부터 지원한다.
조인 대상 필터링
  • 예시
    • 전제 조건
      • 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
    • JPQL
      • SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
    • SQL
      • SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'
연관관계 없는 엔티티 외부 조인
  • 예시
    • 전제 조건
      • 회원의 이름과 팀의 이름이 같은 대상 외부 조인
    • JPQL
      • SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
    • SQL
      • SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name

서브 쿼리

  • JPA에서는 서브 쿼리도 지원한다.
  • 서브 쿼리도 메인 쿼리랑 마찬가지로 엔티티를 기준으로 작성해야 한다.
  • 관련 SQL 함수
    • EXISTS (subquery)
      • 서브쿼리에 결과가 존재하면 참
    • NOT EXISTS (subquery)
      • 서브쿼리에 결과가 존재하지 않으면 참
    • ALL (subquery)
      • 모두 만족하면 참
      • 예시
        • 나이 > ALL (10, 20, 30)이라면 나이 > 10, 나이 > 20, 나이 > 30을 모두 만족해야 한다.
    • ANY (subquery) or SOME (subquery)
      • 조건을 하나라도 만족하면 참
      • 예시
        • 나이 > ANY (10, 20, 30)이라면 나이 > 10, 나이 > 20, 나이 > 30 중 하나라도 만족하면 된다.
    • IN (subquery)
      • 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
    • NOT IN (subquery)
      • 서브쿼리의 결과 중 같은 것이 하나도 없으면 참
  • JPA 서브 쿼리 한계
    • JPA의 서브 쿼리는 WHERE, HAVING 절에서만 사용할 수 있다.
    • SELECT절에서도 서브 쿼리를 사용할 수 있다.
      • 하이버네이트에서 지원
    • 현재 JPQL에서는 FROM절에 서브 쿼리를 사용할 수 없다.
      • 조인으로 풀 수 있으면 풀어서 해결해야 한다.
      • 그러나 하이버네이트6부터는 FROM절에서도 서브 쿼리를 사용할 수 있다.

JPQL 타입 표현과 기타식

JPQL 타입 표현

  • 문자
    • 'HELLO'
    • 'She''s'
  • 숫자
    • Double
      • 10D
    • Float
      • 10F
    • Long
      • 10L
  • Boolean
    • TRUE
    • FALSE
  • ENUM
    • com.example.demo.MemberType.Admin
    • 패키지명을 포함해야 한다.
    • 파라미터 바인딩 방식을 쓰는 게 편하다.
  • 엔티티 타입
    • TYPE(m) = Member
    • 상속 관계에서 사용한다.
    • WHERE절에서 사용한다.
    • 자주 사용되지는 않지만 다형성 쿼리에서는 쓰는 경우가 있다.

기타식

  • SQL과 문법이 같은 식
  • EXISTS, IN
  • AND, OR, NOT
  • =, >, >=, <, <=, <>
  • BETWEEN, LIKE, IS NULL

조건식 (CASE 등등)

기본 CASE식

  • 조건식의 결과가 참인 경우의 값을 반환한다.
select
    case when m.age <= 10 then '학생요금'
        when m.age >= 60 then '경로요금'
        else '일반요금'
    end
from
    Member m

조건 CASE식

  • 값이 일치하는 경우의 값을 반환한다.
select
    case t.name 
        when '팀A' then '인센티브110%'
        when '팀B' then '인센티브120%'
        else '인센티브105%'
    end
from
    Team t

COALESCE

  • 인자로 지정한 요소들을 하나씩 비교해서 NULL이 아니면 NULL이 아닌 첫번째 값을 반환한다..
  • 만약 COALESCE(A, B, C)인 경우에 A와 B가 NULL이라면 C를 반환한다.
select
    coalesce(m.username, '이름 없는 회원')
from
    Member m

NULLIF

  • 두 값이 같으면 null을 반환한다.
  • 두 값이 다르면 첫번째 값을 반환한다.
select
    NULLIF(m.username, '관리자')
from
    Member m

JPQL 함수

JPGQ 기본 함수

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

사용자 정의 함수 호출

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

경로 표현식

경로 표현식이란?

  • .(점) 연산자를 통해 객체 그래프를 탐색하는 것
select
    m.username /* 상태 필드 */
from
    Member m 
join
    m.team t /* 단일 값 연관 필드 */
join
    m.orders o /* 컬렉션 값 연관 필드 */
where t.name = '팀A'

관련 용어

  • 상태 필드 (state field)
    • 단순히 값을 저장하기 위한 필드
    • 예시 : m.username
  • 연관 필드 (association field)
    • 연관관계를 위한 필드
      • 단일 값 연관 필드
        • 대상이 엔티티인 경우에 해당한다.
          • @ManyToOne, @OneToOne
          • 예시 : m.team
      • 컬렉션 값 연관 필드
        • 대상이 컬렉션인 경우에 해당한다.
          • @OneToMany, @ManyToMany
          • 예시 : m.orders

경로 표현식의 특징

  • 상태 필드 (state field)
    • 이름이나 나이처럼 특정 값을 나타내는 상태 필드는 더 이상 경로 탐색을 할 수 없다.
  • 단일 값 연관 경로
    • 단일 값 연관 필드가 조회 대상에 포함될 경우에 묵시적 내부 조인이 발생한다.
      • inner join을 의미한다.
    • 추가 탐색이 가능하다.
  • 컬렉션 값 연관 경로
    • 컬렉션 값 연관 필드가 조회 대상에 포함될 경우에 묵시적 내부 조인이 발생한다.
    • 컬렉션 자체를 가리키는 것이기 때문에 추가 탐색이 불가능하다.
    • FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색할 수 있다.
      • 예시 : select m.username from team t join t.members m

명시직 조인

  • join 키워드 직접 사용하여 조인한 경우를 의미한다.
    • 예시 : select m from Member m join m.team t

묵시적 조인

  • 경로 표현식에 의해 묵시적으로 조인이 발생하는 경우를 의미한다.
    • 내부 조인만 가능
    • 예시 : select m.team from Member m

경로 표현식 - 예제

  • 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 절에서 사용하지만 묵시적 조인으로 인해 FROM (JOIN) 절에 영향을 준다.

TIP

  • 가급적 묵시적 조인 대신에 명시적 조인을 사용해야 한다.
  • 조인은 SQL 튜닝에서 매우 중요한 요소다.
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵다.
    • 추후 유지보수에 매우 곤란함을 제공하기도 한다.

패치 조인

  • 실무에서 매우 중요한 기능이다.
  • SQL 조인 종류에 포함되지 않는다.
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
    • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다.
  • join fetch 명령어를 통해 사용한다.
    • [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
  • JPQL은 결과를 반환할 때 연관관계를 고려하지 않는다.
    • SELECT 절에 지정한 엔티티만 조회한다.
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회한다.
    • 즉시 로딩으로 동작한다.
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념이다.
  • N + 1 문제를 해결할 때 많이 사용된다.
    • N + 1이란 최초 1번 엔티티 A의 목록을 조회하고 A의 목록에 대해 반복문을 돌리면서
      해당 A에 관련된 엔티티 B를 조회할 때 해당하는 B가 영속성 컨텍스트에 존재하지 않아서
      SQL을 새로 실행하는 경우가 최대 N번 (A의 개수를 의미) 발생 할 수 있다는 것을 의미한다.
  • 모든 것을 페치 조인으로 해결할 수 는 없다.
  • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면,
    페치 조인보다는 일반 조인으로 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.

엔티티 페치 조인

  • 특정 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
  • 페치 조인으로 함께 조회하기 때문에 즉시 로딩이 된다.
  • 예시
    • 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
      • 회원과 팀에 대해서 모두 조회한다.

컬렉션 페치 조인

  • 일대다 관계, 컬렉션 페치 조인
  • 예시
    • JPQL
      • select t from Team t join fetch t.members where t.name = 'Team A'
    • SQL
      • SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = 'Team A'

페치 조인과 DISTINCT

  • SQL의 DISTINCT
    • 중복된 결과를 제거하는 명령
    • SQL에서는 DISTINCT를 추가해도 데이터가 조금이라도 다르면 중복이 제거되지 않는다.
  • JPQL의 DISTINCT
    • SQL에 DISTINCT를 추가한다.
    • 애플리케이션에서 엔티티 중복을 제거한다.
      • 같은 식별자를 가진 엔티티를 제거한다.
    • 하이버네이트6 변경 사항
      • DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 자동으로 중복 제거를 시도한다.
  • 예시
    • select distinct t from Team t join fetch t.members where t.name = 'Team A'

페치 조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
    • 하이버네이트는 가능하다.
    • 가급적 사용하지 않아야 한다.
    • 관례적으로 별칭을 주지 않는다.
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.
    • 한 번의 일대다도 조회되는 데이터의 수가 급증할 수 있는데 그 이상은 더욱 급증할 수 있다.
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
    • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다.
    • 방향을 뒤집어서 해결하는 방법도 있다.
      • 기존
        • select t from Team t join fetch t.members m
      • 변경
        • select m from Member m join fetch m.team t
    • @BatchSize를 활용하기도 한다.
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징을 한다.
      • 매우 위험한 방법이다.
  • 연관된 엔티티들을 SQL 한 번으로 조회하여 성능 최적화에 도움을 준다.
  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선권을 갖는다.
    • 글로벌 로딩 전략은 @OneToMany(fetch = FetchType.LAZY)같은 경우를 의미한다.
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩이다.
  • 최적화가 필요한 곳은 페치 조인 적용한다.
    • 예시 : N + 1

다형성 쿼리

  • 슈퍼 클래스와 서브 클래스 간의 관계에 대하여 조건을 설정한다.
  • 예시를 위해 Item이라고 하는 물품 클래스가 있고,
    Item 클래스를 상속받는 Album, Book, Movie 클래스가 있다고 가정한다.

type

  • 조회 대상을 특정 자식으로 한정한다.
  • 예시
    • 전제 조건
      • Item 중에 Book, Movie를 조회
    • JPQL
      • select i from Item i where type(i) IN (Book, Movie)
    • SQL
      • select i from i where i.DTYPE in ('B', 'M')

treat (JPA 2.1)

  • 자바의 타입 캐스팅과 유사한 방식
  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
  • FROM절과 WHERE절에서 사용한다.
    • 하이버네이트에서는 SELECT도 지원한다.
  • 예시
    • 전제 조건
      • 부모인 Item과 자식 Book이 있다.
    • JPQL
      • select i from Item i where treat(i as Book).author = 'kim'
    • SQL
      • select i.* from Item i where i.DTYPE = 'B' and i.author = 'kim'

엔티티 직접 사용

기본 키 값

  • JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
  • 예시
    • JPQL
      • select count(m.id) from Member m => 엔티티의 아이디를 사용
      • select count(m) from Member m => 엔티티를 직접 사용
    • SQL
      • JPQL 둘다 같은 다음 SQL 실행
      • select count(m.id) as cnt from Member m
  • 엔티티를 파라미터로 전달
String jpql = "select m from Member m where m = :member";

/*
    [실행된 SQL]
    select m.* from Member m where m.id=?
*/
List resultList = 
    em.createQuery(jpql) 
    .setParameter("member", member)
    .getResultList();
  • 식별자를 직접 전달
String jpql = "select m from Member m where m.id = :memberId";

/*
    [실행된 SQL]
    select m.* from Member m where m.id=?
*/
List resultList =
    em.createQuery(jpql) 
    .setParameter("memberId", memberId)
    .getResultList();

외래 키 값

Team team = em.find(Team.class, 1L);
String qlString = select m from Member m where m.team = :team;

/*
    [실행된 SQL]
    select m.* from Member m where m.team_id=?
*/
List resultList =
    em.createQuery(qlString)
    .setParameter("team", team)
    .getResultList();
 String qlString = select m from Member m where m.team.id = :teamId;

/*
    [실행된 SQL]
    select m.* from Member m where m.team_id=?
*/
List resultList =
    em.createQuery(qlString)
    .setParameter("teamId", teamId)
    .getResultList();

Named 쿼리

  • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 정적 쿼리로만 사용할 수 있다.
    • 파라미터를 설정하는 것은 가능하다.
    • 미리 정의한 쿼리에 추가 내용을 덧붙일 수는 없다.
  • 어노테이션이나 XML에 정의하여 사용한다.
  • 애플리케이션 로딩 시점에 초기화 후 재사용
    • 애플리케이션 로딩 시점에 JPA나 하이버네이트가 SQL로 파싱해서 캐시에 보유하고 있게 된다.
    • 코스트가 엄청나게 줄어든다.
  • 애플리케이션 로딩 시점에 쿼리를 검증한다.
    • 테스트를 한 번이라도 하고 배포를 한다면 왠만하면 오류를 잡을 수 있다.
  • XML을 통해 작성한 네임드 쿼리가 항상 우선권을 가진다.
  • 애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.

어노테이션

//s:엔티티 정의
@Entity
@NamedQuery(
    name = "Member.findByUsername",
    query="select m from Member m where m.username = :username"
)
public class Member {
    /* 내용 작성 */
}
//e:엔티티 정의

//s:테스트
List<Member> resultList = 
    em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username", "회원1")
    .getResultList();
//e:테스트

XML

<!-- s:persistence.xml -->
<persistence-unit name="hello">
    <mapping-file>META-INF/ormMember.xml</mapping-file>
</persistence-unit name="hello">
<!-- e:persistence.xml -->

<!-- s: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.name = :name
			]]>
		</query>
	</named-query>
	<named-query name="Member.count">
		<query>select count(m) from Member m</query>
	</named-query>
</entity-mappings>
<!-- e:ormMember.xml -->

벌크 연산

  • 여러 건의 데이터를 한꺼번에 UPDATE/DELETE하는 연산
    • 단 건 데이터에 대한 경우는 포함되지 않는다.
  • JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL이 실행된다.
    • 만약에 변경된 데이터가 100건이라면 100번의 UPDATE SQL이 실행된다.
  • 예시
    • 전제 조건
      • 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
    • 동작 순서
      1. 재고가 10개 미만인 상품을 리스트로 조회한다.
      2. 상품 엔티티의 가격을 10% 증가한다.
      3. 트랜잭션 커밋 시점에 변경감지가 동작한다.
  • executeUpdate()의 결과는 영향받은 엔티티 수를 반환한다.
  • UPDATEDELETE를 지원한다.
    • 하이버네이트는 INSERT INTO SELECT도 지원한다.
  • 주의점
    • 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 날린다.
    • 해결법
      • 벌크 연산을 먼저 실행
        • 영속성 컨텍스트에 아무 작업이 되어 있지 않은 상태에서 실행한다.
      • 벌크 연산 수행 후 영속성 컨텍스트 초기화
        • 벌크 연산도 JPQL이 나가는 것이기 때문에 자동으로 flush가 된다.
        • 데이터 불일치성이 일어날 수 있으니 벌크 연산 후에는 반드시 clear를 해준다.
  • 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();

공통 코드

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello"); //애플리케이션 전체 공유 (persistence.xml 참조)
EntityManager em = emf.createEntityManager(); //한번 쓰고 버려야함, 쓰레드간 공유하지 않음
EntityTransaction tx = em.getTransaction(); //조회를 제외한 DML 작업시 필수로 사용
tx.begin();

try {
    //실행 내용
} catch (Exception e) {
    e.printStackTrace();
    tx.rollback();
} finally {
    em.close();
}

emf.close();

출처

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.