11.1 경로 표현식
경로 표현식
select m.username -> 상태 필드
from Member m
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'
- 점을 찍어서 객체 그래프를 탐색할 수 있는 식을 경로 표현식이라고 한다.
- 상태 필드(state field): 단순히 값을 저장하기 위한 필드, 경로 탐색의 끝이며 더이상 탐색 X (불가)
- 연관 필드(association field): 연관 관계를 위한 필드
- 단일 값 연관 경로
- 대상이 엔티티인 경우를 말한다. ex) @ManyToOne, @OneToOne
- 묵시적 내부 조인 발생(join키워드 직접 사용), 탐색 O
- 컬렉션 값 연관 경로
- 대상이 컬렉션인 경우, ex) @ManyToMany, @OneToMany
- 묵시적 내부 조인 발생, 탐색 X
- FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
- 단일 값 연관 경로
- 실무에서 명시적 조인을(JOIN 키워드 직접 사용) 써야 덜 헷갈리고 에러가 발생하지 않는다.
Ex) 단일 값 연관 경로
<hide/>
package jpql;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member1 = new Member();
member1.setUsername("관리자1");
em.persist(member1);
Member member2 = new Member();
member2.setUsername("관리자2");
em.persist(member2);
em.flush();
em.clear();
String query = "SELECT m.team FROM Member m";
List<Team> result = em.createQuery(query, Team.class).getResultList();
for (Team s : result) {
System.out.println("s = " + s);
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 묵시적 내부 조인이 발생
- 실무에서 이렇게 발생하도록 짜지 않는다.
<hide/>
/* SELECT
m.team
FROM
Member m */ select
team1_.id as id1_3_,
team1_.name as name2_3_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
Ex) 컬렉션 값 연관 경로
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Member member1 = new Member();
member1.setUsername("관리자1");
em.persist(member1);
Member member2 = new Member();
member2.setUsername("관리자2");
em.persist(member2);
em.flush();
em.clear();
String query = "SELECT t.members FROM Team t";
Collection result = em.createQuery(query, Collection.class).getResultList();
for (Object o : result) {
System.out.println("o = " + o);
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 값이 없어서 출력은 안 된다.
- t.members 뒤에 점을 찍으면 뭔가 추천되는 값이 없다. 컬렉션 자체를 가리키기 때문이다.
<hide/>
/* SELECT
t.members
FROM
Team t */ select
members1_.id as id1_0_,
members1_.age as age2_0_,
members1_.TEAM_ID as TEAM_ID5_0_,
members1_.type as type3_0_,
members1_.username as username4_0_
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
Ex) 상태 필드 - inner join 없음
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team team = new Team();
em.persist(team);
Member member1 = new Member();
member1.setUsername("관리자1");
member1.setTeam(team);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("관리자2");
member2.setTeam(team);
em.persist(member2);
em.flush();
em.clear();
String query = "SELECT t.members.size FROM Team t";
Integer result = em.createQuery(query, Integer.class).getSingleResult();
System.out.println("res = " + result);
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
<hide/>
/* SELECT
t.members.size
FROM
Team t */ select
(select
count(members1_.TEAM_ID)
from
Member members1_
where
team0_.id=members1_.TEAM_ID) as col_0_0_
from
Team team0_
res = 2
Ex) 컬렉션 값 연관 경로
- 그런데 t.members 뒤에는 더이상 탐색이 안된다.
- 따라서, 명시적 조인을 써야한다. 별칭을 설정하면 별칭을 통해 탐색 가능하기 때문이다.
- 권장: SELECT m FROM Team t JOIN t.members m
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
Team team = new Team();
em.persist(team);
Member member1 = new Member();
member1.setUsername("관리자1");
member1.setTeam(team);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("관리자2");
member2.setTeam(team);
em.persist(member2);
em.flush();
em.clear();
String query = "SELECT t.members FROM Team t";
Collection result = em.createQuery(query, Collection.class).getResultList();
System.out.println("res = " + result);
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
<hide/>
/* SELECT
t.members
FROM
Team t */ select
members1_.id as id1_0_,
members1_.age as age2_0_,
members1_.TEAM_ID as TEAM_ID5_0_,
members1_.type as type3_0_,
members1_.username as username4_0_
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
res = [Member{id=2, username='관리자1', age=0}, Member{id=3, username='관리자2', age=0}]
명시작 조인, 묵시적 조인
- 명시적 조인: "JOIN" 키워드를 직접 사용
- 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생한다. (내부 조인만 가능)
- 외부 조인하려면 명시적 조인을 해야한다.
Ex) 경로 표현식
- 컬렉션은 자기 자신 또는 size() 까지만 가져올 수 있고 더 이상 탐색은 불가능하다.
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 튜닝에 중요한 포인트
11.2 페치 조인(fetch join) (1) - 기본
fetch join이란?
- 지연로딩으로 세팅해도 즉시 로딩(EAGER)로 가져오는 쿼리이다. (지연 로딩보다 페치 조인이 우선)
- 페치 조인은 SQL 조인의 종류가 아니다.
- JPQL에서 성능 최적화를 위해 제공한다.
- 연관된 엔티티나 컬렉션을 SQL로 한번에 조회하는 기능이다.
엔티티 페치 조인
- 회원을 조회하면서 연관된 팀도 함께 조회한다. SQL 한방 쿼리
- SQL을 보면 회원 뿐만 아니라 팀(T.*) 도 함께 SELECT
Ex) 엔티티 페치 조인 - JPQL에 따라 실제 실행되는 SQL
[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
Ex) 페치 조인이 나오게 된 이유
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
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 query = "SELECT m FROM Member m";
List<Member> result = em.createQuery(query, Member.class).getResultList();
for (Member member : result) {
System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- LAZY 설정되어 있어서 프록시로 들어오고 getName() 호출한 시점에야 데이터베이스에 쿼리를 날린다.
- 쿼리가 너무 많이 나온다. "N + 1" 문제
- LAZY, EAGER 둘다 "N + 1" 문제가 발생한다
- fetch join으로 해결 가능
<hide/>
/* SELECT
m
FROM
Member m */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as TEAM_ID5_0_,
member0_.type as type3_0_,
member0_.username as username4_0_
from
Member member0_
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
member = 회원1, 팀A
member = 회원2, 팀A
Hibernate:
select
team0_.id as id1_3_0_,
team0_.name as name2_3_0_
from
Team team0_
where
team0_.id=?
member = 회원3, 팀B
Ex) fetch join - ManyToOne
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
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 query = "SELECT m FROM Member m join fetch m.team";
List<Member> result = em.createQuery(query, Member.class).getResultList();
for (Member member : result) {
System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- join으로 한방 쿼리가 나간다. 멤버와 팀 inner join
- for문에서 getTeam()으로 얻는 team은 프록시가 아니다.
- createQuery() 실행되는 시점에 얻은 팀은 fetch join으로 얻은 실제 엔티티(프록시 아님)
- 지연로딩 없이 깔끔하게 나온다.
<hide/>
Hibernate:
/* SELECT
m
FROM
Member m
join
fetch m.team */ select
member0_.id as id1_0_0_,
team1_.id as id1_3_1_,
member0_.age as age2_0_0_,
member0_.TEAM_ID as TEAM_ID5_0_0_,
member0_.type as type3_0_0_,
member0_.username as username4_0_0_,
team1_.name as name2_3_1_
from
Member member0_
inner join
Team team1_
on member0_.TEAM_ID=team1_.id
member = 회원1, 팀A
member = 회원2, 팀A
member = 회원3, 팀B
컬렉션 페치 조인
- 일대다 관계
[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'
Ex) 컬렉션 페치 조인 시 주의사항
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
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 query = "SELECT t FROM Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class).getResultList();
for (Team team : result) {
System.out.println("team = " + team.getName() + ", member: " + team.getMembers().size() + "명");
for(Member member : team.getMembers()){
System.out.println(" => member = " + member);
}
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
<hide/>
/* SELECT
t
FROM
Team t
join
fetch t.members */ select
team0_.id as id1_3_0_,
members1_.id as id1_0_1_,
team0_.name as name2_3_0_,
members1_.age as age2_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_1_,
members1_.type as type3_0_1_,
members1_.username as username4_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_0__,
members1_.id as id1_0_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
- 팀A 내용이 왜 2개가 출력될까?
- 데이터베이스 특성 때문에 일대다 조인은 여러 개 쿼리가 실행될 수 있다.
- 팀, 멤버 테이블을 조인하는데 두 줄이 나온다. (멤버의 pk가 다르니까)
- JPA는 이 시점에 개입 불가능하다.
Ex) FETCH JOIN이 있는 경우와 없는 경우의 result size 차이
- JOIN FETCH 없을 때 => result size: 2
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
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 query = "SELECT t FROM Team t";
List<Team> result = em.createQuery(query, Team.class).getResultList();
System.out.println("result size : " + result.size());
// for (Team team : result) {
// System.out.println("team = " + team.getName() + ", member: " + team.getMembers().size() + "명");
// for(Member member : team.getMembers()){
// System.out.println(" => member = " + member);
// }
// }
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
- JOIN FETCH 있을 때 => result size = 3
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
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 query = "SELECT t FROM Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class).getResultList();
System.out.println("result size : " + result.size());
// for (Team team : result) {
// System.out.println("team = " + team.getName() + ", member: " + team.getMembers().size() + "명");
// for(Member member : team.getMembers()){
// System.out.println(" => member = " + member);
// }
// }
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 위 결과가 왜 다를까?
- 조인하면서 데이터가 커지기 때문이다.
- 일대다 조인하면 데이터가 더 커질 수 있다는 특성 때문에 이런 현상이 생긴다.
페치 조인과 DISTINCT
- SQL의 DISTINCT는 중복된 결과를 제거하는 명령이다.
- JPQL의 DISTINCT는 2가지 기능 제공
- SQL에 DISTINCT 추가
- 애플리케이션에서 엔티티의 중복을 제거한다.
- DISTINCT가 추가로 애플리케이션에서 중복 제거를 시도한다.
- 같은 식별자를 가진 Team 엔티티를 제거한다.
Ex) DISTINCT
- SQL에 DISTINCT를 추가하지만 데이터가 다르기 때문에 SQL 결과에서 중복 제거 실패한다.
select distinct t
from Team t join fetch t.members
where t.name = ‘팀A’
Ex) DISTINCT
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
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 query = "SELECT DISTINCT t FROM Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class).getResultList();
System.out.println("result size : " + result.size());
for (Team team : result) {
System.out.println("team = " + team.getName() + ", member: " + team.getMembers().size() + "명");
for(Member member : team.getMembers()){
System.out.println(" => member = " + member);
}
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- 컬렉션에서 중복을 제거해준다. => size = 2
<hide/>
/* SELECT
DISTINCT t
FROM
Team t
join
fetch t.members */ select
distinct team0_.id as id1_3_0_,
members1_.id as id1_0_1_,
team0_.name as name2_3_0_,
members1_.age as age2_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_1_,
members1_.type as type3_0_1_,
members1_.username as username4_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_0__,
members1_.id as id1_0_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
result size : 2
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
페치 조인과 일반 조인의 차이
- 일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않는다.
- JPQL은 결과를 반환할 때, 연관 관계를 고려하지 않는다.
- 단지, SELECT 절에 지정한 엔티티만 조회할 뿐이다.
- 아래 예제에서 팀 엔티티만 조회하고 회원 엔티티는 조회하지 않는다.
- 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회한다. (즉시 로딩)
- 페치 조인을 사용하면 즉시 로딩이 일어난다고 보면 된다.
- 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념이다. 한 방 쿼리
[JPQL]
select t
from Team t join t.members m
where t.name = ‘팀A'
[SQL]
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
Ex) 페치 조인과 일반 조인의 차이
- 일반 조인
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
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 query = "SELECT t FROM Team t join t.members";
List<Team> result = em.createQuery(query, Team.class).getResultList();
System.out.println("result size : " + result.size());
for (Team team : result) {
System.out.println("team = " + team.getName() + ", member: " + team.getMembers().size() + "명");
for(Member member : team.getMembers()){
System.out.println(" => member = " + member);
}
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과 - 일반 조인
- 조인했는데 SELECT절에서 팀 id랑 name만 나온다. (윗 부분 JOIN)
- 데이터 늘어나서 3으로 출력된다.
- for문 돌릴 때, members가 초기화되지 않는 상태이다.
- 컬렉션이 프록시는 아니지만 데이터가 없어서 쿼리가 쭉 나온다. (맨 아래)
<hide/>
/* SELECT
t
FROM
Team t
join
t.members */ select
team0_.id as id1_3_,
team0_.name as name2_3_
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
result size : 3
Hibernate:
select
members0_.TEAM_ID as TEAM_ID5_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.age as age2_0_1_,
members0_.TEAM_ID as TEAM_ID5_0_1_,
members0_.type as type3_0_1_,
members0_.username as username4_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
Hibernate:
select
members0_.TEAM_ID as TEAM_ID5_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.age as age2_0_1_,
members0_.TEAM_ID as TEAM_ID5_0_1_,
members0_.type as type3_0_1_,
members0_.username as username4_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
team = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
- fetch join 추가
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
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 query = "SELECT t FROM Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class).getResultList();
System.out.println("result size : " + result.size());
for (Team team : result) {
System.out.println("team = " + team.getName() + ", member: " + team.getMembers().size() + "명");
for(Member member : team.getMembers()){
System.out.println(" => member = " + member);
}
}
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과 - fetch join
- 일반 조인과 다르게 맨 처음 SELECT 절에서 member.id, name 뿐만 아니라 멤버에 대한 내용도 모두 가져온다.
- 나중에 for문을 돌려서 getName(), getMember()를 출력하면 한 번에 멤버 정보를 가져온다.
<hide/>
/* SELECT
t
FROM
Team t
join
fetch t.members */ select
team0_.id as id1_3_0_,
members1_.id as id1_0_1_,
team0_.name as name2_3_0_,
members1_.age as age2_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_1_,
members1_.type as type3_0_1_,
members1_.username as username4_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_0__,
members1_.id as id1_0_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
result size : 3
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
11.3 페치 조인 2 - 한계
페치 조인 특징과 한계
- 페치 조인 대상에는 원칙적으로 별칭을 줄 수 없다.
- 하이버네이트는 가능
- 관례적으로 별칭을 쓰지 않도록 한다. (페치 조인을 여러 단계로 하는 경우는 가끔 별칭을 쓰기도 한다.)
- 가급적 사용 X
- 페치 조인은 연관된 것들을 모두 가져오는 기능이다.
- ex) 팀 안에 5명이 있는데 그 중 3 명만 조회하는 경우? => 위험하다. (지워지거나 오작동 가능성)
- 사상이 안 맞는 "정합성(무모순성, Data Consistency)" 이슈 때문
- 이런 경우는 처음부터 5명 조회하는 SELECT 5명 쿼리를 날려야한다.
- 둘 이상의 컬렉션을 페치 조인할 수 없다.
- 이 부분도 데이터 정합성 이슈가 있다.
- ex) Team의 orders?
- 일대다도 데이터 뻥튀기 되는데 이 경우는 일대다"대"다 => 데이터가 예상치 못하게 뻥튀기 가능하다.
- 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults) 사용 불가
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능 (데이터 뻥튀기가 안 되서 가능)
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징 (매우 위험)
- 페이징은 철저히 데이터베이스 중심적이다. rows를 줄이려는 기능
- 연관된 엔티티들은 SQL 한 번으로 조회된다. => 성능 최적화
- 페치 조인이 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다.
- @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
- 실무에서 글로벌 로딩 전략은 모두 지연 로딩이다.
- 최적화 필요한 곳에만 페치 조인 적용하면 대부분 성능 문제가 해결된다.
페치 조인 정리
- 모든 걸을 페치 조인으로 해결할 수는 없다.
- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야하면 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터만 조회해서 DTO로 반환하는 것이 효과적이다.
Ex) 컬렉션 페치조인과 페이징 API
<hide/>
List<Team> result = em.createQuery(query, Team.class)
.setFirstResult(0) // 조회 시작할 위치
.setMaxResults(1) // 조회할 데이터 수
.getResultList();
Note) 실행 결과
- DB에서 팀에 대한 데이터를 다 끌고 온다. 장애 나기 좋은 조건이다.
<hide/>
Hibernate:
/* SELECT
t
FROM
Team t
join
fetch t.members */ select
team0_.id as id1_3_0_,
members1_.id as id1_0_1_,
team0_.name as name2_3_0_,
members1_.age as age2_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_1_,
members1_.type as type3_0_1_,
members1_.username as username4_0_1_,
members1_.TEAM_ID as TEAM_ID5_0_0__,
members1_.id as id1_0_0__
from
Team team0_
inner join
Member members1_
on team0_.id=members1_.TEAM_ID
result size : 1
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
- 경고 - 페치조인과 페이징
WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
Ex) 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
- 기존의 쿼리
String query = "SELECT t FROM Team t join fetch t.members";
- 바뀐 쿼리 - 멤버부터 시작
String query = "SELECT m FROM Member m join fetch m.team t";
Note) 실행 결과 - 회원 => 팀 다대일은 페이징에 문제가 없다.
Ex) BatchSize를 쓰게 된 배경
- BatchSize는 하위 엔티티를 로딩할 때, 한번에 상위 엔티티 ID를 지정한 숫자만큼 in Query로 로딩한다.
<hide/>
String query = "SELECT t FROM Team t";
List<Team> result = em.createQuery(query, Team.class)
.setFirstResult(0)
.setMaxResults(2)
.getResultList();
Note) 실행 결과
- lazy 로딩이 2번 일어난다. 성능 안 좋다.
- 팀을 10개 조회하면 연관된 멤버를 조회하는데 추가적으로 쿼리가 10번 이상 나갈 것이다.
- 해결 방법: @BatchSize()
<hide/>
/* SELECT
t
FROM
Team t */ select
team0_.id as id1_3_,
team0_.name as name2_3_
from
Team team0_ limit ?
result size : 2
-- 여기부터 중요
Hibernate:
select
members0_.TEAM_ID as TEAM_ID5_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.age as age2_0_1_,
members0_.TEAM_ID as TEAM_ID5_0_1_,
members0_.type as type3_0_1_,
members0_.username as username4_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
Hibernate:
select
members0_.TEAM_ID as TEAM_ID5_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.age as age2_0_1_,
members0_.TEAM_ID as TEAM_ID5_0_1_,
members0_.type as type3_0_1_,
members0_.username as username4_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
team = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
Ex) @BatchSize
- 팀 클래스
<hide/>
@OneToMany(mappedBy = "team")
@BatchSize(size = 100)
private List<Member> members = new ArrayList<>();
- Main 클래스
<hide/>
String query = "SELECT t FROM Team t";
List<Team> result = em.createQuery(query, Team.class)
.setFirstResult(0)
.setMaxResults(2)
.getResultList();
Note) 실행 결과
- 사이즈는 2개로 정상적으로 나온다.
- 그런데 그 아래 쿼리를 보면?
- 아래의 select에서 멤버를 가져오는데 in Query 안에 "?, ?"라고 나온다. TeamA, TeamB와 연관된 내용을 모두 가져온 것이다.
- (기존에 @BatchSize를 쓰기 전에는 @ where members0_.TEAM_ID=? 이었다.)
- 팀에서 멤버스를 가져올 때, LAZY 로딩 상태이다.
- 매인 클래스에서 List<>에 담긴 Team을 in Query로 100 개씩 넘긴다.
- 쿼리 하나 + lazy 로딩 쿼리 n개 => n + 1 문제
- 페치 조인 또는 batchSize() 로 해결한다.
- 아래의 select에서 멤버를 가져오는데 in Query 안에 "?, ?"라고 나온다. TeamA, TeamB와 연관된 내용을 모두 가져온 것이다.
<hide/>
/* SELECT
t
FROM
Team t */ select
team0_.id as id1_3_,
team0_.name as name2_3_
from
Team team0_ limit ?
result size : 2
Hibernate:
/* load one-to-many jpql.Team.members */ select
members0_.TEAM_ID as TEAM_ID5_0_1_,
members0_.id as id1_0_1_,
members0_.id as id1_0_0_,
members0_.age as age2_0_0_,
members0_.TEAM_ID as TEAM_ID5_0_0_,
members0_.type as type3_0_0_,
members0_.username as username4_0_0_
from
Member members0_
where
members0_.TEAM_ID in (
?, ?
)
team = 팀A, member: 2명
=> member = Member{id=3, username='회원1', age=0}
=> member = Member{id=4, username='회원2', age=0}
team = 팀B, member: 1명
=> member = Member{id=5, username='회원3', age=0}
- @BatchSize 대신에 properties에 추가하더라도 같은 기능을 사용 가능
- 쿼리가 n + 1이 아니라 테이블 수를 맞출 수 있어서 최적화 가능하다.
<property name="hibernate.default_batch_fetch_size" value="100" />
Note) 실행 결과 - 결과는 동일하다.
11.4 다형성 쿼리
TYPE
- 조회 대상을 특정 자식으로 한정한다.
Ex) Item 중에 Book(B), Movie(M)를 조회
[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)
- 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. (Java의 타입 캐스팅과 유사)
- FROM, WHERE, SELECT(하이버네이트 지원) 사용
Ex) TREAT
- 부모 Item, 자식 Book
- 다운 캐스팅 처럼 사용
[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’
11.5 JPQL 엔티티 직접 사용
엔티티 직접 사용 - 기본 키 값
- JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
- COUNT() 안에 멤버의 기본 키를 직접 넣어 버린다.
- 비교하기 위한 값은 PK니까 ID가 매개변수로 들어간다.
[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
Ex) 엔티티를 파라미터로 전달하나 식별자를 전달하나 결과는 동일
<hide/>
[엔티티를 파라미터로 전달]
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=?
엔티티 직접 사용 - 외래 키 값
Ex)
- 아래에서 where절에 m.team: 외래 키, 파라미터 team은 기본 키
<hide/>
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=?
11.6 Named 쿼리
Named 쿼리 - 정적 쿼리
- 엔티티에 이름이 부여된 쿼리를 미리 등록해두고 사용하는 기능을 말한다. 쿼리를 재활용한다.
- 정적 쿼리만 가능하다.
- 애너테이션이나 XML에 Named 쿼리를 정의한다.
- 애플리케이션 로딩 시점에 초기화 후, 재사용한다.
- 정적 쿼리는 변하지 않는다. 따라서, 로딩 시점에 쿼리를 SQL로 파싱하고 캐시한다.
- JPQL는 기본적으로 SQL로 변환한 다음에 실행되는데 네임드 쿼리를 로딩 시점에 딱 한 번 실행되고 캐시되니까 비용이 거의 없다고 봐도된다.
- 애플리케이션 로딩 시점에 쿼리를 검증한다.
Ex) Named 쿼리 - 애너테이션
- Member 클래스
- 여기에 쿼리를 잘못 입력하면 SQLsyntax 에러가 난다.
- 클래스 이름을 앞에 붙여주는 게 관례
<hide/>
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "SELECT m FROM Member m WHERE m.username = :username"
)
public class Member {
...
}
- Main
- username을 "회원1"로 수정한다.
<hide/>
List<Member > resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
for (Member member : resultList) {
System.out.println("member = " + member);
}
Note) 실행 결과
- 회원1번 데이터를 가져온다.
- 쿼리가 실행되서 이름이 바뀐 채로 출력된다.
<hide/>
/* Member.findByUsername */ select
member0_.id as id1_0_,
member0_.age as age2_0_,
member0_.TEAM_ID as TEAM_ID5_0_,
member0_.type as type3_0_,
member0_.username as username4_0_
from
Member member0_
where
member0_.username=?
member = Member{id=3, username='회원1', age=0}
Named 쿼리 - XML에 정의
- persistence에 ormMember라는 mapping파일에 넣는다.
- 아래와 같이 데이터를 넣어주면 네임드 쿼리를 불러올 때, 애너테이션에서 불러올 수도 있고 ormMember라는 xml 파일에서도 불러올 수 있다.
- xml이 항상 우선권을 가진다.
- 애플리케이션 운영 환경에 따라 다른 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>
cf) Named 쿼리 - JpaRepository
- 스프링 jpa를 쓰면 JpaRepository를 상속 받은 인터페이스의 메서드 위에 @Query()를 선언하고 괄호 안에 Named 쿼리를 쓸 수 있다.
- jpa가 이를 네임드 쿼리로 등록한다. 애플리케이션 로딩 시점에 파싱도 하고 오류도 잡아 낸다. - 큰 장점
- 위의 예제대로 쓰는 것보다 이 방법이 더 좋다.
11.7 벌크(bulk) 연산
벌크 연산
- ex) 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
- JPA 변경 감지 기능으로 실행하려면 SQL이 너무 많이 실행된다.
- 재고가 10개 미만인 상품을 리스트로 조회
- 상품 엔티티의 가격을 10% 증가한다.
- 트랜잭션 커밋 시점에 변경 감지가 동작한다.
- 변경된 데이터가 100건이라면 100번의 UPDATE SQL이 실행된다.
벌크 연산 예제
- 쿼리 한 번으로 여러 테이블(엔티티)의 로우를 일괄적으로 변경한다. (엔티티)
- executeUpdate(): 쿼리를 실행하고 나서 영향 받은 엔티티의 수를 반환한다.
- UPDATE, DELETE 지원한다.
- INSERT(하이버네이트는 insert into ... SELECT 지원)
벌크 연산 주의 사항
- 벌크 연산은 영속성 컨텍스에 저장하는 과정을 생략하고 데이터베이스에 직접 쿼리한다.
- 데이터 정합성에 어긋날 수 있다.
- 아래 두 가지 중 선택해서 쓴다.
- 벌크 연산을 먼저 실행한다.
- 벌크 연산 수행 후, 영속성 컨텍스트를 초기화한다. em.clear().
Ex) 벌크 연산
- 초기화를 안 화면?
<hide/>
package jpql;
import java.util.Collection;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
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);
member1.setAge(0);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
member2.setAge(0);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
member3.setAge(0);
em.persist(member3);
em.flush();
em.clear();
// flush 되는 시점 (DB에 Age가 0으로 들어간다. 영속성 컨텍스트에서 지워지는 건 아니다.)
int resultCnt = em.createQuery("UPDATE Member m set m.age = 20") // 그런데, DB에는 20으로 업데이트
.executeUpdate();
System.out.println("resultCnt = " + resultCnt);
System.out.println("member1.getAge() = " + member1.getAge());
System.out.println("member2.getAge() = " + member2.getAge());
System.out.println("member3.getAge() = " + member3.getAge());
tx.commit();
}catch (Exception e) {
tx.rollback();
e.printStackTrace();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- createQuery()에서 설정한대로 나이가 20살이 들어가지 않는다.
- 초기화 em.clear()를 안했기 때문이다.
- 초기화하면 영속성 컨텍스트의 내용이 모두 지워져서 그 다음에 em에서 조회하는 경우, 데이터베이스의 정보를 가져온다.
<hide/>
/* UPDATE
Member m
set
m.age = 20 */ update
Member
set
age=20
resultCnt = 3
member1.getAge() = 0
member2.getAge() = 0
member3.getAge() = 0
- 올바른 벌크 연산
<hide/>
int resultCnt = em.createQuery("UPDATE Member m set m.age = 20") // 그런데, DB에는 20으로 업데이트
.executeUpdate();
em.clear();
Member findMember = em.find(Member.class, member1.getId()); // 영속성 컨텍스트에 값이 없으니까 DB에서 가져온다.
System.out.println("findMember = " + findMember.getAge());
Note) 실행 결과 - findMember = 20
'Spring Framework > [인프런] Java ORM 표준 프로그래밍 - JPA' 카테고리의 다른 글
Chapter 10. 객체지향 쿼리 언어1 - 기본 문법 (0) | 2022.09.08 |
---|---|
Chapter 09. 값 타입 (0) | 2022.09.07 |
Chapter 08. 프록시와 연관 관계 관리 (0) | 2022.09.07 |
Chapter 07. 고급 매핑 (2) | 2022.09.06 |
Chapter 06. 다양한 연관관계 매핑 (0) | 2022.09.05 |