JPQL

기본 문법과 쿼리 API

select  :: =
    select 
    from 
    [where ]
    [group by ]
    [having ]
    [order by ]

update  :: = update  [where ]
delete  :: = delete  [where ]

SELECT 문

SELECT m FROM member as m WHERE m.username = 'chae'
  • 엔티티와 속성(member, username)은 대소문자를 구분, JPQL 키워드(select, from)는 대소문자를 구분하지 않는다.
  • Member는 클래스 명이 아니라 엔티티명으로 지정하지 않으면 클래스명이 기본값, 클래스명을 엔티티로 사용하는 것을 추천
  • member as m와 같은 별칭은 필수, 별칭이 없는 경우 오류가 발생한다.(하이버네이트 질의언어인 HQL을 사용하면 별칭이 없어도 됨.)

TypeQuery, Query

쿼리 객체는 TypeQuery와 Query가 존재한다. 반환할 타입을 명확하게 지정 가능하면 TypeQuery, 명확하지 않으면 Query를 사용하면 된다.

// TypeQuery
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.getResultList();
// Query
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List<?> resultList = query.getResultList();

결과 조회

실제 쿼리를 실행해서 DB를 조회하는 메서드

  • query.getResultList() : 결과를 List 컬렉션으로 반환한다. 만약 결과가 없으면 빈 컬렉션 반환.
  • query.getSingleResult() : 결과가 정확히 하나일 때 사용한다. 만약 결과가 없으면 javax.persistence.NoResultException 예외가 발생. 결과가 2개 이상이면 javax.persistence.NonUniqueResultException 예외가 발생.

파라미터 바인딩

JPQL은 이름 기준, 위치 기준 파라미터 바인딩을 지원한다.

이름 기준 파라미터

파라미터를 이름으로 구분하는 방법으로 파라미터 앞에 : 를 붙여 사용한다.

String username = "user";

TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class);

query.setParameter("username", username);
List<Member> resultList = query.getResultList();

// 메서드 체인 방식
List<Member> members = 
    em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class)
    .setParameter("username", username)
    .getResultList();

위치 기준 파라미터

파라미터를 위치로 구분하는 방법으로 ? 다음에 위치 값을 지정한다.

List<Member> members = 
	em.createQuery("SELECT m FROM Member m WHERE m.username = ?1", Member.class)
	.setParameter(1, username)
	.getResultList();

파라미터 바인딩 방식을 사용하지 않으면 SQL 인젝션 공격과 성능 이슈가 발생한다. 파라미터 바인딩은 필수로 사용해야 한다.

프로젝션

SELECT 절에 조회할 대상을 지정하는 것을 프로젝션(projection)이라고 한다. 프로젝션 대상으로는 엔티티, 임베디드 타입, 스칼라 타입(기본 데이터 타입)이 있다.

엔티티 프로젝션

엔티티를 프로젝션 대상으로 사용한다. 조회한 엔티티는 영속성 컨테스트에서 관리된다.

SELECT m FROM Member m

임베디드 타입 프로젝션

임베디드 타입은 엔티티 타입이 아닌 값 타입이므로, 영속성 컨텍스트에서 관리되지 않는다.

String query = "SELECT o.address FROM Order o";
List<Address> addresses = em.createQuery(query, Address.class).getResultList();

스칼라 타입 프로젝션

숫자, 문자, 날짜와 같은 기본 데이터 타입

List<String> usernames = 
    em.createQuery("SELECT username FROM Member m", String.class).getResultList();

Double orderAmountAvg = 
    em.createQuery("SELECT AVG(o.orderAmount) FROM Order o", Double.class).getSingleResult();

여러 값 조회

엔티티 대상이 아닌 꼭 필요한 데이터들만 선택해서 조회할 때 사용한다. 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

List<Object[]> resultList = em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o").getResultList();

for(Object[] row : resultList) {
    Member member = (Member) row[0];
    Product product = (Product) row[1];
    int orderAmount = (Integer) row[2];
}

new 명령어

실무에서는 Object[]를 직접 사용하지 않고 DTO를 만들어 객체로 반환하여 사용한다.

  • 대상 객체의 패키지명을 포함한 전체 클래스명을 입력해야 한다.
  • 순서와 타입이 일치하는 생성자가 필요하다.
