.(점)을 찍어 객체 그래프를 탐색하는 것
select m.username // 상태 필드
from Member m
join m.team t // 단일 값 연관 필드
join m.orders o // 컬렉션 값 연관 필드
where t.name = '팀A'
@ManyToOne
, @OneToOne
, 대상이 엔티티(ex: m.team)@OneToMany
, @ManyToMany
, 대상이 컬렉션(ex: m.orders)String query = "select m.username, m.age From Member m";
String query = "select m.team From Member m";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
select m.team.name From Member m
처럼 team에서 경로 탐색이 더 가능하다.
String query = "select t.members From Team t";
// 명시적 조인: "select m.username From Team t join t.members m"
Collection result = em.createQuery(query, Collection.class)
.getResultList();
명시적 조인: join 키워드 직접 사용
→ select m from Member m join m.team t
묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)
→ select m.team from Member m
단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이를 묵시적 조인이라 한다. 참고로, 묵시적 조인은 모두 내부 조인이다.
select o.member.team from Order o
→ 성공select t.members from Team t
→ 성공select t.members.username from Team t
→ 실패select m.username from Team t join t.members m
→ 성공실무에서는 명시적 조인을 사용하자.
조인은 SQL 튜닝에 중요 포인트
묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움
join fetch
명령어 사용// 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;
팀이 있는 회원을 조회하고 싶을 때 fetch join을 사용하면 내부적으로 inner join을 사용한다. 팀이 없는 회원은 누락된다.
다음은 일반적인 select로 Member를 조회할 때, 연관관계에 있는 Team을 불러 Team.name 까지 조회하는 예시이다. 이 경우 문제가 있다.
// Member와 Team은@ManyToOne
관계에, 지연로딩이 설정되어있다.
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());
// 회원1, 팀A(SQL)
// 회원2, 팀A(1차캐시)
// 회원3, 팀B(SQL)
// 회원 100명 -> N + 1
}
결과를 보면 for문 안에서 Member를 조회한 뒤, Team의 이름까지 조회할 때, 회원1, 회원2는 SQL과 1차 캐시를 통해 팀을 불러오는 것이지만, 회원3에 해당하는 팀B는 아직 조회하지 않았기 때문에 1차 캐시에 없다.
따라서 select 쿼리를 한 번 더 실행하게 된다. 이 경우 N+1 문제가 발생한다.
정리하자면, 최초 JPQL을 통해 Member를 조회해 올때 Team의 정보는 Proxy 객체로 가지고 있다.(실제로는 존재 x)
그렇기에 실제로 getTeam().getName()
을 통해 팀의 정보를 조회하려 할 때 SQL을 수행한다. 주석 내용대로 한 번 가져온 Team의 정보는 1차 캐시에 올라가 있기 때문에 더 조회할 필요는 없지만, 회원 N명을 조회하게 되었을 때 최대 N+1번 Team 조회 쿼리가 수행 될 수 있다.
페치 조인을 사용하면 N+1 문제를 해결하는데, 다음과 같이 회원을 조회하면서 연관된 팀도 함께 조회한다. (SQL 1회)
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());
// 페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 X
}
페치조인은 조회 당시 실제 엔티티가 담기기 때문에, 지연로딩 없이 바로 사용가능하다.
// 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'
이를 수행하면 Team은 하나지만 Member가 1개 이상일 수 있다. // 일대다 관계에선 데이터가 뻥튀기 될 수 있다.
팀A는 1개지만, 그에 해당하는 멤버는 회원1, 회원2로 2개이기 때문에 조회 결과는 위 표처럼 2개의 row가 된다. 팀은 하나이기에 같은 주소값(0x100)을 가진 결과가 두개가 나오고 팀A의 입장에선 회원1, 회원2를 가진다.
// 이것이 바로 결과상의 뻥튀기가 발생한 것임
다음은 컬렉션 페치 조인 사용 코드 예시이다.
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() + "|members=" + team.getMembers().size());
for (Member member : team.getMembers()) {
System.out.println("-> member = " + member);
}
}
select distinct t from Team t join fetch t.members where t.name = '팀A';
위 코드를 실행하면 SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과상 중복 제거를 실패한다.
단순히 쿼리만으로는 중복제거가 안되기 때문에 JPA에선 DISTINCT가 추가로 애플리케이션에서 중복 제거를 시도한다.
코드를 통해 확인해보면 다음과 같다.
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 = " + result.size());
for (Team team : result) {
System.out.println("team = " + team.getName() + "|members=" + team.getMembers().size());
for (Member member : team.getMembers()) {
System.out.println("-> member = " + member);
}
}
참고로 반대로 다대일(N:1), 일대일(1:1)은 결과가 뻥튀기 되지 않는다.
// JPQL
select 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';
참고로 즉시로딩과 fetch 조인에 관련된 의문은 다음을 확인하자.
→ fetch 조인, 엔티티 그래프 질문입니다. - inflearn
// as m 이라는 별칭(alias)는 fetch join에서 사용할 수 없다.
String query = "select t from Team t join fetch t.members as m";
팀을 조회하는 상황에서 멤버가 5명인데 3명만 조회한 경우, 3명만 따로 조작하는 것은 몹시 위험하다.
String query = "select t from Team t join fetch t.members as m where m.age > 10";
String query = "select t from Team t join fetch t.members, t.orders";
String query = "select t From Team t join fetch t.members m";
List<Team> result = em.createQuery(query, Team.class)
.setFirstResult(0)
.setMaxResults(1)
.getResultList();
로그를 보면 경고 로그가 출력된 것을 확인할 수 있고, 메모리에서 페이징을 하면서 쿼리상에는 limit offset이 없다.
해결 방안은 다음과 같다.
String query = "select m From Member m join fetch m.team t";
@BatchSize()
public class Team {
...
@BatchSize(size = 100)
@OneToMany(mappedBy = "team")
private List<Member> members;
...
}
String query = "select t From Team t";
지연 로딩 상태이지만, 조회 시 members를 BatchSize의 size 만큼 조회해온다.
BatchSize()
는 글로벌 설정으로도 할 수 있다. (실무에서 이렇게 관리하신다고 함)
<!-- persistence.xml -->
<property name="hibernate.default_batch_fetch_size" value="100"/>
@OneToMany(fetch = FetchType.LAZY)
(글로벌 로딩 전략)// JPQL
select i from Item i where type(i) IN(Book, Movie);
// SQL
select i from Item 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
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 query = "select m from Member m where m = :member";
Member findMember = em.createQuery(query, Member.class)
.setParameter("member", member1)
.getSingleResult();
// 식별자를 직접 전달
String query = "select m from Member m where m.id = :memberId";
Member findMember = em.createQuery(query, Member.class)
.getParameter("memberId", member1.getId())
.getSingleResult();
위 두 JPQL의 실행된 SQL은 아래와 같다.
select m.* from Member m where m.id=?
Team team = em.find(Team.class, 1L);
String query = "select m from Member m where m.team = :team";
List<Member> members = em.createQuery(query, Member.class)
.getParameter("team", teamA)
.getResultList();
String query = "select m from Member m where m.team.id = :teamId";
List<Member> members = em.createQuery(query, Member.class)
.getParameter("teamId", teamA.getId)
.getResultList();
위 두 JPQL의 실행된 SQL은 아래와 같다.
select m.* from Member m where m.team_id=?
@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", "회원1")
.getResultList();
META_INF/persistence.xml
<persistnece-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>
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select u from User u where u.username = ?1")
Member findByUsername(String username);
}
@Repository
어노테이션이 등록된 인터페이스에서 사용되는 @Query
어노테이션에 있는 JPQL(or native)들이 NamedQuery로써 컴파일 시에 등록되는 것이다.
// 실무에서 이 방식이 많이 쓰인다.
executeUpdate()
의 결과는 영향받은 엔티티 수 반환String query = "update Product p " +
"set p.price = p.price * 1.1 where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(query)
.setParameter("stockAmount", 10)
.executeUpdate();
// 추가로 읽어볼 자료 - Spring Data JPA의 @Modifying