어플리케이션개발/JPA

JPA 프로그래밍 학습 정리 (3) - 연관관계 매핑

안잡아모찌 2024. 1. 18. 17:58

엔티티들은 대부분 다른 엔티티들과의 연관관계가 있다.

이 연관관계에는 방향, 다중성, 연관관계의 주인라는 3가지의 핵심 키워드가 있다.

  • 방향 : 단방향, 양방향
  • 다중성 : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
  • 연관관계의 주인

 

단방향 연관관계

연관관계에서 가장 핵심적이고 중요한 것은 다대일(N:1) 단방향 관계의 이해이다.

테이블의 연관관계 관점에서는 항상 양방향이지만, 객체 연관관계 관점에서는 단향방이 존재한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Member{
    private String id;
    private String username;
 
    private Team team; // 팀의 참조를 보관    
    ...
}
 
public class Team{
    priavte String id;
    private String name;
    ...
}
cs

객체는 참조를 사용해서 연관관계를 탐색할 수 있고 이것을 객체 그래프 탐색이라 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entity
public class Member {
    
    @Id
    @Column(name = "MEMBER_ID")
    private String id;
 
    //연관관계 매핑
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
 
    //연관관계 설정
    public void setTeam(Team team){
        this.team = team;
    }
}
 
@Entity
public class Team {
    
    @Id
    @Column(name = "TEAM_ID")
    private String id;
 
    ...
}
cs

회원 객체의 Member.team 필드를 사용하여 객체 연관관계를 설정한다.

  • @ManyToOne : 다대일 관계 매핑
  • @JoinColumn(name = "TEAM_ID") : 조인 컬럼 매핑

연관관계 매핑 후의 CRUD

저장

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void testSave(){
    //팀1 저장
    Team team1 = new Team("team1""팀1");
    em.persist(team1);
 
    //회원1 저장
    Member member1 = new Member("member1""회원1");
    member1.setTeam(team1); // 연관관계 설정 member1 -> team1
    em.persist(member1);
 
    //회원2 저장
    Member member2 = new Member("member2""회원2");
    member2.setTeam(team1); // 연관관계 설정 member2 -> team1
    em.persist(member2);
}
cs

 

조회

  • 객체 그래프 탐색 : member.getTeam() 로 객체 탐색
  • 객체지향 쿼리사용(JPQL) : select m from Member m join m.team t where t.name = :teamName

 

수정

1
2
3
4
5
6
7
8
9
10
private static void updateRelation(EntityManager em){
 
    //new team2
    Team team2 = new Team("team2""팀2");
    em.persist(team2);
 
    //update new team
    Member member = em.find(Member.class"member1");
    member.setTeam(team2);
}
cs

앤티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다.

 

연관관계 제거

member1.setTeam(null) 처리로 연관관계를 제거한다.

 

연관된 엔티티 제거

연관된 엔티티를 제거하려면 기존에 있던 연관관계들을 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생한다.

 

양방향 연관관계
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Entity
public class Member {
    
    @Id
    @Column(name = "MEMBER_ID")
    private String id;
 
    //연관관계 매핑
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
 
    //연관관계 설정
    public void setTeam(Team team){
        this.team = team;
    }
}
 
@Entity
public class Team {
    
    @Id
    @Column(name = "TEAM_ID")
    private String id;
 
    //==추가==//
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
 
    ...
}
 
cs

mappedBy 속성을 통해 반대쪽 매핑의 필드 이름을 값으로 주면 된다.

1
2
3
4
5
public void biDirection(){
 
    Team team = em.find(Team.class"team1");
    List<Member> members = team.getMembers();
}
cs

 

연관관계의 주인

연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다. 연관관계의 주인은 외래 키가 있는 곳으로 정해야 한다. 그리고 테이블의 다(N)쪽이 항상 외래 키를 가진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void testSaveNonOwner(){
 
    //회원1 저장
    Member member1 = new Member("member1""회원1");
    em.persist(member1);
 
    //회원2 저장
    Member member2 = new Member("member2""회원2");
    em.persist(member2);
 
    Team team1 = new Team("team1","팀1");
    //주인이 아닌 곳만 연관관계 설정
    team1.getMembers().add(member1);
    team1.getMembers().add(member2);
 
    em.persist(team1);
}
cs

양방향 연관관계에서 주인이 아닌 곳에만 값을 입력하는 실수가 가장 흔하다.

 

순수한 객체까지 고려한다면

객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. 그래서 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라 한다. 그리고 기존 관계제거가 없으면 관계를 업데이트 했을때, 관계가 남아있어 문제가 생긴다.

1
2
3
4
5
6
7
8
9
public void setTeam(Team team){
 
    //기존 팀과 관계를 제거
    if(this.team != null){
        this.team.getMembers().remove(this);
    }
    this.team = team;
    team.getMembers().add(this);
}
cs

 

결론

  • 단방향 매핑만으로 테이블과 객체의 연관관게 매핑은 이미 완료
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.

일대다 관계