// DTO를 사용해서 컬렉션에 저장
List<Object[]> resultList = em.createQuery("SELECT m.username, m.age FROM Member m").getResultList();

List<UserDTO> userDTOs = new ArrayList<>();

for(Object[] row : resultList) {
    UserDTO userDTO = new UserDTO((String)row[0], (Integer)row[1]);
    userDTOs.add(userDTO);
}

// new 명령어를 사용해서 저장
TypeQuery<UserDTO> query = em.createQuery("SELECT new com.user.UserDTO(m.username, m.age FROM Member m");

List<UserDTO> resultList = query.getResultList();

페이징 API

JPQL은 페이징을 두 API로 추상화해서 처리한다.

  • setFirstResult(int startPosition) : 조회 시작 위치
  • setMaxResults(int maxResult) : 조회할 데이터 수
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);

query.setFirstResult(0);
query.setMaxResults(10);
query.getResultList();

JPQL 조인

SQL 조인과 기능은 같고 문법만 다르다.

내부 조인

SELECT m FROM Member m INNER JOIN m.team t WHERE t.name = 'team_a';

FROM Member m JOIN m.team t 와 같이 연관 필드로 팀과 조인하여 별칭을 붙여야 한다.
FROM Member m JOIN Team t 와 같이 조인하게 되면 오류가 발생한다.

외부 조인

SELECT m FROM Member m LEFT JOIN m.team t

컬렉션 조인

일대다, 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 의미한다. 컬렉션 값 연관 필드는 내부 조인 시 조회되지 않을 수 있기 때문에 외부 조인을 사용해야 한다.

SELECT t, m FROM Team t LEFT JOIN t.members m

세타 조인

WHERE 절을 사용하며, 내부 조인만 지원한다. 세타 조인을 사용하면 전혀 관계 없는 엔티티도 조인이 가능하다.

SELECT count(m) FROM Member m, Team t WHERE m.username = t.name

JOIN ON 절

JPA 2.1 부터 조인을 사용할 때 ON 절을 지원한다.

// JPQL
SELECT m FROM Member m LEFT JOIN m.team t ON t.name = 'team_a'

// SQL
SELECT M.*, T.*
FROM MEMBER M
LEFT JOIN TEAM T ON M.TEAM_ID = T.ID AND T.NAME = 'team_a'

페치 조인

  • JPA에서 성능 최적화를 위해 제공하는 기능.
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 조회한다.
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야하면 페치 조인 보다는 일반 조인을 사용해서 필요한 데이터들만 조회하여 반환하는 것이 효과적이다.
  • 페치 조인을 사용하면 지연 로딩 설정이 되어있더라도 페치 조인이 우선으로 적용되어 명시적으로 즉시 로딩이 적용된다.

엔티티 페치 조인

연관된 엔티티나 컬렉션을 함께 조회한다. JPQL 조인과는 다르게 페치 조인은 별칭을 사용하지 못 한다(하이버네이트의 경우 페치 조인에도 별칭 사용 가능).

// 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'

컬렉션 페치 조인을 사용하면 동일한 결과값이 2번 출력되는 문제가 발생한다.

페치 조인과 DISTINCT

JPQL의 DISTINCT 명령어를 추가하면 SQL에 DISTINCT를 추가하고, 애플리케이션에서 한번 더 중복을 제거해준다.

SELECT DISTINCT t
FROM Team t JOIN FETCH t.members
WHERE t.name = 'team_a'

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

  • JPQL은 결과를 반환할 때 연관관계는 고려하지 않고 단지 SELECT 절에 지정한 엔티티만을 조회한다.

  • 페치 조인 사용 시, 연관된 엔티티를 함께 조회한다(즉시 로딩). 연관된 엔티티에 대한 정보를 얻어오는 동작을 수행해도 조회 SQL이 발생하지 않는다.

  • 일반 조인 사용 시, 연관된 엔티티를 함께 조회하지 않는다. 연관된 엔티티에 대한 정보를 얻어오는 동작을 수행 시, 조회 SQL이 발생한다.

// 페치조인 JPQL
SELECT t FROM Team t JOIN FETCH t.members

// 페치조인 SQL
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M
ON T.ID = M.TEAM_ID


// 일반 조인 JPQL
SELECT t FROM Team t JOIN t.members

