[김영한 - JPA 기본] #5 연관관계 매핑 기초

단방향 연관관계


Table 의존적인 연관관계 : 다른 예시

아래는 다대일 관계인 Member와 Team에 대한 코드다

Team team = new Team("H.R.");
em.persist(team);

Member member = new Member("swcho");
member.setTeamId(team.getId());
em.persist(member);

tx.commit();

Member 일부 코드

@Column(name = "TEAM_ID")
private Long teamId;

위와같이 getId로 해서 setId로 하는 번잡한 코드를 참조로 바꾸어 보자

단방향 매핑으로 전환

Member

@ManyToOne // 다대일
@JoinColumn(name = "TEAM_ID") // 실제 테이블의 PK
private Team team;

위와같은 Team을 참조로 하는 필드를 추가하면 된다

그외 팁

종종 persist context에 저장되기 때문에 select 쿼리가 나가는 것을 못 볼 때가 있다

만일 보고자 한다면

아래 코드를 추가하면 된다

em.flush(); // DB 동기화
em.clear(); // 캐시 지움

조회 쿼리

Member findMember = em.find(Member.class, member.getId());
select
		member0_.id as id1_0_0_,
		member0_.age as age2_0_0_,
		member0_.createDate as createDa3_0_0_,
		member0_.description as descript4_0_0_,
		member0_.lastModifiedDate as lastModi5_0_0_,
		member0_.name as name6_0_0_,
		member0_.roleType as roleType7_0_0_,
		member0_.TEAM_ID as TEAM_ID8_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_.id=?

위와 같이 join되지 않은 쿼리를 보고자 한다면

아래 Lazy하게 가져오면 된다 (지연로딩 전략)

@ManyToOne(fetch = FetchType.LAZY)
select
		member0_.id as id1_0_0_,
		member0_.age as age2_0_0_,
		member0_.createDate as createDa3_0_0_,
		member0_.description as descript4_0_0_,
		member0_.lastModifiedDate as lastModi5_0_0_,
		member0_.name as name6_0_0_,
		member0_.roleType as roleType7_0_0_,
		member0_.TEAM_ID as TEAM_ID8_0_0_
from
		Member member0_
where
		member0_.id=?

select
		team0_.TEAM_ID as TEAM_ID1_1_0_,
		team0_.name as name2_1_0_
from
		Team team0_
where
		team0_.TEAM_ID=?

객체 그래프 탐색으로 Team 객체 조회

Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();

연관관계 바꾸기

Team rndTeam = new Team("RnD");
em.persist(rndTeam);
findMember.setTeam(rndTeam);

양방향 연관관계와 연관관계의 주인 1- 기본


@Entity
public class Member {

	...

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "TEAM_ID")
	private Team team;

@Entity
public class Team {

	...

	@OneToMany(mappedBy = "team")
	private List<Member> members = new ArrayList<>();

Team에게 있어 Member는 일대다 관계이다

그리고 mappedBy 에는 상대 엔티티의 변수명을 넣는다

굳이 위 사항을 억지로 암기할 필요는 없다 !

예를들어 @ManyToOne 에는 mappedBy 속성이 없는 등 IDE를 통해서 어느쪽이 mapped by 되는 지 알수있다 :)

만일 이름이 일치하지 않으면 아래와 같은 오류가 뜬다

image

Team team = new Team("H.R.");
em.persist(team);

Long lastMemberId = -1L;
for (int i = 1; i <= 2; i++) {
	Member member = new Member("user " + i);
	member.setTeam(team);
	em.persist(member);
	lastMemberId = member.getId();
}

em.flush();
em.clear();

Member findMemberOne = em.find(Member.class, lastMemberId);

// 양방향 탐색
List<Member> findMembers = findMemberOne.getTeam().getMembers();
for (Member findMember : findMembers) {
	System.out.println("findMember = " + findMember);
}

tx.commit();

위와 같은 쿼리로 객체 그래프 탐색이 양방향으로 됨을 알 수 있다

객체와 테이블의 차이점

  • 객체의 양방향은 각 단방향 관계의 합이다

    • Member가 참조하는 team
    • Team이 참조하는 mebers

    • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다
  • 테이블은 외래키(FK) 값 하나로 양방향 관계를 맺는다