일대다(1:N) 관계는 자바컬렉션인 Collection, List, Set, Map 중에 하나를 사용해야 한다.

하지만 외래 키를 연관관계의 주인이 관리할 수 없기때문에 가급적 다대일 양방향 매핑으로 변환해서 관계를 맺어야한다.

 

일대일 관계

  • 주 테이블에 외래 키 관리 : 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.
  • 테이블 관계를 일대일에서 일대다로 변경할 때 테이블구조를 그대로 유지할 수 있다.

 

주 테이블에 외래 키
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 단방향 
@Entity
public class Member{
 
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
 
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Lock locker;
 
    ...
}
 
@Entity
public class Locker{
 
    @Id @GeneratedValue
    @Column(name = "LOCKED_ID")
    private Lond id;
}
 
 
 
// 양방향
@Entity
public class Member{
 
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
 
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Lock locker;
 
    ...
}
 
@Entity
public class Locker{
 
    @Id @GeneratedValue
    @Column(name = "LOCKED_ID")
    private Lond id;
 
    @OneToOne(mappedBy = "locker")
    private Member member;
}
 
cs

 

 

대상 테이블에 외래 키

이 경우, 단방향은 없다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 양방향
@Entity
public class Member{
 
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
 
    @OneToOne(mappedBy = "member")
    private Lock locker;
 
    ...
}
 
@Entity
public class Locker{
 
    @Id @GeneratedValue
    @Column(name = "LOCKED_ID")
    private Lond id;
 
    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
}
 
cs

 

 

다대다

1
2
3
4
5
6
7
8
9
10
11
@Entity
public class Member {
 
    @Id @Column(name = "MEMBER_ID")
    private String id;
 
    @ManyToMany
    @joinaTable(name = "MEMBER_PRODUCT",
                joinColumns = @JoinColumn(name = "MEMBER_ID"),
                inverseJoinColumns)
}
cs

어노테이션을 통해 연결테이블을 바로 매핑하는 것이 가능하다.

  • @JoinTable.name : 연결 테이블 지정
  • @JoinTable.joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼 정보 지정
  • @JoinTable.inverseJoinColumns : 반대 방향인 상품과 매핑할 조인 컬럼 정보 지정

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void save(){
 
    Product productA = new Product();
    productA.setId("A");
    productA.setName("A");
    em.persist(productA);
 
    Member member1 = new Member();
    member1.setId("a");
    member1.setUserName("a");
    
    member1.getProducts().add(productA); //연관관계 설정
    em.persist(member1);
}
 
 
 
public void find(){
 
    Member member = em.find(Member.class"member1");
    List<Product> products = member.getProducts(); //객체그래프 탐색
 
    ...
}
cs

 

다대다의 양방향은

양쪽 중 원하는 곳에 mappedBy로 연관관게의 주인을 지정하고 연관관계 편의 메소드를 추가하면 된다.

 

하지만,  이 매핑은 실무에서 사용하기엔 한계가 있다.

더 필요한 컬럼이 생길수도 있기 때문이다. 그래서 연결엔티티를 새롭게 생성하는 방법이 더 좋다.

  • 식별 관계 : 받아온 식별자를 기본 키 + 외래 키로 사용한다.
  • 비식별 관계 : 받아온 식별자는 외래 키로만 사용하고 새로운 식별자를 추가한다. (추천)

 

식별관계
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
 
    @Id
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member; // MemberProductId.member와 연결
 
    @Id
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product; //MemberProductId.product와 연결
 
    ...
}
 
public class MemberProductId implements Serializable {
 
    private String member; // MemberProduct.member와 연결
    private String product; //MemberProduct.product와 연결
 
    //hashCode and equals
 
    @Override
    public boolean equals(Object o) {...}
}
cs

부모테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키(@Id + @JoinColumn)으로 사용하는 것을 식별 관계라 한다.

2개의 복합 키를 만들려면 별도의 식별자 클래스가 있어야 한다. 식별자 클래스는

  • Serializable을 구현해야한다
  • equals와 hashCode 메소드를 구현해야 한다.
  • 기본생성자가 있어야한다.
  • public 클래스여야 한다.

 

새로운 기본 키 사용

데이터베이스에서 자동으로 생성해주는 대리 키를 Long 값으로 사용하는 것

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Entity
public class Order{
 
    @Id @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;
 
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    priavte Member member;
 
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    priavte Product product;
 
    ...
}
 
 
public void save(){
 
    //save member
    Member member1 = new Member();
    member1.setId("a");
    ..
    em.persist(member1);
 
    //save product
    Product product = new Product();
    product.setId("b");
    ..
    em.persist(product);
 
    //save Order
    Order order = new Order();
    order.setMember(member1); //주문 회원 - 연관관계 설정
    order.setProduct(product); //주문 상품 - 연관관계 설정
    order.setOrderAmount(2); // 주문수량
    em.persist(order);
}
 
 
public void find(){
 
    Long orderId = 1L;
    Order order = em.find(Order.class, OrderId);
 
    Member member = order.getMember();
    Product product = order.getProduct();
}
cs