// 일반 조인 SQL
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M
ON T.ID = M.TEAM_ID

페치 조인의 특징과 한계

  • JPA 객체 그래프 탐색 의도는 연관된 모든 데이터를 가지고 오는 것이다. 페치조인은 연관된 엔티티들을 SQL 한 번으로 모두 조회하므로 객체 그래프를 유지할 때 사용하면 효과적이다. 특정 조건의 데이터만 필요하다면 별도의 SQL 조회를 하는 것이 좋다.

  • 연관된 엔티티를 함께 조회하기 때문에 SQL 호출 횟수를 줄여 성능 개선이 가능하다.

  • 글로벌 로딩 전략보다 우선 적용된다. 글로딩 로딩 전략을 지연 로딩으로 사용하고 최적화가 필요할 때 페치 조인을 사용하여 성능 개선이 가능하다.

글로벌 로딩 전략 : 엔티티에 직접 적용하는 로딩 전략 @OneToMany(fetch = FetchType.LAZY)

페치 조인의 단점

  • 페치 조인 대상에는 별칭을 줄 수 없다. 그래서 SELECT, WHERE, 서브 쿼리에 페치 조인 대상을 사용하지 못 한다. 하이버네이트는 별칭을 지원하지만 사용하지 않는게 좋다. 특정 조건의 데이터만 조작하는 경우에도 해당되지 않는 나머지 데이터들이 변경되거나 제거될 수 있기 때문이다. 이 때 데이터 정합성 문제가 발생한다.

  • 둘 이상의 컬렉션을 페치 조인하지 못 한다. 구현체에 따라 가능하지만 카테시안 곱이 발생하여 주의가 필요하다. 하이버네이트의 경우 org.hibernate.loader.MultipleBagFetchException 예외가 발생한다.

  • 컬렉션을 페치 조인하면 페이징 API를 사용하지 못 한다. 컬렉션(일대다)이 아닌 단일 필드(일대일, 다대일)들은 페이징 API가 사용가능하다. 하이버네이트의 경우 컬렉션을 페치 조인하고 페이징 API를 사용하면 경고 로그가 발생한다. 데이터가 적으면 상관 없지만 많은 경우 성능 이슈와 메모리 초과 예외가 발생하게 된다.

경로 표현식

.(점) 을 찍어서 객체 그래프를 탐색하는 것.

  • 상태 필드 : 단순히 값을 저장하기 위한 필드 (m.username, m.age)
  • 연관 필드 : 연관관계를 위한 필드, 임베디드 타입 포함
    • 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티 (m.team)
    • 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션 (t.members)

명시적 조인과 묵시적 조인

  • 명시적 조인 : JOIN을 직접 적어주는 것.
SELECT m FROM Member m JOIN m.team t
  • 묵시적 조인 : 경로 표현식에 의해 묵시적으로 조인이 발생하는 것. 내부조인만 가능하다.
SELECT m.team FROM Member m

경로 표현식과 특징

  • 상태 필드 경로 : 경로 탐색의 끝, 더는 탐색 할 수 없다.
// 상태 필드 경로 탐색 JPQL
SELECT m.username, m.age FROM Member
  • 단일 값 연관 경로 : 묵시적으로 내부 조인 발생, 값 연관 경로 계속 탐색 가능(외부 조인은 명시적으로 JOIN 키워드가 필요함).
// JPQL
SELECT o.member FROM Order o

// SQL
SELECT M.*
FROM ORDER O
INNER JOIN MEMBER M ON O.MEMBER_ID = M.ID // 묵시적 조인(내부 조인)
  • 컬렉션 값 연관 경로 : 묵시적으로 내부 조인 발생, 더는 탐색할 수 없지만 FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색할 수 있다.
// 컬렉션  연관 경로 탐색

SELECT t.members FROM Team t // 성공

SELECT t.members.username FROM Team t // 실패
-> SELECT m.username FROM Team t JOIN t.members m // 새로운 별칭 필요

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

  • 항상 내부 조인을 사용한다.

  • 컬렉션은 경로 탐색의 끝, 컬렉션에서 경로 탐색을 하려면 명시적인 조인 별칭이 필요하다.

  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 FROM 절에 영향을 준다.

  • 묵시적 조인은 조인이 일어나는 상황을 파악하기 어렵다. 그래서 명시적 조인을 사용하는 것이 좋다.

