다대일 관계의 반대 방향은 항상 일대다 관계이고 일대다 관계의 반대 방향은 항상 다대일 관계다. DB 테이블의 일, 다 관계에서 외래키는 항상 다쪽에 있다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private STring username;
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
...
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
public void setTeam(Team team) {
this.team = team;
//무한 루프에 빠지지 않도록 확인
if(!team.getMembers().contains(this)) {
team.getMembers().add(this);
}
}
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name="TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy="team")
private List<Member> members = new ArrayList<>();
public void addMember(Member member) {
this.members.add(member);
//무한 루프에 빠지지 않도록 체크
if(member.getTeam() != null) {
member.setTeam(this);
}
}
}
일대다 관계는 다대일 관계의 반대 방향이다. 일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션 Collection, List, Set, Map 중에 하나를 사용해야 한다.
@Entity
public class Team {
@Id @GeneratedValue
@Column(name="TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name="TEAM_ID") //MEMBER 테이블의 TEAM_ID(FK)
private List<Member> members = new ArrayList<>();
}
일대다 단방향 관계를 매핑할 때는 @JoinColumn
을 명시해야 한다. 그렇지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전약을 기본으로 사용해서 매핑한다.
단점은 매핑한 객체가 관리하는 외래키가 다른 테이블에 있다는 점이다. 본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT 한 번에 끝낼 수 있지만 다른 테이블에 있으면 연관관계 처리를 위한 UPDATE 문이 추가로 실행되야 한다.
Member m1 = new Member("member1");
Member m2 = new Member("member2");
Team t1 = new Team("team");
t1.getMembers().add(m1);
t1.getMembers().add(m2);
em.persist(m1);
em.persist(m2);
em.persist(team1);
위의 코드를 실행하면 아래의 SQL이 실행된다.
insert into Member(MEMBER_ID, username) values(null, ?);
insert into Member(MEMBER_ID, username) values(null, ?);
insert into Team(TEAM_ID, name) values(null, ?);
update Member set TEAM_ID = ? where MEMBER_ID=? //비효율적인 추가 UPDATE문
update Member set TEAM_ID = ? where MEMBER_ID=? //비효율적인 추가 UPDATE문
일대다 양방향 매핑은 존재하지 않는다. 키 자체가 다쪽에 있기 때문에 one 쪽이 연관관계의 주인이 될 수 없기 때문이다.
그러나 완전히 불가능한 것은 아니다. 일대다 단방향 매핑 반대편에 같은 외래키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가하면 된다.
그러나 이 방식은 결코 추천하지 않고 다대일 앙방향 매핑을 권장한다.
@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<>();
}
@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;
}
일대일 관계는 양쪽이 서로 하나의 관계만 가진다. 일대일 관계는 다음과 같은 특징이 있다.
일대일 관계를 구성할 때 객체지향 개발자들은 주 테이블에 외래 키가 있는 것을 선호한다. JPA도 주테이블에 외래키가 있으면 좀 더 편하게 매핑할 수 있다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
@OneToOne
@JoinColumn(name="LOCKER_ID")
private Locker locker;
...
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
private String name;
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private STring username;
@OneToOne
@JoinColumn(name="LOCKER_ID")
private Locker locker;
...
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
private String name;
//1:1 관계에서도 연관관계의 주인이 아니면 mappedBy가 필요하다.
@OneToOne(mappedBy="locker")
private Member member;
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy="member")
private Locker locker;
}
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn(name="MEMBER_ID")
private Member member;
}
관계형 DB는 정규화된 테이블이 2개로 다대다 관계를 표현할 수 없다. 그래서 이를 연결해주는 테이블을 추가해야 한다. 그런데 객체는 테이블과 다르게 두 객체로 다대다 관계를 만들 수 있다.
여기서 중요한 점은 @ManyToMany
와 @JoinTable
을 사용해서 연결 테이블을 바로 매핑하였다는 것이다.
@Entity
public class Member {
@Id @Column(name="MEMBER_ID")
private String id;
private String username;
@ManyToMany
@JoinTable(
name="MEMBER_PRODUCT",
joinColumn = @JoinColumn(name="MEMBER_ID"),
inverseJoinColumns = @JoinColumn(name="PRODUCT_ID"))
private List<Product> products = new ArrayList<>();
}
@Entity
public class Product {
@Id @Column(name="PRODUCT_ID")
private String id;
private String name;
}
Product productA = new Product();
productA.setId("productA");
productA.setName("상품A");
em.persist(productA);
Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");
member1.getProducts().add(productA);
em.persist(member1);
Member member = em.find(Member.class, "member1");
List<Product> products = member.getProducts();
for(Product product: products) {
System.out.println("product.name = " + product.getName());
}
대상 테이블(연관관계의 주인이 아닌 테이블)에 mappedBy를 지정한다.
@Entity
public class Product {
@Id
private String id;
@ManyToMany(mappedBy="products") //영방향 추가
private List<Member> members;
}
@ManyToMany
를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 편리해진다. 그러나 실무에서 이 매핑을 사용하기에는 현실적으로 어렵다.
보통은 연결 테이블에 주문 수량 컬럼이나 주문 날짜 같은 컬럼이 더 필요하다. 따라서 엔티티간의 고나계도 테이블 관계처럼 다대다 → 일대다, 다대일 관계로 풀어야 한다.
다대다 연관관계를 일대다 다대일 관계로 풀어내기 위해 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야 한다.
아래에서는 권장하는 비식별 관계만 살펴본다.
@Entity
public class Order {
@Id @GeneratedValue
@Column(name="ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name="MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name="PRODUCT_ID")
private Product product;
private int orderAmount;
}
@Entity
public class Member {
@Id @Column(name="MEMBER_ID")
private String id;
private String username;
@OneToMany(mappedBy="member")
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Product {
@Id @Column(name="PRODUCT_ID")
private String id;
private String name;
}
public void save() {
//회원 저장
Member member1 = new Member();
member1.setId("Member1");
member1.setUsername("회원1");
em.persist(member1);
//상품 저장
Product productA = new Product();
productA.setId("productA");
productA.setName("상품");
em.persist(productA);
//주문 저장
Order order = new Order();
order.setMember(member1);
order.setProduct(productA);
order.setOrderAmount(2);
em.persist(order);
}
Long orderId = 1L;
Order order = em.find(Order.class, orderId);
Member member = order.getMember();
Product product = order.getProduct();