상세 컨텐츠

본문 제목

JPA 연관관계 매핑 (일대다, 다대일, 일대일, 다대다)

기록 - 프로그래밍/Java

by wjjun 2024. 1. 17. 00:27

본문

 

다중성

엔티티 간에 일대일 관계인지 다대일 관계인지 다중성을 갖고 있다.

다대일(@ManyToOne), 일대다(@OneToMany), 일대일(@OneToOne), 다대다(ManyToMany)

 

다중성을 쉽게 판단하기 위해서는 반대 방향으로 생각을 해보면 됩니다

실무에서는 다대일과 일대다 관계를 사용하며 다대다 관계는 거의 사용되지 않습니다.

 

 

단방향 양방향

테이블은 외래키로 조인을 이용해서 양방향 쿼리가 가능합니다. 그래서 방향의 개념도 없습니다.

객체는 참조용 필드를 가지고 있어 참조하고 있는 연관된 객체를 조회할 수 있습니다.

 

 

다대일 양방향 N:1과 1:N 

객체 연관관계

(N) Member : id, Team team, username

(1) Team : id, List members, name

 

테이블 연관관계

MEMEBER : MEMBER_ID(PK), TEAM_ID(FK), USERNAME

TEAM : TEAM_ID(PK), NAME

 

양방향 관계에서는 외래키가 있는 곳이 양방향 관계의 주인입니다.

일대다, 다대일 관계에서는 항상 다(N)에 외래키가 있습니다.

N을 의미하는 MEMBER 테이블이 외래키를 갖고 있어 Member.team이 연관관계의 주인입니다.

JPA에서는 외래 키를 관리할 때 주인만 사용합니다.

주인이 아닌 Team.members 조회 시에는 JQPL이나 객체 그래프를 탐색할 때 사용합니다.

 

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

어느 한 쪽만 참조하면 양방향 관계가 성립되지 않습니다.

항상 서로 참조하기 위한 메서드 작성이 필요합니다.

setTeam() 메서드 내부에 addMember() 메서드가 포함되도록 한 것이 서로 참조하도록 하는 메서드입니다.

 

 

 

일대다 단방향 1:N

엔티티를 하나 이상 참조하는 관계로 자바 컬렉션 Collection.List, Set, Map 중에 하나를 사용해야 합니다.

 

객체 연관관계 매핑

Team : id, name, List memebers

Member : id, username

 

테이블 연관관계 매핑

TEAM : TEAM_ID(PK), NAME

MEMBER : MEMBER_ID(PK), TEAM_ID(FK), USERNAME

 

팀 엔티티의 Team.members로 회원 테이블의 TEAM_ID 외래 키를 관리합니다.

엔티티 Team.members >> 테이블 MEMBER.TEAM_ID(FK)

public class Team {
    ...
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<Member>(); 
}

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

 

보통 자신이 매핑한 테이블의 외래키를 관리합니다. 이 매핑은 반대쪽 테이블 외래 키를 관리합니다.

일대다 관계에서는 외래 키는 항상 다쪽 테이블에 있습니다.

하지만 다쪽에서는 반대쪽 Team 엔티티에만 참조 필드인 members가 있습니다.

 

일대다 단방향 관계 매핑에는 @JoinColumn을 명시해야 합니다.

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

 

 

일대다 단방향 매핑 단점

매핑한 객체가 관리하는 외래 키가 다른 테이블에 있는 점입니다. 본인 테이블에 외래 키가 있다면 엔티티 저장과 관계 처리를 INSERT SQL로 한번에 처리할 수  있지만 다른 테이블에 외래키가 있어 UPDATE SQL을 추가로 실행해야 합니다.

public void testSave() {

    Member member1 = new Member("member1");
    Member member2 = new Member("member2");
    
    Team team1 = new Team("team1");
    team1.getMembers().add(member1);
    team1.getMembers().add(member2);
    
    em.persist(member1); // INSERT - member1
    em.persist(member2); // INSERT - member2
    em.persist(team1); // INSERT - team1, UPDATE - member1.fk, UPDATE - member2.fk
    
    transaction.commit();
}

 

일대다 단뱡항 매빙보다 다대일 양방향 매핑을 사용하는 것이 좋다

일대다 단방향 매핑을 이용하면 (1) 엔티티를 매핑한 테이블이 아닌 다른 테이블의 외래 키를 관리해야 합니다. (2) 성능 문제 (3) 관리의 부담 이라는 단점이 있습니다.

 

해결하기 위한 방법으로는 일대다 단방향 매핑 대신 다대일 양방향 매핑을 사용해야 합니다.

다대일 양방향 매핑은 관리할 외래 키가 자신 테이블에 있어 일대다 단방향 매핑의 문제가 발생되지 않는다.

다대일 양방향으로 변경할 때는 테이블 모양은 같으며 엔티티만 수정하면 된다.

 

 

일대다 양방향 1:N N:1

일대다 양방향 매핑은 없습니다. 다대일 양방향 매핑을 사용합니다. (왼쪽을 연관관계 주인으로 가정, 다대일이면 다(N)가 연관관계 주인)

 

 

@OneToMany는 연관관계 주인이 될 수 없다. 관계형 데이터베이스 특성상 일대다, 다대일 관계는 항상 다 쪽에 외래키가 있다.

그래서 @OneToMany, @ManyToOne 둘 중 연관관계 주인은 항상 다 쪽인 @ManyToOne을 사용한 곳이다,

그래서 @ManyToOne에는 mappedBy 속성이 없다.

 

읽기만 가능하도록 설정하면 일대다 매핑 양방향도 이용할 수는 있다