서브 쿼리

SELECT, FROM 절에는 사용 할 수 없고 WHERE, HAVING 절에서만 사용 가능하다. 하이버네이트의 HQL은 SELECT 절의 서브 쿼리도 허용된다.

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) 서브 쿼리 결과가 하나라도 같은게 있으면 참. 서브 쿼리 아닌 곳에도 사용함.

다형성 쿼리

상속관계(@Inheritance)로 구성된 부모 엔티티를 조회하면 자식 엔티티도 함께 조회한다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item { ... }

@Entity
@DiscriminatorValue("B")
public class Book extends Item {
    ...
    private String author;
    
    private String isbn;
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item {
    ...
    private String artist;
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
    ...
    private String director;

    private String actor;
}
...
List<?> resultList = em.createQuery("SELECT i FROM Item i").getResultList();

// 단일 테이블 전략(InheritanceType.SINGLE_TABLE)
SELECT * FROM ITEM

// 조인 전략(InheritanceType.JOINED)
SELECT 
    I.ITEM_ID,
    B.AUTHOR,
    A.ARTIST,
    M.ACTOR,
    ...
FROM ITEM I
LEFT OUTER JOIN BOOK B ON I.ITEM_ID = B.ITEM_ID
LEFT OUTER JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID
LEFT OUTER JOIN MOVIE M ON I.ITEM_ID = M.ITEM_ID

TYPE

상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 사용한다.

// JPQL
SELECT i FROM Item i WHERE type(i) IN (Book, Movie)

// SQL
SELECT I FROM ITEM I
WHERE I.DTYPE IN ('B', 'M')

TREAT

상속 구조에서 부모 타입을 특정 타입으로 다룰 때 사용한다. 자바의 타입 캐스팅과 비슷한 기능. JPA 표준은 FROM, WHERE 절에서만 사용 가능하다. 하이버네이트의 경우 SELECT 절에도 사용 가능하다.

// 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'

사용자 정의 함수 호출

JPA 2.1 부터 사용자 정의 함수를 지원한다.
FUNCTION(function_name {, function_arg}*)

SELECT FUNCTION('group_concat', i.name)
FROM Item i

하이버네이트의 DB 함수 호출

하이버네이트를 사용할 경우 아래와 같이 방언 클래스를 상속해서 사용할 DB 함수를 미리 등록해야 한다.

public class MyH2Dialect extends H2Dialect {
    public MyH2Dialect() {
        registerFunction (
            "group_concat",
            new StandardFunction("group_concat", StandardBasicTypes.STRING)
        );
    }
}

registerFunction의 두 번째 인자로는 하이버네이트의 SQLFunction 구현체를 주면 된다. 위 코드는 기본 함수를 사용하겠다는 의미로 StandardFunction을 사용했다. 첫 번째 인자로 함수 이름, 두 번째 인자로 리턴 타입을 주고 있다.

// 상속한 Dialect 등록
<property name="hibernate.dialect" value="com.study.dialect.MyH2Dialect">
// 함수 사용
SELECT group_concat(i.name) FROM Item i

엔티티 직접 사용

객체 인스턴스는 참조값으로 식별하고 테이블 로우는 기본 키 값으로 식별하기 때문에 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본키 값을 사용한다.

기본키 값

// JPQL
SELECT COUNT(m.id) FROM Member m // 엔티티의 아이디를 사용
SELECT COUNT(m) FROM Member m // 엔티티를 직접 사용

// SQL 실행결과는 동일
SELECT COUNT(M.ID) AS CNT
FROM MEMBER M
// 엔티티를 파라미터로 전달
String jpql = "SELECT m FROM Member m WHERE m = :member";
List<?> resultList = em.createQuery(jpql).setParameter("member", member).getResultList();

// 식별자를 직접 전달
String jpql = "SELECT m FROM Member m WHERE m.id = :memberId";
List<?> resultList = em.createQuery(jpql).setParameter("memberId", memberId).getResultList();

// SQL
SELECT M.* FROM MEMBER M WHERE M.ID = ?

외래키 값

// 엔티티를 파라미터로 전달
Team team = em.find(Team.class, 1L);
String jpql = "SELECT m FROM Member m WHERE m.team = :team";
List<?> resultList = em.createQuery(jpql).setParameter("team", team).getResultList();

// 식별자를 직접 전달
String jpql = "SELECT m FROM Member m WHERE m.team.id = :teamId";
List<?> resultList = em.createQuery(jpql).setParameter("teamId", teamId).getResultList();

// SQL
SELECT M.* FROM MEMBER M WHERE M.TEAM_ID = ?

Named 쿼리(정적 쿼리)

em.createQuery("SELECT ... ") 처럼 JPQL을 직접 문자로 넘기는 것을 동적 쿼리라고 한다. 미리 정의한 쿼리에 이름을 부여해서 해당 이름으로 사용하는 것을 Named 쿼리라고 한다.

Name 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해두므로 오류를 빨리 확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다.

애너테이션 정의

@Entity
@NamedQueries({
    @NamedQuery(
        name = "Member.findByUsername",
        query = "SELECT m FROM Member WHERE m.username =:username"
    ),
    @NamedQuery(
        name = "Member.count",
        query = "SELECT COUNT(m) FROM Member m"
    )
})

class Member {
    ...
}

위처럼 엔티티에 @NamedQuery, @NamedQueries 애너테이션을 사용해서 직접 정의해주면 된다.
Named 쿼리의 이름은 관리의 편의성을 위함이다. 그리고 Named 쿼리는 영속성 유닛 단위로 관리되므로 충돌을 방지하기 위해 이름으로 구분한다.

List<Member> result = em.createNamedQuery("Member.findByName", Member.class)
                        .setParameter("username", "joont")
                        .getResultList();

XML 정의

자바로 멀티라인 문자를 다루는 것은 번거로우므로, Named 쿼리를 작성할 때는 XML을 사용하는 것이 더 편리하다.

<!--xml version="1.0" encoding="UTF-8"?-->
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" version="2.0">
    <named-query name="Member.findByUserName">
        <query>
            select m 
            from Member m 
            where m.username = :username
        </query>
    </named-query>

