점을 찍어서 객체 그래프를 탐색하는 것을 경로표현식이라고 한다. 3가지 유형이 있다.
select m.team from Member m 을 하면 자바코드에서는 바로 team 엔티티를 가져오는 것처럼 보이지만 실제 쿼리 나가는걸 보면 db에서는 team_id로 조인하는 쿼리가 나가는 것을 확인할 수 있다.
// 단일값 연관경로
String query = "select m.team.name from MemberTest m"; // name은 상태필드, 경로탐색의 끝, 묵시적 내부조인(탐색o)
String query2 = "select m.team from MemberTest m"; // select team from member join team, 조인(묵시적내부조인)
따라서 조인을 명시적으로 작성하지 않았는데 SELECT절에서 m.team.name이라든지 m.team처럼 연관관계에 있는 team을 조회하는 경우 쿼리에서 조인이 나가는 걸 묵시적 내부조인이 발생했다고 한다. team엔티티의 name이나 속성값으로 탐색이 가능한데, 단일 값 연관경로는 묵시적 내부조인이 발생하지만, 탐색이 가능하다.
컬렉션 값 연관경로는 일대다 관계에 있는 엔티티들이 있을때 마찬가지로 명시적으로 JPQL을 작성할때는 조인이 없다가 쿼리를 작성했을때 다쪽 엔티티를 SELECT했기에 내부조인 쿼리가 나가는 묵시적 내부조인이 발생한다.
// 컬렉션 값 연관경로
String query3 = "select t.members from Team t";// select members from team inner join member
String query4 = "select size(t.members) from Team t"; // size 사용가능
Collection result3 = em.createQuery(query3, Collection.class).getResultList(); // 묵시적 내부조인, 탐색x
for (Object o : result3) {
System.out.println("o = "+ o);
}
단일 값 연관경로와는 다르게 연관관계에 있는 members(다쪽 엔티티)에서 탐색이 불가능하다. 하지만 size(t.members)처럼 컬렉션 사이즈를 select하는건 지원해준다. 엔티티 컬렉션에서 탐색하고 싶으면 FROM절에서 명시적 조인을 작성해서 별칭을 얻어서 탐색하면 된다.
// 컬렉션 값 연관경로
String query3 = "select m.username from Team t join t.members m"; // from절 명시적 내부조인(별칭), 탐색o
결론은 명시적 조인을 사용해라. 묵시적 내부조인 즉, 자바코드로 JPQL작성할때 조인을 안써서 쉽게 코드를 작성할 순 있겠지만, 나중에 나가는 쿼리를 보면 어디에서 조인이 발생하는 건지, 어떻게 수정해야할지 막막해질수도 있다. 그러니까 JPQL로 차라리 조인을 쓰는게 유지보수에 좋다.
명시적 조인은 이름만 어렵지 join키워드를 사용하는게 다이기 때문에, 묵시적 내부조인을 방지하는 차원에서 꼭 쓰자
SQL 조인의 종류는 아니다. 다만 JPQL에서 성능 최적화를 위해서 제공해주는 기능 중 하나다. 지연로딩을 사용하면 연관된 엔티티도 나중에 SELECT쿼리가 나가는 N+1문제가 발생한다. 이럴때 한번에 연관된 엔티티까지 모두 조회하는 기능으로 join fetch명령어가 사용된다.


다대일 관계로 위처럼 사용하면 문제가 없다. 하지만 문제가 되는건 일대다 관계에서 fetch join을 사용할때가 문제다. select t from team t join fetch t.members를 하면 하나의 TEAM_ID에 대해 다쪽 MEMBER 테이블에 매핑되는 수가 많아서 결과적으로 실제 TEAM의 데이터 수보다 훨씬많이 데이터가 뻥튀기 되어 조회된다. 같은 TEAM 엔티티가 같은 주소를 참조하기 때문이다.
String query = "select m from MemberTest m join fetch m.team"; // Team 엔티티 초기화, 페치조인>지연로딩, 다대일 조인
String query2 = "select t from Team t join fetch t.members"; // 일대다 조인, data 뻥튀기(단점)
List<MemberTest> result = em.createQuery(query, MemberTest.class).getResultList(); //select member, team
List<Team> result2 = em.createQuery(query2, Team.class).getResultList();
for (MemberTest member : result) { // teamA, teamB 각각 select
System.out.println("member="+member.getUsername()+", team name = " + member.getTeam().getName());
// 회원1, 팀A(1차 캐시)
// 회원2, 팀A(1차 캐시)
// 회원3, 팀B(1차 캐시)
// 총 1번
}
for (Team team : result2) {
System.out.println("team = "+team.getName()+", team.getMembers().size() = "+ team.getMemberList().size()); // 중복발생
for (MemberTest member : team.getMemberList()){
System.out.println("member = " +member);
}
}

