일대다 (1:N)
관계는 다대일 관계의 반대 방향입니다.
일 (1)
이 외래키를 관리하며, 연관관계의 주인 입니다.
해당 관계는 표준 스펙에서 지원은 하지만, 실무에서 권장하지 않는 관계 입니다.
일대다 연관관계를 사용하여 발생하는 문제점들을 통해서 그 이유를 알아보도록 하겠습니다.
이해를 돕기 위해, 회원(N, Member)과 팀(1, Team)의 관계를 예시로 들어보겠습니다.
위 테이블 모델링 조건에 일대다 단방향 관계를 위한 추가 조건은 아래와 같습니다.
위 객체 모델링을 보면, 테이블 모델링 상 외래키를 소유하고 있는 MEMBER 테이블의 반대편인 TEAM 테이블의 객체인 Team 클래스가 외래키를 관리하고 있는 특이한 모습입니다.
해당 객체 모델링을 코드로 나타내어 보도록 하겠습니다.
Team 클래스 (일대다
에서 일
에 해당합니다.)
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private Long id;
@Column(name = "NAME")
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID (FK)
pricate List<Member> members = new ArrayList<>();
// Getter, Setter, Constructor, ...
}
Member 클래스 (일대다
에서 다
에 해당합니다.)
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
// Getter, Setter, Constructor
}
일대다 단방향
매핑에서는 일
에 해당하는 클래스에 다
에 해당하는 클래스를 Collection 프레임워크로 감싼 형태의 참조 필드로 작성해주시면 됩니다.
이때, 해당 참조 필드위에 @OneToMany
와 @JoinColumn(name = "외래키 이름")
을 추가하여 줍니다.
외래키를 매핑할 때 사용합니다.
자세한 속성은 JPA 단방향 연관관계 에 있는 설명과 동일합니다.
일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 합니다.
그렇지 않으면 JPA 는 연결 테이블을 중간에 생성하고, 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용하여 매핑합니다.
일대다 관계에서 사용합니다.
속성 | 기능 | 기본값 |
---|---|---|
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다 | true |
mappedBy | 연관관계의 주인 필드를 선택한다. | |
fetch | 글로벌 패치 전략을 설정한다. (자세한 내용은 추후에) | @ManyToOne=FetchType.EAGER (즉시 로딩), @OneToMany=FetchType.LAZY (지연 로딩) |
cascade | 영속성 전이 기능을 사용한다. (자세한 내용은 추후에) | |
orphanRemoval | true 로 설정 시, 고아 객체를 즉시 삭제합니다. | false (고아 객체를 삭제하지 않습니다.) |
targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않음 |
연관관계를 등록, 삭제 하는 예제를 통해 연관관계를 어떻게 사용하는지 알아보겠습니다.
Member member1 = new Member(0L, "회원1");
Member member2 = new Member(1L, "회원2");
entityManager.persist(member1); // INSERT-MEMBER1
entityManager.persist(member2); // INSERT-MEMBER2
Team team1 = new Team(0L, "팀1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);
entityManager.persist(team1); // INSERT-TEAM1, UPDATE-MEMBER1.FK, UPDATE-MEMBER2.FK
INSERT INTO MEMBER (MEMBER_ID, USERNAME) VALUES (0, '회원1');
INSERT INTO MEMBER (MEMBER_ID, USERNAME) VALUES (1, '회원2');
INSERT INTO TEAM (TEAM_ID, NAME) VALUES (0, '팀1');
UPDATE MEMBER SET TEAM_ID = 0 WHERE MEMBER_ID = 0;
UPDATE MEMBER SET TEAM_ID = 0 WHERE MEMBER_ID = 1;
실행되는 SQL 코드를 보면 @OneToMany 단방향 매핑의 단점을 알 수 있습니다.
해결법은 우선 다대일 단방향 매핑으로 테이블 모델링과 일치시켜준 후, 필요에 따라 일대다 단방향 매핑을 추가한 다대일 양방향 매핑을 사용합니다.
orphanRemoval 옵션을 true 로 하여 고아 객체를 어떻게 관리하는지 살펴보겠습니다.
Team team = entityManager.find(Team.class, 0L);
entityManager.remove(team);
UPDATE MEMBER SET TEAM_ID = null WHERE TEAM_ID = 0;
DELETE FROM MEMBER WHER MEMBER_ID = 0;
DELETE FROM MEMBER WHER MEMBER_ID = 1;
DELETE FROM TEAM WHER MEMBER_ID = 0;
고아객체 삭제 옵션을 사용하였기 때문에, team 객체의 삭제로 인하여 고아객체가 되어버린 member 객체 역시 삭제되었습니다.
이런 매핑은 공식적으로 존재하지 않습니다..
대신 다대일 양방향 매핑을 사용하도록 하자 (일대다 양방향 == 다대일 양방향)
일(1), 다(N) 관계에서는 항상 다(N)에 외래키가 있으므로 @OneToMany, @ManyToOne 둘 중에 연관관계의 주인은 항상 다 쪽인 @ManyToOne 입니다.
- 이런 이유로 @ManyToOne 에는 연관관계의 주인을 설정해주는 mappedBy 속성이 존재하지 않습니다..
그렇다고 일대다 양방향 매핑이 완전히 불가능한 것 은 아닙니다.
일(1)
(Team) 입니다.해당 객체 모델링을 코드로 나타내어 보도록 하겠습니다.
Team 클래스 (일대다
에서 일
에 해당합니다.)
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private Long id;
@Column(name = "NAME")
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID (FK)
pricate List<Member> members = new ArrayList<>();
// Getter, Setter, Constructor, ...
}
Member 클래스 (일대다
에서 다
에 해당합니다.)
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
// Getter, Setter, Constructor
}
@ManyToOne과 @JoinColumn을 사용해서 연관관계를 매핑하면, 다대일 단방향 매핑이 되어버립니다. 그런데 반대쪽 Team에서 이미 일대다 단방향 매핑이 설정되어있습니다.
이런 상황에서는 두 엔티티에서 모두 테이블의 FK 키를 관리 하게 되는 상황이 벌어집니다.
그걸 막기 위해서 insertable, updatable 설정을 FALSE로 설정하고 읽기 전용 필드로 사용해서 양방향 매핑처럼 사용하는 방법입니다.
위 방법은 일대다 양방향 매핑이라기 보다, 일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 읽기 전용으로 추가하여 일대다 양방향 처럼 보이게 하는 방법입니다.
결론은 다대일 양방향을 사용하면 편합니다.