외래키의 주인은 누구?

  • MEMBER 테이블의 TEAM_ID (FK) 가 삽입/변경되는 것이
    Member 객체의 team에 의존할지, Team 객체의 mebers에 의존할지 부터가 의문이다

  • 예를들어 회원을 새로운 팀으로 바꾸고자 할 때 이다 !

  • 만일 연관관계 주인을 거꾸로 헀을때…

    • 발생 가능한 상황 Team의 members에 업뎃을 시켰는데 실 MEMBER테이블의 FK에 업뎃 쿼리가 난간다 !
    • 한번 상황을 재현해보려 했지만 잘 되지 않았다 ㄷ.ㄷ
      @Column(s) not allowed on a @ManyToOne property: hellojpa.domain.Member.team
      

연관관계의 주인 Owner

  • 다대일의 다 ! 외래키가 있는 곳이 주인이다 !

    • 진짜 매핑 - 연관관계의 주인 (Member.team) - can write
    • 가짜 매핑 - 주인의 바대편 (Team.members) - read only
  • @JoinColumn@ManyToOne 이 있는 곳이 주인이었는줄 알았지만 사실은 그렇지 않았다 !

    • 사실 @JoinColumn연관관계 주인이 아니다 !
      • 이 어노테이션은 생략이 가능하다
        • 생략시 MEMBER_ID 등 엔티티 네임 기준으로 default가 잡힌다
      • 일대일 관계에서 양쪽 모두 @JoinColumn 적용 가능
      • 그렇다면 코드상으로 누가 연관관계의 주인인가?
        mapped by 속성을 기준으로 정해진다
        해당 속성을 가진 필드는 연관관계의 주인이 아니다 -> 반대편이 주인
        (말이 어려우니 코드로 이해하고 암기해야 한다)
  • 연관관계의 주인만이 외래키를 관리한다 (등록, 수정)

    • 주인이 아닌 쪽은 조회만 가능하다
  • 주인이 아닌쪽은 mappedBy 속성으로 주인의 참조 객체명을 지정한다 (말이 어렵기 떄문에 코드로 보자 !)

다시 한번 코드로 나타내면 아래와 같다

@Entity
public class Member {

	@ManyToOne()
	@JoinColumn(name = "TEAM_ID")
	private Team team;
@Entity
public class Team {

	@OneToMany(mappedBy = "team")
	private List<Member> members = new ArrayList<>();
참고로 join 문장은 우리가 아는 DB 벤더사의 그것과 같다
select
  m.id, m.name, m.team_id, t.name
from member m
  join team t
  on m.team_id = t.team_id;

주의할 점

Member member = new Member("user A");
em.persist(member);

Team team = new Team("H.R.");
team.getMembers().add(member);
em.persist(team);

tx.commit();

위와 같은 상황에서는

MEMBER테이블에 TEAM_ID가 할당되지 않는다

왜그러냐면 Team은 연관관계의 주인이 아니기 때문에 쓰기 기능이 없기 때문 !

아래처럼 해주면 정상적으로 FK가 할당된다

Member member = new Member("user A");
em.persist(member);

Team team = new Team("H.R.");
// team.getMembers().add(member);
em.persist(team);

member.setTeam(team);

tx.commit();

살펴보기 : 함정 포인트 : 주의할 것!

Team team = new Team("H.R.");
em.persist(team);

Member member = new Member("user A");
member.setTeam(team);
em.persist(member);

// team.getMembers().add(member);

em.flush();
em.clear();

Team findTeam = em.find(Team.class, team.getId()); // SELECT ~ TEAM
List<Member> members = findTeam.getMembers(); // SELECT ~ MEMBER

for (Member m : members) {
	System.out.println("m.getName() = " + m.getName());
}

tx.commit();

위 결과에는 이상이 없다

그러나 객체 지향 관점에서

team.getMembers().add(member);

위 코드가 없다는 것은 이상해 보인다

그리고 실질적으로 문제 2가지 있다

아래 코드를 보자

Team team = new Team("H.R.");
em.persist(team);

Member member = new Member("user A");
member.setTeam(team);
em.persist(member);

// team.getMembers().add(member);

// em.flush();
// em.clear();

Team findTeam = em.find(Team.class, team.getId()); // 1차 캐시
List<Member> members = findTeam.getMembers(); //  select 쿼리가 나가지 않는다

for (Member m : members) {
	System.out.println("m.getName() = " + m.getName());
}

위 구문에는 동기화도 캐시 초기화도 하지 않는다

때문에 캐시가 아직 남아 있기 때문에
DB에서 데이터를 끌고 오지 않고 1차 캐시에 의존하며 members 컬렉션을 가져오지 못한다