똑같은 team엔티티A가 두번이나 조회되는걸 확인할 수 있는데,
distinct는 sql에서 중복 데이터를 제거해주기도 하지만, db에서 애플리케이션 구동시 애플리케이션 레벨에서 중복 식별자를 가진 엔티티를 제거해주는 역할을 해준다.
일반조인을 하면 연관된 엔티티를 조회해오지 않는다. 단지 select절에 지정한 엔티티만을 조회할 뿐이다. 하지만 패치조인을 하면 연관된 엔티티까지 모두 조회해온다.
String query = "select t from Team t join t.members m"; // SELECT TEAM만 됨, MEMBERS 데이터 로딩X
String query2 = "select t from Team t join fetch t.members m"; // SELECT TEAM, MEMBERS 모두 데이터 로딩

패치조인은 즉시로딩이라고 생각해도 좋다. 객체 그래프 sql을 한번에 조회해오기 때문이다.

패치조인 대상에 대해 별칭(as)은 줄 수 없다! select m from Member m join fetch m.team t ❌❌ 또한, 둘 이상의 컬렉션을 동시에 페치조인할 수 없다. 예를 들어 Team과 Member가 일대다 관계, Team과 Order이 일대다 관계라고 해보자. 내가 member와 order를 모두 함께 join fetch를 써서 일대다대다로 하면 잘못하면 데이터 뻥튀기에 뻥튀기가 되어버려서 진짜 애플리케이션 망가질지도 모른다. 따라서 패치조인할때 컬렉션은 하나만 해주자. 또 중요한 한계는 컬렉션 패치조인할때 페이징 API는 쓰지 못한다. 일대다 관계를 페치조인해서 데이터를 다 긁어왔다 치자. 그리고 여기서 페이징을 쓰면 잘 이해는 안가지만 한 엔티티에 연결된 다쪽 엔티티의 절반만 가져오게되는 대참사가 발생할 수 있다고 한다. 이점은 좀더 공부해서 채워넣겠다. 다만 다대일이나 일대일같은 단일 값 연관필드는 페치조인하고도 페이징 API써도 무방하다. 컬렉션에 대해 패치조인해서 페이징을 쓰고 싶다면 @BatchSize를 쓰거나 글로벌전략으로 배치사이즈를 설정하는 대체방안이 있으니 안도하자!
정리하면, 모든 관계를 지연로딩으로 설정했는데 Team-Member가 일대다 관계로 설정했고 나중에 기능을 만들때 페이징을 써야만 하는 상황이 왔다. 하지만 컬렉션 값은 페치조인으로 Team과 Member을 한번에 조회해오면 안된다. Member를 사용할때마다 select 쿼리가 나가니 N+1문제가 발생한다. 따라서 @BatchSize나 글로벌 전략으로 배치 사이즈를 설정해서 멤버 조회시 team_id 있는대로 다 긁어오는 방법을 사용하도록 한다. 실제로 나가는 쿼리는 이렇게 IN절로 batch size가 100이라면 100개의 team_id를 긁어와서 SELECT MEMBER를 해주는 방식이다.