    <named-query name="Member.findByAgeOver">
        <query><![CDATA[
            select m 
            from Member m 
            where m.age > :age
        ]]></query>
    </named-query>

    <named-native-query name="Inter.findByAlal" result-class="sample.jpa.Inter">
        <query>select a.inter_seq, a.inter_name_ko, a.inter_name_en from tb_inter a where a.inter_name_ko = ?</query>
    </named-native-query>
</entity-mappings>

기타

  • JPQL에서 Enum은 = 비교연산만 지원한다. 하이버네이트에서는 아래와 같이 사용할 수 있다.
Delivery delivery = em.createQuery("SELECT d FROM Delivery d WHERE d.deliveryStatus LIKE '%CO%'", Delivery.class)
                    .getSingleResult();
  • JPQL에서 임베디드 타입은 비교를 지원하지 않는다. 하이버네이트에서는 아래와 같이 사용할 수 있다.
Delivery foundDelivery = em.createQuery("select d from Delivery d where d.address = :address", Delivery.class)
                            .setParameter("address", new Address("daegu", "안심로", "777-333"))
                            .getSingleResult();
  • JPA는 ''를 길이 0인 Empty String으로 인식하지만 DB에 따라 ''를 null로 사용하는 곳이 있으니 확인하고 사용해야 한다.

네이티브 SQL

JPA는 표준 SQL이 지원하는 대부분의 SQL 문법과 함수들을 지원하지만, 특정 DB만 지원하는 함수나 문법, SQL 쿼리 힌트 같은 것들은 지원하지 않는다. 이런 기능을 사용하기 위해서는 네이티브 SQL을 사용해야 한다. 네이티브 SQL이란 JPA에서 일반 SQL을 직접 사용하는 것을 말한다.

엔티티 조회

Query createNativeQuery(String sqlString, Class resultClass)

반환타입을 줘도 TypedQuery가 아닌 Query를 반환한다. 이 메서드로 조회해온 엔티티는 영속성 컨텍스트에서 관리된다. 그러므로 모든 필드를 다 조회하는 SQL을 실행해야 한다. 특정 필드만 조회해오면 오류가 발생한다.

String sql = "SELECT * FROM Member WHERE id = 1";

Member memberFormNative = (Member)em.createNativeQuery(sql, Member.class).getSingleResult();
Member memberFromJPQL = em.find(member.class, 1);

assertSame(memberFromnNative, memberFromJPQL); // 성공

값 조회

Query createNativeQuery(String sqlString)
String sql = "SELECT id, name, age FROM Member";

List<Object[]> resultList = em.createNativeQuery(sql).getResultList();

for(Object[] row : resultList) {
    Integer id = row[0];
    String name = row[1];
    Integer age = row[2];
}

Named 네이티브 쿼리

애너테이션의 경우 @NameNativeQuery을 사용하면 되고, XML의 경우 <named-native-query> 사용하면 된다.
되도록 JPQL을 사용하고, 기능이 부족하면 HQL, 마지막 수단으로 네이티브 SQL을 사용하자.

벌크 연산(UPDATE, DELETE)