  • 마치 순수한 자바 객체 상태이다

양방향 편의 메서드를 사용하자

사람이다 보면 어느 한쪽의 setter를 놓쳐서 양방향 관계를 놓칠 수 있다

이때 누구 하나를 정해놓고 양방향 편의 메서드를 작성하자

이때 흔하디 흔한 setter 메서드보다는

chageTeam 등 메서드명을 직접 적어 놓는 것이 명시적이기 때문에 좋다

// caller
member.changeTeam(team);

// callee
public void changeTeam(Team team) {
	this.team = team;
	team.getMembers().add(this);
}

혹은 다른쪽에 편의 메서드를 삽입해도 된다

team.addMember(member);

public void addMember(Member member) {
	members.add(member);
	member.setTeam(this);
}

상황에 따라 적절한 곳에 작성하면 된다

그리고 상황에 따라 비즈니스가 복잡할 때는 내부에 null 체크 등을 걸어두어서 작성하면 된다 !

주의할 것 - 그외

  • 양방향 매핑시 무한 루프 !!
    • IDE - toString()
      • 예) 이때 객체들끼리 (team <-> member) 순환참조가 일어나면서 스택오버플로 발생
    • lombok - @ToString
    • JSON
      • 예) 아는 지인분 프로젝트할때 본적이 있다 ㅠㅠ
      • Member Json에서 Team을 반환할때 다시 Team Json에서 Member를 반환하고.. 등등
      • 답이 거의 정해져 있다. 컨트롤러에서 직접적으로 엔티티를 반환하지 말것! DTO를 반환해라 !

설계적인 관점에서 양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 것이다!

    • 양방향 매핑은 반대방향으로 조회 기능이 추가된 것 뿐 ! (객체 그래프 탐색)
  • JPQL에서 / 실제 업무에서 역방향으로 탐색할 일이 많음 !

  • 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 된다

    • 왜냐? 테이블에 영향을 주지 않아 !

연관관계의 주인을 정하는 기준

  • 비즈니스 로직 기준으로 선택하면 안됨

    • 예를들어 바퀴, 자동차 테이블이 있을때 (다대일)
      자동차가 더 중요하다고 이걸 기준으로 하면 안된다 외래키인 바퀴 기준으로 주인으로 정해야 한다
  • 연관관계의 주인은 외래키의 위치를 기준으로 정해야함 (일대다의 )

실전 예제 코드

  • Member의 orders 컬렉션은 없는 것이 깔끔하다..

    • 영한님은 없는 것이 맞다고 보셨다
    • 그러나 학습상의 이유로 넣음
  • 양방향은 JPQL 등의 조회를 할 때가 있을 떄 넣어두어도 늦지 않다

일부만 가져왔으며 몇몇 부분에 대해선 생략하였습니다

@Entity
public class Member {

	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;

	@Column(length = 30)
	private String name;

	private String city;

	@Column(name = "ZIP_CODE")
	private String zipcode;

	@OneToMany(mappedBy = "member")
	private List<Order> orders = new ArrayList<>();
@Entity(name = "ORDERS")
public class Order {

	@Id @GeneratedValue
	@Column(name = "ORDER_ID")
	private Long id;

	public void addOrderItem(OrderItem orderItem) {
		orderItems.add(orderItem);
		orderItem.setOrder(this);
	}
@Entity
public class Item {

	@Id @GeneratedValue
	@Column(name = "ITEM_ID")
	private Long id;

	@Column(name = "ITEM_NAME")
	private String name;

	@OneToMany(mappedBy = "item")
	private List<OrderItem> orderItem = new ArrayList<>();
@Entity
public class OrderItem {

	@Id
	@GeneratedValue
	private Long id;

	@ManyToOne
	@JoinColumn(name = "ORDER_ID")
	private Order order;

	@ManyToOne
	@JoinColumn(name = "ITEM_ID")
	private Item item;

	@Column(name = "ORDER_PRICE")
	private int orderPrice;




© 2020.12. by 따라쟁이

Powered by philz