[JPA] 객체지향 쿼리 언어
자바 ORM 표준 JPA 프로그래밍 섹션10. 객체지향 쿼리 언어1 - 기본 문법
객체지향 쿼리 언어 소개
JPA는 다양한 쿼리 방법을 지원하는데 JPQL, JPA Criteria, QueryDSL, 네이티브 SQL 등이 있다. 혹은 JDBC API를 직접 사용하여 MyBatis, SpringJdbcTemplate과 함께 사용할 수도 있다.
JPQL
JPQL은 가장 단순한 조회 방법으로 EntityManager.find()랑 객체 그래프 탐색(a.getB().getC())가 가능하다.
JPA를 사용하면 엔티티 객체를 중심으로 개발해야 하는데 이때 문제는 검색 쿼리이다. 검색을 할 때에도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 하는데 모든 DB 데이터를 객체로 변호나해서 검색하는 것은 불가능하다. 따라서 어플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건인 포함된 SQL이 필요하다.
JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공하며 SQL과 문법이 유사하다. SELECT, FROM, WHER, GROUP BY, HAVING, JOIN 등 ANSI 표준이 지원하는 SQL 문법은 모두 지원한다. 따라서 특정 DB SQL에 의존적이지 않고 결국 SQL로 변환된다. 차이점은 JPQL은 엔티티 객체를 대상으로 쿼리하지만 SQL은 DB 테이블을 대상으로 쿼리한다는 점이다. JPQL은 단순한 String이기 때문에 동적 쿼리를 만들기 어렵다는 단점이 있다.
List<Member> result = em.createQuery("select m From Member m where m.name like '%kim%'", Member.class).getResultList();
Criteria
문자가 아닌 자바코드로 JPQL을 작성할 수 있으며 JPQL의 빌더 역할을 한다. 오타 발생 시 컴파일 오류가 나고 동적 쿼리를 쓸 수 있다는 장점이 있다. 하지만 너무 복잡하고 실용성이 없다는 큰 단점이 있다. 그래서 Criteria 대신에 QueryDSL 사용을 권장한다.
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
QueryDSL도 문자가 아닌 자바코드로 JPQL을 작성할 수 있으며 JPQL의 빌더 역할을 한다. 컴파일 시에 문법 오류를 찾을 수 있고 동적 쿼리 작성이 편리하다는 장점이 있다. QueryDSL은 단순하고 쉬워 실무 사용에 권장된다. JPQL을 잘 알면 QuertDSL은 공식 문서를 통해 금방 사용법을 익힐 수 있다.
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로 해결할 수 없는 특정 DB에 의존적인 기능이다. 예를 들면 오라클 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 직접 사용
JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나 스프링 JdbcTemplate, 마이바티스 등을 함께 사용할 수 있다. 단 영속성 컨텍스트를 적절한 시점에 강제로 flush하는 것이 필요하다. 예를 들면 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동 flush 하면 된다.
JPQL 기본문법
기본 문법
select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자를 구분한다. (Member, age)
- JPQL 키워드는 대소문자 구분을 안한다. (SELECT, FROM, where)
- 테이블 이름이 아닌 엔티티의 이름을 사용한다.
- 별칭이 필수이다. (m)
- as는 생략 가능하다.
- 집합과 정렬이 아래와 같이 가능하다. (GROUP BY, HAVING, ORDER BY)
- TypeQuery는 반환 타입이 명확할 때 사용하고 Query는 반환 타입이 명확하지 않을 때 사용한다.
TypedQuery<Member> query =
em.createQuery("SELECT m FROM Member m", Member.class);
Query query =
em.createQuery("SELECT m.username, m.age from Member m");
- 결과 조회 API
- query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
- 결과가 없으면 빈 리스트 반환
- query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환
- 결과가 없으면 : javax.persistence.NoResultException
- 둘 이상이면 : javax.persistence.NonUniqueResultException
- query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
- 위치 기반의 파라미터 바인딩도 가능하나 권장되지는 않는다. 이름 기준으로 사용하는 것이 권장된다.
프로젝션
프로젝션이란 SELCET 절에 조회할 대상을 지정하는 것으로 프로젝션의 대상은 주로 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 될 수 있다. 아래와 같이 사용할 수 있다.
SELECT m FROM Member m -> 엔티티 프로젝션
SELECT m.team FROM Member m -> 엔티티 프로젝션
SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
DISTINCT로 중복 제거도 가능하다.
여러 값을 조회하고 싶을 때는 아래와 같이 조회할 수 있다.
1. Query 타입으로 조회
Member member = new Member();
member.setUsername("영서");
em.persist(member);
em.flush();
em.clear();
List resultList = em.createQuery("select m.username, m.age from Member m")
.getResultList();
Object o = resultList.get(0);
Object[] result = (Object[]) 0;
System.out.println("result"+result[0]);
System.out.println("result"+result[1]);
2. Object[] 타입으로 조회
Member member = new Member();
member.setUsername("영서");
em.persist(member);
em.flush();
em.clear();
List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m")
.getResultList();
System.out.println("result"+resultList[0]);
System.out.println("result"+resultList[1]);
3. new 명령어로 조회
단순값을 DTO로 바로 조회하는 방법이다. 대신 패키지 명을 포함한 전체 클래스명을 입력해야 하며 순서와 타입이 일치하는 생성자가 필요하다.
Member member = new Member();
member.setUsername("영서");
em.persist(member);
em.flush();
em.clear();
List<MemberDTO> resultList = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
.getResultList();
MemberDTO memberDto = resultList.get(0);
System.out.println("memberDto : "+ memberDto.getUsername());
System.out.println("memberDto : "+ memberDto.getAge());
페이징 API
JPA는 페이징을 다음 두 API로 추상화 한다.
1. setFirstResult(int startPosition) : 조회 시작 위치를 지정하면 된다. 0부터 시작한다.
2. setMaxResults(int maxResult) : 조회할 데이터 수를 넣어주면 된다.
아래와 같이 사용할 수 있다.
for (int i =0; i<100; i++){
Member member = new Member();
member.setUsername("Member"+i);
member.setAge(i);
em.persist(member);
}
em.flush();
em.clear();
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(0)
.setMaxResults(10)
.getResultList();
System.out.println("result.size="+result.size());
for (Member member1 : result){
System.out.println("member1"+member1);
}
조인
- 내부 조인
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
아래와 같이 사용할 수 있다.
Member member = new Member();
member.setUsername("Member1");
member.setAge(20);
em.persist(member);
Team team = new Team();
team.setName("teamA");
member.setTeam(team);
em.persist(team);
em.flush();
em.clear();
String query = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
JPA 2.1부터 ON 절을 활용한 조인을 지원한다. 지원 기능은 아래 두 가지이다.
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'
2. 연관관계가 없는 엔티티를 외부 조인할 수 있다.
예를 들어 회원의 이름과 팀의 이름이 같은 대상 외부 조인하려면 아래와 같이 할 수 있다.
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
서브 쿼리
서브 쿼리는 SQL에서의 서브 쿼리와 동일하다. 기존 쿼리 안에 삽입된 또 다른 쿼리를 의미한다. 서브쿼리는 메인쿼리와 관계가 없으며 변수도 공유하지 않는다. 공유하게 되면 성능이 저하되기 때문이다.
서브쿼리 지원 함수는 아래와 같다.
- [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 절에서만 서브쿼리가 사용 가능하다. Hibernate 지원 하에서는 SELECT 절도 가능하다. FROM 절의 서브쿼리는 현재 JPQL에서는 불가능하여 조인을 풀어 해결해야 한다.
JPQL 타입 표현
JPQL 타입 표현은 문자와 숫자 등을 JPQL로 어떻게 표현할지에 대한 이야기이다.
- 문자 : ‘HELLO’, ‘She’’s’
- 숫자 : 10L(Long), 10D(Double), 10F(Float)
- Boolean : TRUE, FALSE
- ENUM : jpabook.MemberType.Admin (패키지명 포함)
- 엔티티 타입 : TYPE(m) = Member (상속 관계에서 사용)
조건식(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이 아니면 반환해주는 함수이다. NULLIF는 두 값이 같으면 null을, 다르면 첫번째 값을 반환해주는 함수이다.
//사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username,'이름 없는 회원') from Member m
//사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m
JPQL 기본 함수
JPQL에서 사용하는 기본 함수 중 가장 대표적인 표준 함수는 CONCAT, SUBSTRING, TRIM이다. 이 함수들은 DB에 상관없이 사용할 수 있다. 이외 함수들은 아래와 같이 있다.
- CONCAT
- SUBSTRING
- TRIM
- LOWER, UPPER
- LENGTH
- LOCATE
- ABS, SQRT, MOD
- SIZE, INDEX(JPA 용도)
위 함수에서 사용이 안되면 사용자 정의 함수를 호출해야 한다. 예를 들어 Hibernate는 사용 전 방언에 추가해야 한다. 사용하는 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'
여기서 나온 용어를 정리하면 아래와 같다.
1. 상태 필드 (state field)
단순히 값을 저장하기 위한 필드이다. 예시는 m.username 이 있다. 상태 필드는 경로 탐색의 끝으로 더이상 탐색이 불가능하다.
//JPQL
select m.username, m.age from Member m
//SQL
select m.username, m.age from Member m
2. 연관 필드 (association field)
연관관계를 위한 필드로 두 가지로 분류할 수 있다.
- 단일 값 연관 필드 : @ManyToOne, @OneToOne을 사용하며 대상이 엔티티이다. (Ex. m.team) 단일 값 연관 경로는 묵시적 내부 조인이 발생하며 탐색이 가능하다.
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany를 사용하며 대상이 컬렉션이다. (Ex.orders) 컬렉션 값 연관 경로는 묵시적 내부 조인이 발생하지만 탐색은 불가능하다. FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색이 가능하다.
//단일 값 연관 경로 탐색
//JPQL
select o.member from Order o
//SQL
select m.*
from Orders o
inner join Member m on o.member_id = m.id
명시적 조인은 join 키워드를 직접 사용하는 조인이며 묵시적 조인은 경로 표현식에 의해 묵시적으로 SQL 조인이 발생하여 내부 조인만 가능하다.
//명시적 조인
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 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 준다.
따라서 묵시적 조인은 쿼리 튜닝에 어려움이 많기 때문에 명시적 조인을 쓰는 것이 권장된다. 조인은 SQL 튜닝에 중요 포인트이기 때문이다.
페치 조인 (Fetch join)
페치 조인은 SQL 조인 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 이는 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능으로 join fetch 명령어를 사용한다.
페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
예를 들면 회원을 조회하면서 연관된 팀도 함께 조회하고 싶을 때 SQL에서는 회원 뿐만아니라 팀도 SELECT도 하는데 JPQL에서는 회원뿐만 아니라 팀도 동적으로 가지고 올 수 있다. 이 경우는 ManyToOne 일 때 쓰는 페치 조인이다.
[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
아래 SQL은 즉시로딩에 사용하는 쿼리랑 동일하나 위 JPQL은 쿼리를 통해 내가 원하는 객체 그래프를 한 번에 정할 수 있다.
아래와 같이 사용할 수 있다. 지연로딩으로 세팅하더라도 페치 조인이 우선으로 동작한다.
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());
}
컬렉션 페치 조인은 OneToMany 일 때나 컬렉션일 때 페치 조인을 하는 것이다. 아래와 같이 SQL, JPQL문을 짤 수 있다.
[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 명령을 사용하면 된다. 하지만 SQL 것으로는 모두 제거가 불가능하기 때문에 JPQL의 DISTINCT을 사용하는데 JPQL의 DISTINCT는 2가지 기능을 제공한다.
1. SQL에 DISTINCT 추가
2. 어플리케이션에서 엔티티 중복을 제거
DISTINCT는 아래와 같이 사용할 수 있다.
select distinct t
from Team t join fetch t.members
where t.name = ‘팀A
1번 기능까지만 생각했을 때, 아래와 같은 경우에서는 SQL에 DISTINCT를 추가해도 데이터가 다르기 때문에 SQL 결과에서 중복 제거가 실패한다.
따라서 2번 기능까지 생각하면, 아래와 같이 추가로 애플리케이션에서 중복 제거를 시도하여 같은 식별자를 가진 Team 엔티티를 제거한다. 하지만 이런 경우는 데이터가 불어나는 1대다 관계에서만 일어나는 일이다.
페치 조인과 일반 조인의 차이
일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않는다. JPQL은 결과를 반환할 때 연관관계를 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다. 하지만 페치 조인은 사용할 때 연관된 엔티티도 함께 조회하는 즉시 로딩이 일어난다. 결국 객체 그래프를 SQL 한 번으로 조회하는 개념이다. 따라서 N+1 문제를 해결할 수 있다.
페치 조인의 한계
- 페치 조인 대상에는 별칭을 줄 수 없다. Hibernate는 가능하나 정합성 이슈로 가급적 사용을 권장하지 않는다.
- 둘 이상의 컬렉션은 페치 조인을 할 수 없다.
- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다. 다만 일대일이나 다대일 같은 단일 값 연관 필드들은 데이터가 불어나지 않기 때문에 페치 조인 해도 페이징이 가능하다. Hibernate는 경고 로그를 남기고 메모리에서 페이징을 하기 때문에 매우 위험하다.
페치 조인의 특징
- 페치 조인은 연관된 엔티티들을 SQL 한 번으로 조회하기 때문에 성능 최적화에 좋다.
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다.
모든 것을 페치 조인으로 해결할 수는 없지만 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다. 따라서 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면 페치 조인보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
다형성 쿼리
아래와 같이 엔티티를 다형적으로 설계한 경우 다형성 쿼리를 사용할 수 있다.
JPA는 이런 경우에 조회 대상을 특정 자식으로 한정할 수 있다. 예를 들어 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’)
Java의 타입 캐스팅과 유사한 TREAT가 있다. 상속 구조에서 부모타입을 특정 자식타입으로 다룰 때 사용할 수 있고 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”;
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 qlString = “select m from Member m where m.team = :team”;
List resultList = em.createQuery(qlString)
.setParameter("team", team)
.getResultList();
String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString)
.setParameter("teamId", teamId)
.getResultList();
//실행된 SQL
select m.* from Member m where m.team_id=?
Named 쿼리
말 그대로 쿼리에 미리 이름을 부여하고 사용하는 JPQL의 기능이다. 정적 쿼리만 가능하며 어노테이션이나 XML에 정의해두어야 한다. 어플리케이션 로딩 시점에 초기화 후 재사용이 가능하여 어플리케이션 로딩 시점에 쿼리를 검증해준다는 장점이 있다.
어노테이션 정의는 아래와 같이 사용할 수 있다.
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"
)
public class Member extends BaseEntity{
...
}
public class JpaMain {
public static void main(String[] args) {
...
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
for (Member member : resultList){
System.out.println("member="+member);
}
...
}
}
XML 정의는 아래와 같이 할 수 있다.
[META-INF/persistence.xml]
<persistence-unit name="jpabook" >
<mapping-file>META-INF/ormMember.xml</mapping-file>
[META-INF/ormMember.xml]
<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>
어노테이션과 XML 중에서는 XML이 우선권을 가지며 어플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.
벌크 연산
벌크 연산이란 SQL에서 PK 하나를 찍어 UPDATE/DELETE 하는 것을 제외한 모든 UPDATE문, DELETE문이라 볼 수 있다.
예를 들어 재고가 10개 미만인 모든 상품의 가격을 10% 상승한다 했을 때 JPA 변경 감지 기능을 실행하려면 너무 많은 SQL이 실행된다.
- 재고가 10개 미만인 상품을 리스트로 조회한다.
- 상품 엔티티의 가격을 10% 증가한다.
- 트랜잭션 커밋 시점에 변경감지가 동작한다.
- 만약 변경된 데이터가 100건이라면 100번의 UPDATE SQL이 실행된다.
이때 벌크 연산을 사용하면 쿼리 한 번으로 여러 엔티티의 로우를 변경할 수 있다. 벌크 연산은 UPDATE, DELETE를 지원한다. 추가로 executeUpdate()는 영향 받은 엔티티 수를 반환해주는 메소드이다.
하지만 벌크 연산 사용 시 주의점이 있는데 벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리를 넣는 연산이기 때문에 벌크 연산을 먼저 실행하거나 벌크 연산 수행 후에 영속성 컨텍스트를 초기화 해야 한다.