  • JPQL로 여러 건을 한 번에 수정하거나 삭제할 때 사용한다.
  • executeUpdate 메서드를 사용한다. 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.

UPDATE 벌크 연산

String sql = "UPDATE Product p SET p.prce = p.price * 1.1" + 
            "WHERE p.stockAmount < :stockAmount";

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

DELETE 벌크 연산

String sql = "DELETE FROM Product p " +
            "WHERE p.price < :price";

int resultCount = em.createQuery(sql)
                    .setParameter("price", 100)
                    .executeUpdate();

벌크 연산시 주의사항

벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리한다는 특징이 있으므로 주의해야 한다. 즉, 영속성 컨텍스트와 DB 간에 데이터가 차이가 발생할 수 있다는 말이다.

벌크 연산 영속성 문제 해결방법

  • 벌크 연산 직후 em.refresh(entity)를 사용하여 DB에서 다시 데이터를 조회시킨다.

  • 벌크 연산을 먼저 실행시킨다. 벌크 연산을 가장 먼저 실행하면 이미 변경된 내용을 DB에서 가져온다.(가장 실용적인 해결책)

  • 벌크 연산 수행 후 영속성 컨텍스트를 초기화 시킨다. 영속성 컨텍스트가 초기화되면 DB에서 다시 데이터를 조회하기 때문이다.

영속성 컨텍스트와 JPQL

JPQL로 엔티티를 조회하면 JPQL 쿼리가 날라가고, 조회한 엔티티를 영속성 컨텍스트에 저장한다. 이 때, 이미 영속성 컨텍스트에 존재하는 엔티티와 동일한 엔티티는 버려진다. 기존 영속성 컨텍스트에 덮어쓰지 않는 이유는 영속 상태인 엔티티의 동일성을 보장해야하기 때문이다.

JPQL과 플러시 모드

플러시 모드는 FlushMode.AUTO (기본값), FlushMode.COMMIT 이 있다.

FlushMode.AUTO

트랜잭션이 끝날 때나 커밋될 때, 이 외에 JPQL 쿼리 실행 직전에도 플러시를 호출한다.

Member member1 = em.find(Member.class, 1);
member1.setName("modifiedName");

Member member2 = em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
                    .setParameter("id", 1)
                    .getSingleResult();

assertThat(member1.getName(), member2.getName()); // 성공

변경감지는 플러시 될 때 발생하므로, JPQL에서 아직 변경되지 않은 값을 가진 데이터를 가져올 것이라 생각할 수 있지만, FlushMode.AUTO는 영속 상태인 엔티티의 동일성을 보장하기 위해 JPQL 실행 전에 플러시를 수행한다. 그러므로 위 테스트는 성공한다.

FlushMode.COMMIT

FlushMode.COMMIT은 쿼리 전에 플러시를 수행하지 않으므로 직접 em.flush를 호출하거나 Query 객체에 플러시 모드를 설정해줘야 한다.

em.setFlushMode(FlushMode.COMMIT);

Member member1 = em.find(Member.class, 1);
member1.setName("modifiedName");

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

Member member2 = 
    em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
    .setParameter("id", 1)
    .setFlushMode(FlushMode.AUTO)
    .getSingleResult();

assertThat(member1.getName(), member2.getName()); // 성공

FlushMode.COMMIT은 너무 잦은 플러시가 일어나는 경우, 플러시 횟수를 줄여 성능을 최적화하고자 할 때 사용할 수 있다.

Categories:

Updated:

Comments