작성
·
399
0
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// 아래는 getter, setter
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Team getTeam() {
return team;
}
public void setTeam(Team team) {
this.team = team;
}
public void changeTeam(Team team){
this.team = team;
//연관관계 편의 메소드
team.getMembers().add(this);
}
}
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();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
//member.setTeam(team); //owner에 넣어야 DB 반영됨
member.changeTeam(team); //연관관계 편의 메소드
em.persist(member);
Member member1 = new Member();
member1.setUsername("member2");
member1.changeTeam(team);
em.persist(member1);
em.flush();
em.clear();
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
System.out.println("=====================");
for (Member m : members) {
System.out.println("m = " + m.getUsername());
}
System.out.println("=====================");
tx.commit();
} catch (Exception e){
tx.rollback();
} finally {
em.clear();
}
emf.close();
}
Team, Member가 양방향 연관관계를 맺고 있는 상태에서 우선 Team을 조회하고 Team의 members를 사용하는 시점에 Member를 조회하는 예시입니다.
처음에는
select m.team_id, m.member_id, m.username
from member as m
where member.team_id = 1;
이런 형태로 select 쿼리가 발생할 것으로 예상했습니다.
하지만 실제 hibernate로 발생한 쿼리를 확인하니 아래와 같은 쿼리가 발생했습니다.
Hibernate:
select
members0_.TEAM_ID as team_id3_0_0_,
members0_.MEMBER_ID as member_i1_0_0_,
members0_.MEMBER_ID as member_i1_0_1_,
members0_.TEAM_ID as team_id3_0_1_,
members0_.USERNAME as username2_0_1_
from
Member members0_
where
members0_.TEAM_ID=?
hibernate가 생성한 select쿼리에서 team_id, member_id 컬럼이 두번 나오는 이유가 무엇인가요?
답변 2
0
안녕하세요.
해당 문제에 대해서 인터넷에서 검색한 결과와 직접 중단점을 찍어가며 테스트를 수행해본 결과를 요약해서 전달드립니다. 저도 주니어 개발자로 아직 실력이 부족해, 많은 부분을 추론에 의지하여 말씀드릴 수 밖에 없는 점은 양해 부탁드립니다.
먼저 스택 오버 플로우 질문과 답변에 나온 내용으로, Hibernate 6.0 미만 버전에서는 쿼리문을 만드는 단계에서 복잡한 연관 관계가 있는 테이블인 경우에 PK, FK가 걸려있는 열을 중복해서 검색하는 쿼리문이 만들어진다는 것을 알 수 있었습니다.
Hibernate 6.0 이상의 버전에서는 쿼리문을 만드는 로직이 바뀌어서 해당 현상이 더 이상 발생하지 않습니다.
아래는 제가 Hibernate 6.4 버전에서 테스트 해본 결과입니다.
이 현상에 대해서 직접 테스트 해본 결과 제가 내린 결론은 다음과 같았습니다.
Hibernate는 애플리케이션 실행시 @Entity가 붙은 클래스들을 순회하며 데이터베이스에서 엔티티를 로딩하기 위한 계획(Load plan)과 쿼리를 만들기 위한 정보(Query Spaces)를 만듭니다. Query Spaces를 만드는 중에 엔터티의 Column들을 돌며 Query에 사용할 별칭을 만듭니다.
Hibernate의 Trace 로그를 찍어보니 아래와 같은 로그를 확인할 수 있었습니다.
2023-12-22 06:01:19 DEBUG [org.hibernate.loader.plan.build.spi.LoadPlanTreePrinter] LoadPlan(entity=hellojpa.Member)
- Returns
- EntityReturnImpl(entity=hellojpa.Member, querySpaceUid=<gen:0>, path=hellojpa.Member)
- EntityAttributeFetchImpl(entity=hellojpa.Team, querySpaceUid=<gen:1>, path=hellojpa.Member.team)
- QuerySpaces
- EntityQuerySpaceImpl(uid=<gen:0>, entity=hellojpa.Member)
- SQL table alias mapping - member0_
- alias suffix - 0_
- suffixed key columns - {member_i1_0_0_}
- JOIN (JoinDefinedByMetadata(team)) : <gen:0> -> <gen:1>
- EntityQuerySpaceImpl(uid=<gen:1>, entity=hellojpa.Team)
- SQL table alias mapping - team1_
- alias suffix - 1_
- suffixed key columns - {team_id1_1_1_}
2023-12-22 06:01:19 DEBUG [org.hibernate.loader.entity.plan.EntityLoader] Static select for entity hellojpa.Member [NONE]: select member0_.MEMBER_ID as member_i1_0_0_, member0_.TEAM_ID as team_id3_0_0_, member0_.USERNAME as username2_0_0_, team1_.TEAM_ID as team_id1_1_1_, team1_.name as name2_1_1_ from Member member0_ left outer join Team team1_ on member0_.TEAM_ID=team1_.TEAM_ID where member0_.MEMBER_ID=?
이는 Member Entity에 대한 Load plan과 Query Spaces가 만들어졌다는 로그로 보입니다. 그런데 바로 다음 로그에서 아래와 같이 Members Collection에 대한 Load Plan과 Query Spaces가 만들어졌다는 것을 알 수 있었습니다.
2023-12-22 06:01:19 DEBUG [org.hibernate.loader.plan.build.spi.LoadPlanTreePrinter] LoadPlan(collection=hellojpa.Team.members)
- Returns
- CollectionReturnImpl(collection=hellojpa.Team.members, querySpaceUid=<gen:0>, path=[hellojpa.Team.members])
- (collection element) CollectionFetchableElementEntityGraph(entity=hellojpa.Member, querySpaceUid=<gen:1>, path=[hellojpa.Team.members].<elements>)
- QuerySpaces
- CollectionQuerySpaceImpl(uid=<gen:0>, collection=hellojpa.Team.members)
- SQL table alias mapping - members0_
- alias suffix - 0_
- suffixed key columns - {team_id3_0_0_}
- entity-element alias suffix - 1_
- 1_entity-element suffixed key columns - member_i1_0_1_
- JOIN (JoinDefinedByMetadata(elements)) : <gen:0> -> <gen:1>
- EntityQuerySpaceImpl(uid=<gen:1>, entity=hellojpa.Member)
- SQL table alias mapping - members0_
- alias suffix - 1_
- suffixed key columns - {member_i1_0_1_}
2023-12-22 06:01:19 DEBUG [org.hibernate.loader.collection.plan.CollectionLoader] Static select for collection hellojpa.Team.members: select members0_.TEAM_ID as team_id3_0_0_, members0_.MEMBER_ID as member_i1_0_0_, members0_.MEMBER_ID as member_i1_0_1_, members0_.TEAM_ID as team_id3_0_1_, members0_.USERNAME as username2_0_1_ from Member members0_ where members0_.TEAM_ID=?
QuerySpaces 중간에 JOIN으로 시작하는 곳을 보면, Members Collection이 Member Entity에 대한 QuerySpace 정보를 갖고 있다는 걸 알 수 있었습니다.
이 정보들을 저는 아래와 같이 정리하였습니다.
1. Hibernate는 Entity와, 연관 관계에 사용되는 Entity Collection에 대한 Load Plan과 Query Space를 만들고 저장한다.
Collection의 Query Space는 해당 컬렉션에 저장되는 Entity의 Query Space 정보를 갖고 있다.
그런데 Collection Query Space와 Entity Query Space 둘 모두가 연관 관계에 사용되는 컬럼의 키값(예제의 경우에는 team id와 member id)를 갖고 있다.
Collection을 불러오면 Collection Query Space 자신이 갖고 있는 키값과 Entity Query Space가 갖고 있는 키를 모두 불러온다.
이 정보를 갖고 select 문을 만드니 중복이 발생해버린다.
감사합니다.
0