Spring

다양한 연관관계 매핑

potatoo 2024. 10. 5. 19:13
728x90

다중성은 왼쪽을 연관관계의 주인으로 한다. 다대일의 경우 다(N)가 연관관계의 주인이다.

 

다대일 단방향(N:1)

다대일 관계의 반대 방향은 항상 일대다 관계다.

일대다 관계의 반대 방향은 항상 다대일 관계다.

외래키는 항상 다쪽에 있다.

객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.

 

다대일 양방향(N:1, 1:N)

양방향은 외래키가  있는 쪽이 연관관계의 주인이다.

항상 다(N)에 외래키가 있다. 주인이 아닌 쪽은 조회를 위한 JPQL이나 객체 그래프를 탐색할 때 사용한다.

 

양방향 연관관계는 항상 서로를 참조해야 한다.

항상 서로를 참조하게 하려면 연관관계 편의 메소드를 작성하는 것이 좋다. 편의 메소드는 한 곳에만 작성하거나 양쪽 다 작성할 수 있다. 하지만 양쪽 다 작성하면 무한루프에 빠질 수 있음으로 주의해야 한다.

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
    
    public void addMember(Member member) {
    	this.members.add(member);
        if (member.getTeam != this) {
        	member.setTeam(this);
        }
    }
}

 

일대다 단방향(1:N)

보통 자신이 매핑한 테이블의 외래키를 관리하는데, 이 매핑은 반대쪽 테이블에 있는 외래키를 관리한다.

일대다 관계에서 외래키는 항상 다쪽 테이블에 있다. 하지만 다 쪽 엔티티에는 외래키를 매핑할 수 있는 참조 필드가 없다. 대신에 일인 부분에 참조필드가 있다. 따라서 반대편 테이블의 외래키를 관리하는 특수한 모습이 나타난다.

@Entity
public class Team {
	
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<Member>();
    
}

@Entity
public class Member {
	
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
}

일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다.

그렇지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다.

일대다 단방향의 단점은 객체가 관리하는 외래키가 다른 테이블에 있다는 점입니다.

INSERT SQL 한 번으로 끝날일이 UPDATE SQL을 추가로 실행해야 합니다.

Member member1 = new Member("member1");
Member member2 = new Member("member2");

Team team = new Team("team");
team.getMembers().add(member1);
team.getMembers().add(member2);

em.persist(member1); // INSERT
em.persist(member2); // INSERT
em.persist(team); // INSERT + UPDATE-member1 + UPDATE-member2

 

따라서 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는게 좋다.

성능과 관리면에서 그게 좋다.

 

일대다 양방향(1:N, N:1)

이건 존재하지 않는다.

대신에 다대일 양방향 매핑을 사용한다.

정확히 말하면 양방향 매핑에서 @OneToMany는 연관관계의 주인이 될 수 없다.

데이터베이스의 특성상 항상 외래키는 다 쪽에 있기 때문이다.

연관관계의 주인은 항상 다 쪽인 @ManyToOne을 사용한 곳이다. 이런 이유로 @ManyToOne에는 mappedBy 속성이 없다.

 

일대다 양방향 매핑이 완전히 불가능한 것은 아니다. 일대다 단방향 매핑 반대편에 같은 외래키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 추가하면 된다.

@Entity
public class Team {
	
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<Member>();
    
}

@Entity
public class Member {
	
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;
}

 

일대일(1:1)

일대일 관계는 주테이블이나 대상 테이블 둘 중 어느 곳이나 외래키 가질 수 있다.

 

주 테이블에 외래키

주 테이블에 외래키를 두고 대상 테이블을 참조하는 방식이다. 객체 참조와 비슷하게 사용할 수 있다. 그래서 객체지향 개발자들이 선호한다.

대상 테이블에 외래키

일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있어서 데이터베이스 개발자들은 보통 이 방식을 선호한다.

 

다대다(N:N)