@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@BatchSize(size = 100)
@OneToMany(mappedBy = "team")
private List<MemberTest> members = new ArrayList<>();
...
}
Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
MemberTest member1 = new MemberTest();
MemberTest member2 = new MemberTest();
MemberTest member3 = new MemberTest();
member1.setTeam(teamA);
member1.setUsername("회원1");
member2.setUsername("회원2");
member3.setUsername("회원3");
member2.setTeam(teamA);
member3.setTeam(teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.flush();
em.clear();
//컬렉션 페치조인시 페이징API X
String query = "select t from Team t"; // LAZY LOADING
List<Team> result = em.createQuery(query, Team.class) // SELECT TEAM
.setFirstResult(0)
.setMaxResults(2)
.getResultList();
for (Team team : result) {
System.out.println("team = "+team.getName()+", team.getMembers().size() = "+ team.getMemberList().size()); // 중복제거(같은 식별자 가진 team 엔티티 제거)
for (MemberTest member : team.getMemberList()){
System.out.println("member = " +member);//SELECT MEMBER WHERE TEAM_ID IN(? , ?, ..), LAZY LOADING 가져올때 한번에 모든 팀 ID인 MEMBER 가져옴
}
System.out.println();
}
// 총 2번
또는 <property name="hibernate.default_batch_fetch_size" value="100"/> 처럼 글로벌하게 적용하는 방법이 있다.
JPQL에서는 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본키값을 사용하는 쿼리가 나간다.

JPQL에서는 편리하게 엔티티를 COUNT(M)처럼 직접 사용하지만 SQL은 그렇게 못하기에 ID값으로 바뀌어서 쿼리가 나가게 된다. 직접 ID값을 파라미터 바인딩으로 전달되나 직접 엔티티를 사용하나 SQL에서는 기본키값으로 동일한 쿼리가 나간다.

같은 방식으로 m.team처럼 연관된 엔티티를 직접 사용했을때에도 JPQL에서는 가능하지만 실제 SQL나가는 쿼리를 보면 m.team_id로 바뀌어서 쿼리가 나감을 확인할 수 있다. 이것 또한 외래키값으로 JPQL 파라미터 바인딩하나, 직접 엔티티 값을 사용하나 똑같이 SQL에서는 외래키 값으로 바뀌어서 나감은 동일하다.

Named 쿼리는 미리 엔티티 @Entity가 정의된 클래스에 미리 쿼리에 이름을 부여해서 애플리케이션 로딩시점에 함께 로딩되고 문법적으로 문제가 있으면 컴파일 에러를 내주는 좋은 기능이다. 하지만 정적쿼리이고 애플리케이션 로딩시점에 초기화해서 다시 사용할 수 있다는 장점이 있다.
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from MemberTest m where m.username = :username"
)
public class MemberTest {
@Id @GeneratedValue
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
@Enumerated(EnumType.STRING) // query에서 이넘명
private MemberType type;
/*@OneToMany(mappedBy = "member")
List<OrderTest> orderList = new ArrayList<>();*/
}
List<MemberTest> result = em.createNamedQuery("Member.findByUsername",MemberTest.class) // NamedQuery
.setParameter("username", "회원1")
.getResultList();
xml로도 Named 쿼리를 설정할 수 있고 xml에 설정한게 항상 우선권을 가진다. spring data jpa에서는 @Query라는 이름으로 Named 쿼리를 사용할 수 있다.
SQL에서 여러개의 UPDATE, DELETE 쿼리가 나가는 거라고 생각하면 된다. 벌크연산은 언제 사용해야할까?

결국에는 백만건이상 데이터가 정말 많을수도 있다. 그러면 일일이 쿼리가 나가면서 애플리케이션은 부담이 될것이고 느려질수밖에 없다. 그래서 나온게 벌크연산이다. 쿼리한번으로 여러 테이블의 로우를 변경할 수 있다.
Team teamA = new Team();
teamA.setName("teamA");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("teamB");
em.persist(teamB);
MemberTest member1 = new MemberTest();
MemberTest member2 = new MemberTest();
MemberTest member3 = new MemberTest();
member1.setUsername("회원1");
member2.setUsername("회원2");
member3.setUsername("회원3");
member1.setTeam(teamA);
member2.setTeam(teamA);
member3.setTeam(teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.flush();
em.clear();
//모든 회원 나이를 10으로 UPDATE
int resultCountColumn = em.createQuery("update MemberTest m set m.age = 20")
.executeUpdate();//벌크연산, update 1회
System.out.println(resultCountColumn); // 3
벌크연산은 UPDATE와 DELETE를 지원해주므로 필요할때 적극적으로 사용하길 바란다.
주의할점은, 벌크연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리를 날리는 거이기 때문에 잘못하면 데이터 정합성이 망할수도 있다. 따라서 1. 영속성 컨텍스트로 작업한게 없다면 벌크연산을 먼저 수행해라. 2. 영속성 컨텍스트로 작업한게 있다면 벌크연산 수행하고 나서 em.clear()로 영속성 컨텍스트를 초기화해라.
//모든 회원 나이를 10으로 UPDATE
//flush 자동 호출(commit, query)
int resultCountColumn = em.createQuery("update MemberTest m set m.age = 20")
.executeUpdate(); // db에만 반영
// 영속성 컨텍스트에는 반영 아직x
System.out.println("member1.getAget() = "+member1.getAge());// 0
System.out.println("member2.getAget() = "+member2.getAge());// 0
System.out.println("member3.getAget() = "+member3.getAge());// 0
MemberTest findMember = em.find(MemberTest.class, member1.getId());
System.out.println(findMember.getAge()); // 0
em.clear(); // 영속성 컨텍스트 초기화
MemberTest findMember2 = em.find(MemberTest.class, member1.getId());
System.out.println(findMember2.getAge()); // 20