티스토리 뷰
이전 글: https://gojs.tistory.com/49
JPA 값 타입
JPA는 Java 엔티티와 DB 테이블 간의 매핑을 도와주는 프레임워크이다.따라서 엔티티의 필드 타입과 테이블의 컬럼 타입도 매핑해준다.엔티티의 각 필드에 정의할 수 있는 타입들에 대해서 알아
gojs.tistory.com
객체지향 쿼리
JPA에서 하나의 식별자로 하나의 엔티티를 조회할 수 있고, 조회한 엔티티를 기준으로 삼아 연관된 엔티티를 찾을 수 있다.
- 식별자로 조회: EntityManager.find(id)
- 객체 그래프 탐색으로 조회: entity.getAnother()
그러나 이와 같은 방법으로만 개발하는 것에는 한계가 있기 때문에 데이터베이스에서 필터링된 데이터를 조회할 방법이 필요하다.
(모든 회원정보를 메모리에 올려놓고 탐색할 수는 없다)
JPA에서 제공하는 검색 방법
1) JPQL (Java Persistence Query Language)
2) Criteria 쿼리 (JPQL 작성을 편리하게 도와주는 API)
3) Native SQL (직접 SQL을 만들어 사용)
4) QueryDSL (JPQL 작성을 편리하게 도와주는 빌더 클래스)
이 중 JPQL은 엔티티 객체를 조회하는 객체지향 쿼리이다.
(데이터베이스 방언 기준으로 구현되어있어 특정 데이터베이스에 의존하지 않는다)
JPQL
그렇다면 JPQL을 사용하는 방법에 대해서 아래와 같은 예제를 기반으로 한 번 알아보자.
@Entity(name = "Member")
public class Member {
@Column(name = "name")
private String username;
...
}
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
위 예제를 살펴보면 em.createQuery(...) 메서드의 파라미터에 jpql 문자열과 반환 타입을 전달한다.
그리고 getResultList() 메서드를 실행하여 JPQL을 SQL로 변환하여 데이베이스를 조회한다.
"select m " +
"from Member as m " +
"where m.username = 'kim'"
select
m.id as id,
m.age as age,
m.team_id as team,
m.name as name
from
Member m
where
m.name = 'kim';
위와 같이 JPQL을 SQL로 변환시켜서 데이터를 조회한다.
Criteria 쿼리
Criteria는 JPQL을 생성하는 클래스로 문자열이 아닌 빌더패턴을 이용하여 JPQL을 생성한다.
Criteria 쿼리의 장점은 아래와 같다.
1. 컴파일 시점에 오류 발견 가능
2. IDE에서 코드 자동완성 기능 제공
3. 동적 쿼리 작성이 편리함
CriteriaQuery<Member> cq = em
.getCriteriaBuilder()
.createQuery(Member.class);
.select(query.from(Member.class))
.where(cb.equal(m.get("username"), "kim");
List<Member> resultList = em.createdQuery(cq).getResultList();
Criteria를 사용하게 된다면 위와 같이 빌더 패턴을 기반으로 쿼리를 작성할 수 있다.
Criteria는 명확하게 가진 장점이 존재하는 것에 비해 복잡하고 장황하여 사용하기 불편하기도 하다.
Query DSL
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;
List<Member> members = query
.from(member)
.where(member.username.eq("kim"))
.list(member);
QueryDSL을 활용하여 JPQL을 작성한 예시이다.
사용하기 편리하고 간결하여 한 눈에 들어온다.
네이티브 SQL
특정 데이터베이스에서만 사용하는 기능은 JPA에 표준화되어 있지 않을 수 있기 때문에, JPQL로 해당 기능을 사용하지 못할 수 있다.
이과 같은 경우에는 네이티브 SQL을 사용함으로써 해당 기능을 사용할 수 있다.
(데이터베이스 종류가 바뀌는 경우에 수정이 필요하다는 단점 존재)
String sql = "select id, age, team_id, name from member where name = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getReulstList();
JPQL 기본 문법과 쿼리 API
SELECT 문
"SELECT m FROM Member as m WHERE m.username = 'hello'"
- 대소문자 구분: 엔티티와 속성은 대소문자를 구분한다.
- 엔티티 이름: JPQL에서 사용되는 Member는 엔티티명이다.
- 별칭 필수: Member에 m이라는 별칭을 줬듯이 필수로 별칭을 사용해야한다.
TypeQuery, Query
반환 타입이 명확하면 TypeQuery, 그렇지 않다면 Query를 사용한다.
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member as m", Member.class);
List<Member> resultList = query.getResultList();
Query query = em.createQuery("SELECT m.username, m.age FROM Member as m");
List<Object> resultList = query.getResultList();
결과 조회
List<Member> resultList = query.getResultList();
결과가 없으면 빈 컬렉션을 반환한다.
Member result = query.getSingleResult();
결과가 없거나 1개보다 많으면 예외가 발생하기 때문에 정확하게 결과값이 한 개일 때 사용한다.
파라미터 바인딩
TypeQuery<Member> query = em
.createQuery("SELECT m FROM Member as m WHERE m.username = :username", Member.class);
query.setParameter("username", "User1");
List<Member> resultList = query.getResultList();
:username으로 정의된 파라미터에 값을 바인딩하기 위해서 setParameter(...) 메서드를 호출했다.
List<Member> resultList = em
.createQuery("SELECT m FROM Member as m WHERE m.username = :username", Member.class)
.setParameter("username", "User1")
.getResultList();
위와 같이 메서드 체이닝 방식으로 작성할 수도 있다.
List<Member> resultList = em
.createQuery("SELECT m FROM Member as m WHERE m.username = :username", Member.class)
.setParameter(1, "User1")
.getResultList();
위와 같이 파라미터 순서를 기준으로 값을 바인딩할 수도 있다.
프로젝션
SELECT 절에 조회 대상을 지정하는 것을 프로젝션이라고 한다.
프로젝션이 가능한 조회 대상으로는 엔티티, 임베디드 타입, 스칼라 타입이 있다.
- 엔티티 프로젝션: 영속성 컨텍스트에서 관리
"SELECT m FROM Member as m"
- 임베디드 타입 프로젝션: 영속성 컨텍스트에서 관리되지 않음
String query = "SELECT o.address FROM Order as o";
List<Address> addressList = em.createQuery(query, Address.class).getResultList();
- 스칼라 타입 프로젝션
Double avageOrderAmount = em
.createQuery("SELECT avg(o.orderAmount) FROM Order as o", Double.class)
.getSingleResult();
- 여러 값 조회
List resultList = em
.createQuery("SELECT m.username, m.age FROM Member as m")
.getResultList();
for (Object[] row : resultList) {
String username = (String) row[0];
Integer age = (Integer) row[1];
}
- new 사용
List<UserDTO> resultList = em
.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member as m", UserDTO.class)
.getResultList();
페이징 API
List<Member> members = em
.createQuery("SELECT m FROM Member as m ORDER BY m.username DESC", Member.class)
.setFirstResult(10)
.setMaxResults(20)
.getResultList();
위 코드는 11번째부터 30번째까지 20건의 데이터를 조회한다.
집합과 정렬
통계 정보를 구하기 위해 집합 함수를 사용하기도 한다.
"SELECT COUNT(m), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age) FROM Member as m"
집합 함수 사용에 있어 아래 내용들을 참고하자.
- null 값은 무시하여 통계에 합산되지 않는다.
- 값이 없는 경우 집합 함수를 사용하면 null이 된다. (count는 0)
- distinct를 집합 함수안에 사용할 수 있다.
- count(distinct 임베디드타입)은 지원하지 않는다.
"SELECT t.name, COUNT(m.age) as cnt" +
"FROM Member m LEFT JOIN m.team t" +
"GROUP BY t.name" +
"ORDER BY cnt"
위와 같이 JPQL을 작성하여 JOIN, GROUPING, 정렬을 처리할 수 있다.
JPQL 조인
String jpql = "SELECT m" +
"FROM Member m INNER JOIN m.team t" +
"WHERE t.name = :teamName";
List<Member> members = em
.createQuery(jpql, Member.class)
.setParameter("teamName", "teamA")
.getResultList();
위와 같이 JPQL을 작성하여 아래와 같은 조인 SQL 쿼리를 작성할 수 있다.
SELECT
m.id as id,
m.age as age,
m.team_id as team_id,
m.name as name
FROM
Member m
INNER JOIN
Team t
ON
m.team_id = t.id
WHERE
t.name = "teamA";
두 구문의 가장 큰 차이는 JPQL에서 INNER JOIN m.team t라고 명시하여 연관 필드를 사용하였다는 점이다.
이에 따라 SQL의 ON절을 적절히 생성하였다.
페치 조인
JPQL에서 성능 최적화를 위해 연관 엔티티를 한 번에 조회할 수 있는 페치 조인 기능을 제공한다.
"SELECT m FROM Member as m JOIN FETCH m.team"
위와 같이 JOIN FETCH 구문을 활용하여 Member와 Team을 한 번에 조회할 수 있다.
(Team에는 별칭을 사용할 수 없다)
SELECT
M.*,
T.*
FROM
Member m
INNER JOIN
Team t
ON
m.team_id = t.id;
페치조인 구문이 실행되면 위와 같은 SQL으로 변환되어 실행된다.
따라서 위 그림과 같은 객체 그래프를 가지는 엔티티를 반환받는다.
List<Member> members = em
.crateQuery("SELECT m FROM Member m JOIN FETCH m.team", Member.class)
.getResultList();
for (Member member : members) {
String username = member.getUsername();
// 지연 로딩 발생 안함
String teamname = member.getTeam().getName();
}
따라서 위와 같이 객체 그래프 탐색하는 코드를 실행시키더라도 이미 페치 조인으로 받아온 데이터는 지연 로딩이 발생하지 않는다.
List<Team> teams = em
.createQuery("SELECT t FROM Team t JOIN FETCH t.members WHERE t.name = 'teamA'", Team.class)
.getResultList();
for (Team team : teams) {
System.out.println("teamname = " + team.getName());
for (Member member : team.getMembers()) {
System.out.println("username = " + member.getUsername());
}
}
output console -------------------------
teamname = teamA
username = member1
username = member2
teamname = teamA
username = member1
username = member2
그러나 위와 같이 일대다 관계로 조회하게 된다면 결과가 두 번 출력된다.
SQL이 실행되면 위와 같은 데이터베이스 조회 결과를 얻게 된다.
조회한 데이터를 메모리에 올려서 위 그림과 같은 결과 객체가 생성된다.
따라서 결과 컬렉션에는 동일한 Team 객체가 두 개 들어있기 때문에 두 번의 결과가 출력되는 것이다.
"SELECT DISTINCT t" +
"FROM Team t JOIN FETCH t.members" +
"WHERE t.name = 'teamA'"
위와 같이 DISTINCT 구문을 이용한다고 하더라도 실제 SQL 결과는 동일할 것이다.
하지만 JPA가 조회 결과를 객체로 만드는 과정에서 중복 데이터를 걸러내어 동일한 Team 객체가 매핑되는 현상을 해소할 수 있다.
페치 조인 사용 시 참고할 점
사용하지 않는 엔티티를 로딩하는 것은 성능에 악영향을 끼칠 수 있기 때문에 엔티티에 적용하는 로딩 전략은 기본적으로 지연 로딩 전략을 사용하는 것이 좋다.@OneToMany(fetch = FetchType.LAZY)
페치 조인은 최적화가 필요한 시점에 사용하면 좋다.
그러나 페치 조인도 아래와 같은 한계점을 가지고 있다.
1. 대상에 별칭 불가
- 조인 대상 기준 정렬, 그룹핑 불가
- 2차 캐시와 함께 사용하는 경우 무결성 보장이 어려움
2. 둘 이상의 컬렉션 페치 불가
3. 페이징 API 사용 불가
- 하이버네이트가 수행하기 때문에 성능 이슈 발생할 수 있음
경로 표현식
경로 표현식은 .을 찍어서 필드를 참조하여 객체 그래프를 탐색하는 것이다.
이 때 참조하는 필드는 상태 필드와 연관 필드로 구분할 수 있다.
// 상태 필드 경로 탐색
select m.username, m.age
from Member m
상태 필드는 단순히 값을 저장하는 필드로 경로 탐색의 끝이다.
select o.member.team
from Order o
where o.product.name = 'productA'
and o.address.city = 'JINJU'
SELECT t.*
FROM Orders o
INNER JOIN Member m
ON o.member_id = m.id
INNER JOIN Team t
ON m.team_id = t.id
INNER JOIN Product p
ON o.product_id = p.id
WHERE p.name = 'productA'
AND o.city = 'JINJU';
연관 필드는 연관관계를 위한 필드로 참조하게 되면 내부적으로 조인이 발생한다.
JPQL의 select 절에 경로 탐색을 사용하지만 SQL의 from 절에 영향을 준다.
서브 쿼리 예제
// 나이가 평균보다 많은 회원
select m
from Member m
where m.age > (select avg(m2.age)
from Member m2)
// 한 건이라도 주문한 고객
select m
from Member m
where 0 < (select count(o)
from Orders o
where m = o.member)
// 한 건이라도 주문한 고객
select m
from Member m
where m.orders.size > 0
// 팀 A 소속인 회원
select m
from Member m
where exists (select t
from m.team t
where t.name = 'teamA')
// 주문량이 각 상품량의 재고량보다 많은 주문들
select o
from Orders 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)
// 나이가 20살 이상인 회원을 포함한 팀
select t
from Team t
where t IN (select t2
from Team t2
join t2.members m2
where m2.age > 20)
조건식 규칙
- 문자
- 작은 따옴표 사용 ('HELLO')
- 작은 따옴표 두 개 사용 시 작은 따옴표 하나로 인식 ('I''m')
- 숫자
- Long (10L)
- Double (10D)
- Float (10F)
- 날짜
- Date ({d '2024-12-23'})
- Time ({t '23-59-59'})
- Datetime ({ts '2012-12-03 10-11-11.123'})
- Boolean
- TRUE
- FALSE
- Enum
- 패키지명 전체 입력 (jpabook.MemberType.Admin)
컬렉션 식 예제
// 성공
select m
from Member m
where m.orders is not empty
// 실패
select m
from Member m
where m.order is null
// 성공
select t
from Team t
where :memberParam member of t.members
CASE 식 예제
// 기본 case
select
case
when m.age <= 10 then 'studentFee'
when m.age >= 60 then 'olderFee'
else 'generalFee'
end
from Member m
// 심플 case
select
case t.name
when 'teamA' then '110%'
when 'teamB' then '120%'
else '105%'
end
from Team t
// COALESCE
select
coalesce(m.username, 'unknown')
from Member m
// null if
select NULLIF(m.username, 'admin')
from Member m
다형성 쿼리 예제
상속 관계 엔티티를 대상으로 부모 엔티티를 조회하면 자신 엔티티도 같이 조회한다.
// Item, Book, Album, Movie 모두 조인해서 조회
List resultList = em
.createQuery("select i from Item i")
.getResultList();
// Type으로 설정된 엔티티만 조인해서 조회
List resultList = em
.createQuery("select i from Item i where type(i) IN (Book, Movie)")
.getResultList();
// 부모 타입을 자식 타입으로 핸들링
List resultList = em
.createQuery("select i from Item i where treat(i as Book).author='kim'")
.getResultList();
Named Query
- 동적 쿼리: 런타임에 JPQL을 문자로 완성해서 직접 넘기는 것
- 정적 쿼리: 미리 만들어진 쿼리를 사용하는 것
Named Query는 정적 쿼리로 @NamedQuery를 사용해 자바코드나 xml 파일에 미리 작성할 수 있다.
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"
)
public class Member {
...
}
List<Member> resultList = em
.createQuery("Member.findByUsername", Member.class)
.setParameter("username", "member1")
.getResultList();
QueryDSL
QueryDSL은 Criteria의 복잡하다는 단점을 보완할 수 있는 좋은 오픈소스 프로젝트이다.
사용하기 위해서는 querydsl-jpa, querydsl-apt 디펜던시를 추가해서 사용한다.
public void queryDSL() {
EntityManager em = emf.createEntityManager();
JPAQuery query = new JPAQuery(em);
QMemeber qMember = new QMemeber("m");
List<Member> members = query
.from(qMember)
.where(qMember.name.eq("회원1"))
.orderBy(qMember.name.desc())
.list(qMember);
}
QueryDSL을 사용할 때 JPAQuery 인스턴스를 생성하게 되는데, 이 때 EntityManager 타입의 인스턴스를 생성자에게 넘겨준다.
그리고 사용할 쿼리 타입을 생성하게 되는데 이 때 JPQL에서의 별칭을 생성자에 넘겨준다.
기본 Q 생성
쿼리 타입(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("goodProduct").and(item.price.gt(20000)))
.list(item);
select item
from Item i
where i.name = ?1 and i.price > ?2
결과 조회 메서드
쿼리 작성이 끝나고 결과 조회 메서드를 호출하면 실제 데이터베이스를 조회한다.
- uniqueResult()
- 조회 결과가 한 건일 때 사용
- 조회 결과가 없으면 null 반환
- 조회 결과가 다건이면 예외 발생
- 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);
페이징 처리는 offset(), limit() 메서드를 이용할 수 있다.
정렬 처리는 orderBy() 메서드를 사용할 수 있고 쿼리 타입이 제공하는 asc(), desc() 메서드를 사용할 수 있다.
그룹핑
query
.from(item)
.groupBy(item.price)
.having(item.price.gt(10000))
.list(item);
조인
조인은 innerJoin(), leftJoin(), rightJoin(), fullJoin()을 사용할 수 있다.
또한 fetch 조인이나 세타 조인을 사용할 수 있다.
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);
// 세타 조인 사용
query
.from(order, member)
.where(order.member.eq(member))
.list(order);
서브 쿼리
QItem item = QItem.item;
QItem itemSub = new QItem("itemSub");
query
.from(item)
.where(item.price.eq(
new JPASubQuery().from(itemSub).unique(itemSub.price.max())
))
.list(item);
프로젝션과 결과 반환
select절에 조회 대상을 지정하는 것을 프로젝션이라고 한다.
QItem item = QItem.item;
List<String> result = query
.from(item)
.list(item.name);
for (String name : result) {
...
}
위와 같이 프로젝션 대상이 하나이면 해당 타입으로 반환한다.
QItem item = QItem.item;
List<Tuple> result = query
.from(item)
.list(item.name, item.price);
for (Tuple tuple : result) {
String name = tuple.get(item.name);
String price = tuple.get(item.price);
}
프로젝션 대상이 여러 개면 기본적으로 Tuple 타입의 인스턴스에 값이 담긴다.
public class ItemDTO {
private String username;
private int price;
}
QItem item = QItem.item;
List<ItemDTO> result = query
.from(item)
.list(Projections.bean(ItemDTO.class, item.name.as("username"), item.price));
QItem item = QItem.item;
List<ItemDTO> result = query
.from(item)
.list(Projections.fields(ItemDTO.class, item.name.as("username"), item.price);
QItem item = QItem.item;
List<ItemDTO> result = query
.from(item)
.list(Projections.constructor(ItemDTO.class, item.name.as("username"), item.price);
ItemDTO 인스턴스에 결과를 담기 위해서 위와 같이 빈 생성, 필드 주입, 생성자 주입 등을 이용할 수 있다.
수정, 삭제 배치 쿼리
QItem item = QItem.item;
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
long count = updateClause
.where(item.name.eq("jpa tutorial"))
.set(item.price, item.price.add(100))
.execute();
QItem item = QItem.item;
JPADeleteClause deleteClause = new JPADeleteClause(em, item);
long count = deleteClause
.where(item.name.eq("jpa tutorial"))
.execute();
동적 쿼리
SearchParam param = new SearchParam();
param.setName("jpa");
param.setPrice(11000);
QItem item = QItem.item;
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(param.getName()) {
builder.and(item.name.contains(param.getName()));
}
if (param.getPrice != null) {
builder.and(item.price.gt(param.getPrice()));
}
List<Item> result = query
.from(item)
.where(builder)
.list(item);
메서드 위임
public class ItemExpression {
@QueryDelegate(Item.class)
public static BooleanExpression isExpensive(QItem item, Integer price) {
return item.price.gt(price);
}
}
위와 같이 @QueryDelegate로 적용할 엔티티를 지정한다.
query
.from(item)
.where(item.isExpensive(1000))
.list(item);
위임 메서드를 지정해놓은 엔티티에 대해서는 위와 같이 간략한 코드를 작성할 수 있다.
네이티브 쿼리
JPQL은 특정 데이터베이스에 종속적인 기능을 제공하지 않는다.
그러나 특정 데이터베이스에서만 지원하는 함수, 쿼리 힌트, 스토어드 프로시즈 등을 이용하기 위한 방법을 제공하고 있는데 가장 대표적인 기능이 네이티브 쿼리이다.
String sql = "select id, age, team_id" +
"from member" +
"where age > ?";
List<Member> resultList1 = em.createNativeQuery(sql, Member.class)
.setParameter(1, 20)
.getResultList();
List<Object[]> resultList1 = em.createNativeQuery(sql, Member.class)
.setParameter(1, 20)
.getResultList();
String sql = "select m.id, age, name, team_id, i.order_count" +
"from member m" +
"left join " +
" (select im.id, count(*) as order_count" +
" from orders o, member im" +
" where o.member_id = im.id) i" +
"on m.id = i.id";
List<Object[]> resultList = em.createNativeQuery(sql, "memberWithOrderCount")
.getResultList();
for (Object[] row : resultList) {
Member member = (Member) row[0];
BigInteger orderCount = (BigInteger) row[1];
...
}
@Entity
@SqlResultSetMapping(name = "memberWithOrderCount",
entities = { @EntityResult(entityClass = Member.class) },
columns = { @ColumnResult(name = "ORDER_COUNT") })
public class Member { ... }
벌크 연산
엔티티를 수정하기 위해서 각 엔티티를 대상으로 remove()를 수행하는 방법이 있다.
그러나 이 방법으로 수 많은 데이터를 하나씩 처리하는 것은 많은 비용이 발생한다.
이러한 경우 한 번에 수정하거나 삭제하는 벌크 연산을 사용한다.
String sqlString = "update Product p" +
"set p.price = p.price * 1.1" +
"where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(sqlString)
.setParameter("stockAmount", 10)
.executeUpdate();
위와 같이 코드를 작성하여 벌크 연산을 수행하면 되지만, 영속성 컨텍스트를 거치지 않기 때문에 발생할 수 있는 문제가 있다.
Product productA = em
.createQuery("select p from Product p where p.name = :name", Product.class)
.setParameter("name", "productA")
.getSingleResult();
System.out.println("Before bulk operation: " + productA.getPrice());
em.createQuery("update Product p set p.price = p.price * 1.1")
.executeUpdate();
System.out.println("After bulk operation: " + productA.getPrice());
위 코드를 보면 벌크 연산을 직접 데이터베이스에 수행하기 때문에 영속성 컨텍스트에 존재하는 Product 인스턴스의 price 값이 갱신되지 않았다.
이와 같은 문제를 해결하기 위해서 아래의 방법을 참고하자.
1. em.refresh(productA) 사용 (데이터베이스에서 상품 다시 조회)
2. 벌크 연산 먼저 실행 후 영속성 컨텍스트 초기화
영속성 컨텍스트와 JPQL
JPQL의 프로젝션 대상이 엔티티인 경우에만 영속성 컨텍스트에서 관리된다.
(임베디드 타입, 값 타입 등 다른 타입을 조회하는 경우에는 영속성 컨텍스트에서 관리되지 않는다)
em.find(Member.class, "member1");
List<Member> resultList = em
.createQuery("select m from Member m" , Member.class)
.getResultList();
위와 같이 영속성 컨텍스트에 이미 존재하는 엔티티를 JPQL로 다시 조회하게 되면 영속성 컨텍스트에 있던 엔티티를 반환한다.
즉 member1을 키로 가지는 Member 엔티티를 영속성 컨텍스트에 있는 값으로 대체한다.
두 개의 값이 다른 경우이더라도 READ_COMMITED를 보장하기 위해서인 것 같다..
또한 EntityManager 클래스의 find(...) 메서드의 경우에는 우선 영속성 컨텍스트에서 엔티티를 찾고 없는 경우에만 데이터베이스를 조회한다. (1차 캐시)
반면에 JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다.
플러시 모드와 JPQL
플러시는 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 작업이다.
JPQL은 항상 데이터베이스를 조회하기 때문에 올바른 결과를 얻기 위해 실행하기 전 플러시를 호출해서 현재 변경 상태를 먼저 반영한다.
플러시 모드는 FlushModeType.AUTO가 기본 값이지만, FlushModeType.COMMIT으로 설정하게 된다면 커밋 시에만 플러시를 호출하고 쿼리 실행 시에는 호출하지 않는다.
em.setFlushMode(FlushModeType.COMMIT);
product.setPrice(2000);
Product product = em
.createQuery("select p from Product p where p.price = 2000", Product.class)
.getSingleResult();
위 코드를 살펴보면 플러시모드를 COMMIT으로 설정했기 때문에 쿼리 실행 전 플러시를 수행하지 않기 때문에 정확하게 원하는 결과를 얻을 수 없다.
하나의 로직에 여러 개의 JPQL이 발생하여 여러 개의 플러시가 수행되는 경우에는 성능 이슈가 발생할 수 있다.
이와 같은 경우에는 플러시 모드를 COMMIT으로 설정해서 성능을 최적화할 수 있다.
다음 글: https://gojs.tistory.com/56
Spring Data JPA 알아보기
JPA를 사용해서 개발을 진행하는 경우에도 중복적으로 코드를 입력하게된다.Spring Data JPA는 이에 대해 보다 간편하게 개발할 수 있도록 지원하는 프로젝트이다. org.springframework.data spring-data-jpa 3.
gojs.tistory.com
'공부 > JPA' 카테고리의 다른 글
JPA에서 트랜잭션을 고려한 영속성 관리 전략 (4) | 2024.10.02 |
---|---|
Spring Data JPA 활용하기 (0) | 2024.09.22 |
JPA 값 타입 구현하기 (0) | 2024.07.27 |
JPA 프록시와 로딩 전략 (0) | 2024.07.27 |
JPA Entity 간 연관관계 매핑 (2) (0) | 2024.07.27 |