RDB에서는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
그래서 보통 일대다, 다대일로 풀어서 연결 테이블을 사용한다. 그런데 객체의 경우는 객체 2개로 다대다 관계를 표현 할 수 있다.

컬렉션을 사용해서 서로 참조하면 되기 때문이다.

 

다대다 단방향

@JoinTable을 사용해서 연결 테이블을 매핑할 수 있다.

 

속성

- name : 연결 테이블을 지정 ex) 회원과 상품의 경우 MEMBER_PRODUCT 연결 테이블

- joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정 ex) MEMBER_ID

- inverseJoinColumns : 반대 방향인 상품과 매핑할 조인 컬럼 정보 지정 ex) PRODUCT_ID

@Entity
public class Member {
	
    @Id @Column(name = "MEMBER_ID")
    private String id;
    
    private String username;
    
    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT",
    joinColumns = @JoinColumn(name = "MEMBER_ID"),
    inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
    private List<Product> products = new ArrayList<Product>();
}

@Entity
public class Product {
	
    @Id @Column(name = "PRODUCT_ID")
    private String id;
    
    private String name;
}

 

다대다 양방향

역방향에도 @ManyToMany를 추가 하고, 연관관계의 주인을 지정한다.

mappedBy로 주인이 아닌 엔티티를 지정한다.

양방향 연관관계는 연관관계 편의 메소드를 추가해서 관리하는 것이 편리하다.

 

다대다 매핑을 사용하면 연결 테이블을 자동으로 처리해줘서 도메인 모델이 단순해지고 여러가지 편리점이 있다.

하지만 실무에서 사용하기에는 한계가 있다고 한다.

보통 연결 테이블에 추가적인 컬럼이 필요하다. 회원과 상품의 연결 테이블의 경우에는 수량과 주문 날짜 같은 것들이 필요하다.

그래서 @ManyToMany는 여기서 더 이상 사용할 수 없다. 그래서 @OneToMany와 @ManyToOne으로 나누어야 한다.

 

연결 테이블을 위한 복합 기본키

@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
	
    @Id
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
    
    @Id
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
    
    private int orderAmount;
}

public class MemberProductId implements Serializable {
	
    private String member;
    private String product;
    
    @Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}

기본키 + 외래키 한번에 매핑했다. @IdClass를 사용해서 복합 기본키를 매핑했다.

JPA에서 복합키를 사용하려면 별도의 식별자 클래스를 만들어야 한다. 그리고 @IdClass로 식별자 클래스를 지정하면 된다.

 

복합 기본키를 사용하기 위해서 필요한 것

- 별도의 식별자 클래스

- Serializable 구현

- equals와 hashCode 구현

- 기본 생성자

- public인 식별자 클래스

- @IdClass 또는 @EmbeddedId를 사용한다.

 

※ 식별 관계 : 부모 테이블의 기본키를 받아서 기본키 + 외래키로 사용하는 것

 

복합키를 사용하면 불편한 점

- 항상 식별자 클래스를 만들어야 한다. 조회를 할 때 식별자 클래스로 엔티티를 조회한다.

- 복잡하다. ORM 매핑에서 처리할 일이 상당히 많아진다.

- 식별자 클래스도 만들어야하고, @IdClass 또는 @EnbeddedId도 사용해야 한다.

- equals, hashCode도 구현해야 한다.

 

복합키를 사용하지 않고, 다대다 관계 구성법

Long 값의 대리키를 사용한다. 간편하고 영구적으로 쓸 수 있으며 비즈니스에 독립적이다. 또한 ORM을 수행할 때 복합키를 만들지 않아돼서 간단히 매핑을 할 수 있다.

 

※ 비식별 관계 : 받아온 식별자를 외래키로만 사용하고 새로운 식별자를 추가한다.

 

식별자 클래스를 만들지 않아도 되는 비식별 관계를 선호한다.

728x90