근데 이러면 둘 다 같은 키를 관리하기 때문에 문제가 발생할 수 있다.

그래서 반대편인 다대일 쪽은 insertable, updatable false로 설정해서 읽기만 가능하게 설정한다.

public class Member {
    ...
    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    prviate Team team;
}

 

 

일대일 매핑 1:1

양쪽 모두 하나의 관계만 갖는다

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

 

 

1. 주 테이블 외래 키

주 테이블에 외래 키를 두고 대상 테이블을 참조한다

외래 키를 객체 참조와 비슷하게 사용할 수 있어 객체지향 개발자가 선호하는 방식

(JPA도 주 테이블에 외래 키가 있으면 편리하게 매핑 가능)

 

주 테이블 외래키 장점은 주 테이블만 확인해도 대상 테이블과 연관관계를 확인할 수 있다.

 

 

1-1. 단방향

MEMBER 주 테이블, LOCKER 대상 테이블

[1] 객체 연관관계

Member : id, Locker locker, usernmae

Locker : id, name

 

[2] 테이블 연관관계

MEMBER : MEMBER_ID(PK), LOCKER_ID (FK, UNI), USERNAME

LOCKER : LOCKER_ID(PK), NAME

public class Member {
    ...
    @OneToOne
    @JoinColumn(nmae = "LOKCER_ID")
    private Locker locker;
}

public class Locker {
    ...
    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
}

 

 

1-2. 양방향

[1] 객체 연관관계

Member : id, Locker locker, username

Locker : id, name, Member member

 

[2] 테이블 연관관계

MEMBER : MEMBER_ID(PK), LOCKER_ID(FK, UNI), USERNAME

LOCKER : LOCKER_ID(PK), NAME

public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
}

public class Locker {
    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    ...
}

양방향이므로 연관관계 주인을 정해야 합니다.
MEMBER 테이블이 외래 키를 가지고 있어 Member 엔티티에 있는 Member.locker 가 연관관계 주인입니다.

반대 매핑인 사물함의 Locker.member는 mappedBy를 선언해서 연관관계의 주인이 아니라고 설정했습니다.

 

 

2. 대상 테이블 외래 키

전통적 데이터베이스 개발자들은 보통 대상 테이블에 외래 키를 두는 것을 선호한다.

대상 테이블 외래키 장점은 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.

 

 

2-1. 단방향

대상 테이블에 외래 키가 있는 단방향 관계는 JPA 에서 지원하지 않습니다

JPA 2.0부터 일대다 단방향 관계에서 대상 테이블에 외래 키가 있는 매핑을 허용하지만, 일대일 단방향은 이런 매핑을 허용하지 않습니다.

[1] 객체 연관관계

Member : id, Locker locker, username

Locker : id, name, Member member

 

[2] 테이블 연관관계

MEMBER : MEMBER_ID(PK), LOCKER_ID(FK, UNI), USERNAME

LOCKER : LOCKER_ID(PK), MEMBER_ID(FK,UNI), NAME -> 대상 테이블(LOCKER)에 외래 키가 있으면 JPA는 매핑을 허용하지 않습니다.

 

 

2-2. 양방향

외래 키가 있는 양방향 관계

 

객체 연관관계

Member : id, Locker locker, username

Locker : id, name, Member member

 

테이블 연관관계

MEMBER : MEMBER_ID(PK), USERNAME

LOCKER : LOCKER_ID(PK), MEMBER_ID(FK, UID), NAME

public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @OneToOne(mappedBy = "member")
    private Locker locker;
}

public class Locker {
    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
}

일대일 매핑에서 대상 테이블에 외래 키를 두고 싶다면 양방향으로 매핑합니다.

주 엔티티 Member 엔티티 대신 대상 엔티티 Locker를 연관관계의 주인으로 만들어 LOCKER 테이블의 외래 키를 관리 

프록시 사용시 외래키를 직접 관리하지 않는 일대일 관계는 지연 로딩으로 설정해도 즉시 로딩됩니다.

Locker.member는 지연로딩 가능

Member.locker는 지연로딩 불가능해 즉시 로딩 (해결방법은 프록시 대신 bytecode instrumentation 사용)

 

 

다대다 N:N

정규화된 테이블 2개로 다대다 관계는 표현할 수 없다.

중간에 연결 테이블을 추가해야 한다.

 

Member : MEMBER_ID(PK), username

Member_Product : MEMBER_ID(PK, FK), PRODUCT_ID(PK, FK)

Product :  PRODUCT_ID(PK), NAME

 

객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있습니다.

객체가 서로 참조하며 @ManyToMany를 사용해서 매핑할 수 있습니다

 

다대다 단방향

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

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

회원과 상품 엔티티를 @ManyToMany로 매핑했습니다.

@ManyToMany와 @JoinTable을 이용해서 연결 테이블을 바로 매핑한 것입니다.

그래서 회원과 상품을 별도로 연결하는 회원_상품 엔티티 없이도 매핑이 가능한 것입니다.

 

@JoinTable 속성

@JoinTable.name : 연결 테이블 지정

@JoinTable.joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼정보 지정 -> MEMBER_ID 설정

@JoinTable.inverseJoinColumns : 반대 방향인 상품과 매핑할 조인 컬럼정보 지정 -> PRODUCT_ID 설정

 

@ManyToMany는 실무에서 사용이 현실적으로 어렵다.

'회원_상품' 테이블에 엔티티가 추가되면 '회원' 또는 '상품' 엔티티는 추가한 컬럼들을 매핑할 수 없다.

결국 엔티티를 테이블 관계처럼 일대다 또는 다대일 관계로 변경해야만 한다.

관련글 더보기

댓글 영역