자바 ORM 표준 JPA 프로그래밍 - 기본편 - sec11
출처 : JPA 기본편
.을 찍어 객체 그래프를 탐색하는 것
select m.username -> 상태 필드
엔티티안에서 값을 바로 찍어내는 것
from Member m
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'
상태 필드: 경로 탐색의 끝, 탐색❌
JPQL: select m.username, m.age from Member m
SQL: select m.username, m.age from Member m
단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색O
String query = "select m.team From Member m";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
String query = "select t.members From Team t";
Collection result = em.createQuery(query, Collection.class)
.getResultList();
size같은 경우에는 가져올 수 있으나 나머지들은 .을 찍어도 나오는게 없음
join의 경우 성능 튜닝에 지대한 영향을 줌 그래서 막 join이 되면 안되게 관리 해야 함
• FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
"select m.username From Team t join t.members m"
select m from Member m join m.team t
select m.team from Member m
항상 내부 조인
select o.member.team from Order o
-> 성공 => join이 2번 일어남
select t.members from Team
-> 성공
select t.members.username from Team t
-> 실패 => 컬렉션에서는 더 들어갈 수 없음 from 별칭을 사용하면 모를까
select m.username from Team t join t.members m
-> 성공
SQL 조인 종류X
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
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);
Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);
em.flush();
em.clear();
String jpql = "select m from Member m";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for (Member member : members) {
System.out.println("username = " + member.getUsername() + ", " + "teamName = " + member.getTeam().name());
}
/* 결과값
username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B
*/
첫 번재 루프를 돌면서 회원1꺼를 가져오게 되겠죠~ 프록시가 .getName()으로 값을 직접 사용하는 순간에 영속성 컨텍스트에 내놔라고 하면서 team을 조회하는 쿼리가 나가게 됨(이 순간에 teamA가 영속성 컨텍스트에 들어가게 됨)
회원2도 1이랑 같은 팀이고 teamA는 이제 1차 캐시에 있으니깐 따로 쿼리문이 나가지 않고 바로 가져오게 됨
회원3는 teamB인데 이 팀은 아직 영속성에 없어서 회원1이 겪은 상황을 동일하게 겪게 됨
=> N(팀을 찾기 위한 쿼리)+1(회원을 찾기 위해 날리는 맨 첫 번째 쿼리) 문제 발생
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());
}
팀의 쿼리가 따로 안나감, 애초에 팀이랑 멤버를 다 들고 왔음 => 쿼리가 날라가서 result에 담기는 순간에 team은 프록시가 아니라 진짜 데이터가 담김
일대다 관계, 컬렉션 페치 조인
• [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";
List<Team> result = em.createQuery(jpql, Team.class)
.getResultList();
for (Team team : result) {
System.out.println("team = " + member.getName() + "|members= " + team.getMembers().size());
}
/*결과값
team = 팀A|members=2
team = 팀A|members=2
team = 팀B|members=1
데이터는 가져오지만 뻥튀기 발생
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);
}
}
/* 결과값
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
*/
SQL의 DISTINCT는 중복된 결과를 제거하는 명령
JPQL의 DISTINCT 2가지 기능 제공
1. SQL에 DISTINCT를 추가
2. 애플리케이션에서 엔티티 중복 제거
select distinct t from Team t join fetch t.members where t.name = ‘팀A’
이렇게 했을 때, SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과
에서 중복제거 실패! (정말 모든게 일치해야 데이터베이스에서 삭제를 해줌)
그래서! DISTINCT가 추가로 애플리케이션에서 중복 제거시도 => 같은 식별자를 가진 Team 엔티티 제거
반대로 다대일은 뻥튀기가 안됨(Member에서 team으로 가는 경우)
일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
<일반 조인>
String query = select t from Team t join t.members m
team만 가져오는 걸 볼 수 있음 + 데이터 뻥튀기도 됨
또한 컬렉션은 프록시가 아니지만 데이터가 로딩 시점에 로딩이 안되있기 때문에 members를 조회하는 쿼리가 또 나가게 됨
페치 조인은 연관된 엔티티를 함께 조회함
<페치 조인>
select t from Team t join fetch t.members
쿼리 한 번에 select절에 모든 내용을 담아서 나가서 우리가 원하는 깔끔한 결과 완성
select t From Team t join fetch t.members as m where m.members 뭐시기 안됨"
String jpql = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(jpql, Team.class)
.setFirstResult(0)
.setMaxResult(1)
.getResultList();
WARN: HHH000104: firstResult/MaxResults specified with collection fetch; applying in memory!
페이징 쿼리가 나가지 않음 => DB에서 이 팀에 대한 쿼리를 다 끌고옴
• 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
• 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
query = "select m From Member m join fetch m.team t";
String query = "select t from Team t";
for (Team team : result) {
System.out.println("team = " + member.getName() + "|members= " + team.getMembers().size());
}
우선 팀을 찾는 쿼리를 내보냄
루프를 돌면서 팀과 관련된 멤버들을 lazy로딩을 통해서 가져옴 그래서 팀 B까지 해서 총 3번의 쿼리가 나가게 됨
=> 성능이 안 나옴, 불필요하게 쿼리가 많이 나게 되니깐
3. BetchSize(size=@)
레이지 로딩을 통해서 team을 가져올 때 betchsize에서 적은 숫자만큼 한 번에 끌고 옴
4. 글로벌 세팅
persistence.xml에 hibernate.default_batch_fetch_size value=100
이런 식으로 적용하면 3번과 똑같은 효과를 낼 수 있음
조회 대상을 특정 자식으로 한정하고자 할 때 ex) 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’)
자바의 타입 캐스팅과 유사
• [JPQL]
select i from Item i
where treat(i as Book).auther = ‘kim’
• [SQL]
select i.* from Item i
where i.DTYPE = ‘B’ and i.auther = ‘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=?
*/
미리 정의해서 이름을 부여해두고 사용하는 JPQL
@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) 재고가 10개 미만인 모든 상품의 가격을 10% 상승하고자 할 때
JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행
1. 재고가 10개 미만인 상품을 리스트로 조회한다.
2. 상품 엔티티의 가격을 10% 증가한다.
3. 트랜잭션 커밋 시점에 변경감지가 동작한다.
• 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행
쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
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();
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
• 벌크 연산을 먼저 실행
• 벌크 연산 수행 후 영속성 컨텍스트 초기화