여기서 다중성은 DB를 기준으로 다중성을 체크해주면 된다.
참고로 현업에서는 다대다 매핑은 사용하면 안 된다.
why? =>
테이블
• 외래 키 하나로 양쪽 조인 가능
• 사실 방향이라는 개념이 없음
객체
• 참조용 필드가 있는 쪽으로만 참조 가능
• 한쪽만 참조하면 단방향
• 양쪽이 서로 참조하면 양방향 (사실은 단방향이 2개가 서로를 바라보고 있는 개념이다.)
테이블
외래 키 하나로 두 테이블이 연관관계를 맺음
객체
• 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데
• 객체 양방향 관계는 참조가 2군데 있음. 둘중 테이블의 외래 키를 관리할 곳을 지정해야함
외래 키를 관리하는 참조가 바로 연관관계의 주인이 됨.
• 주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능
혹시 이해가 잘 안 된다면 앞선 포스팅의 예제부분에서 훨신 쉽게 이해할 수 있다.
참고로 이 포스팅에서 나오는 연관관계의 앞 부분을 연관관계의 주인이다고 가정하고 작성하는 것이다.
테이블 관계에서는 당연히 항상 "다" 방향에 외래키가 있는게 맞다.
@Data
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Data
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
}
Member 에서는 Team 을 참조해야하기 때문에 "다" 인 부분에 연관관계를 설정해준 모습이고 반대로 Team에서는 굳이 Member를 참조할 필요가 없기 때문에 그냥 본인 테이블의 컬럼만 선언하면 된다.
그리고 만약 양방향을 원한다면 그냥 Team에도 아래 코드처럼 참조하는 코드를 추가하면 되는데 앞서 언급했듯 이때 Table 의 구조는 변하지 않는다.
Table은 원래 양방향이기 때문이다.
@Data
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<>();
}
이 모델은 권장하지 않는 모델이다.
일대 다는 "다" 가 아닌 "일" 쪽에서 외래키도 관리하고 뭔가를 해보겠다는 것이다.
그럼 복잡도가 매우 높아지는데 Team의 "List members"가 변경되면Member 테이블의 Team 이 변경되어야한다.
@Data
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
}
@Data
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
/*@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();*/
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
}
이런식으로 entity 를 설정하고 아래 코드로 query 를 실행시켜보면
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setName("member1");
em.persist(member);
Team team = new Team();
team.setName("teamA");
team.getMembers().add(member);
em.persist(team);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
결론적으로 불필요한 update query 가 한 번 더 나가게 되는 것이다.
그리고 더 결정적인 이유는 비즈니스 로직상 내가 Team 을 바꿨는데 갑자기 Member 테이블의 쿼리가 나가기 때문에 굉장히 모호하고 추적이 어렵다.
따라서 일대다 로 설계가 나와버린 경우에는 application 레벨에서 조금 손해(굳이 Member->Team 을 바라보는 field 를 추가하는 행위)를 보더라도 양방향으로 만들어두는 것이 더 좋다. 그리고 사용을 다대일 처럼 사용하면 되는 것이다.
특징
• 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
• 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
• 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
• @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가함)
단점
• 엔티티가 관리하는 외래 키가 다른 테이블에 있음
• 연관관계 관리를 위해 추가로 UPDATE SQL 실행
일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자
@Data
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
/*@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();*/
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
}
@Data
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
// @Column(name = "TEAM_ID")
// private Long teamId;
@ManyToOne
@JoinColumn(name= "TEAM_ID", insertable = false, updatable = false)
private Team team;
}
• 일대일 관계는 그 반대도 일대일
• 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
• 주 테이블에 외래 키
• 대상 테이블에 외래 키
• 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가
• 다대일(@ManyToOne) 단방향 매핑과 유사
public class Locker {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member;
}
@Data
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
...
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
그냥 마치 다대일 관계 한 것 처럼 해주면 된다.
• 다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인
• 반대편은 mappedBy 적용
• 단방향 관계는 JPA 지원X, 방법 없음.
• 양방향 관계는 지원 (대상 테이블에 외래 키 양방향)
• 사실 일대일 주 테이블에 외래 키 양방향과 매핑 방법은 같음
사실 굉장히 어색한 것 임 그냥 Member 에서 관리하던걸 Locker 로 넘기고 그것에 맞춰 객체 연관관계를 다시 설정해준 것일 뿐임.
그래서 "주 테이블에 외래 키 양방향" 과 "대상 테이블에 외래 키 양방향" 뭘 사용해야 할까?
사실 트레이드 오프라고 표현할 수 있을지 모르겠지만 개발자와 DBA 의 관점 차이가 여기서 갈린다. 실제로 배포중인 DB를 갈아엎은 건 굉장히 위험한 일이라 DB를 기준으로 설계하는 것이 맞겠지만 만약 위와같은 상황에서 비즈니스가 "멤버가 여러개의 락커를 사용가능" 혹은 "하나의 락커에 여러명의 사용자가 사용가능" 이런식으로 어떻게 바뀌나에 따라 이 외래키를 Member 에서 관리하나 Locker 에서 관리하냐 갈릴 수 있다. 그래서 그걸 잘 예측하여 설계를하면 좋은데 사실 개발자 기준에서는 Member 에 해당 키를 갖고있는게 훨씬 유리하다. 멤버만 조회하면 어떤 locker를 사용하는지, locker 가입자 인지 아닌지 등을 판별할 수 있기 때문이다.
주 테이블에 외래 키 (개발자 관점)
• 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
• 객체지향 개발자 선호
• JPA 매핑 편리
• 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
• 단점: 값이 없으면 외래 키에 null 허용
대상 테이블에 외래 키 (DBA 관점)
• 대상 테이블에 외래 키가 존재
• 전통적인 데이터베이스 개발자 선호
• 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
• 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨 => Member 테이블만으로는 Locker 사용 여부등 조회가 불가능 => Locker 를 조회하는 query 가 반드시 날아감 => proxy 만들 필요가 없음 => 무조건 즉시 로딩이 됨.
• @ManyToMany 사용
• @JoinTable로 연결 테이블 지정
• 다대다 매핑: 단방향, 양방향 가능
@Entity
@Data
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
// 양방향을 원한다면 아래 코드 추가
@ManyToMany(mappedBy = "products")
private List<Member> members = new ArrayList<>();
}
@Data
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
...
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();
}
이렇게 하면 바로위 사진 처럼 동작하게 된다.
• 편리해 보이지만 실무에서 사용X
• 현장은 연결 테이블이 단순히 연결만 하고 끝나지 않는다. 분명 주문시간, 수량 같은 데이터가 들어올 수 있는데 이 정보 데이터를 더 추가할 수 없다는 단점이 존재한다.
• 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
• @ManyToMany -> @OneToMany, @ManyToOne 사실 그냥 @ManyToMany 를 안 쓰는 것임 ㅋㅋ
ID(PK) 는 어지간 하면 비즈니스와 관련이 없는 랜덤값으로 넣어주는 것이 좋다.
그래야 비즈니스 변경이 잦은데 그런 영향으로부터 자유로울 수 있다.
그니까 그냥 @GenerateValue 를 애용하자
• 외래 키를 매핑할 때 사용
속성 | 설명 | 기본 값 |
---|---|---|
name | 매핑할 외래 키 이름 | "필드명"+"_"+"참조하는 테이블의 기본 키 컬럼 명" |
referencedColumnName | 외래 키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본 키 컬럼명 |
foreignKey(DDL) | 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다. | - |
unique nullable insertable updatable columnDefinition table | @Column의 속성과 같다. | - |
• 다대일 관계 매핑
속성 | 설명 | 기본 값 |
---|---|---|
optional | false로 설정하면 연관된 엔티티가 항상 있어야한다. | TRUE |
fetch | 글로벌 페치 전략을 설정한다. | -@ManyToOne=FetchType.EAGER -@OneToMany=FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다. | - |
targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다. | - |
• 일대다 관계 매핑
속성 | 설명 | 기본 값 |
---|---|---|
mappedBy | 연관관계의 주인 필드를 선택한다. | - |
fetch | 글로벌 페치 전략을 설정한다. | -@ManyToOne=FetchType.EAGER -@OneToMany=FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다. | - |
targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다. | - |