3. 엔티티 매핑: 다양한 연관관계 매핑

xellos·2022년 6월 22일
0

JPA

목록 보기
6/7
post-thumbnail

다대일

다대일 관계의 반대 방향은 항상 일대다 관계이고 일대다 관계의 반대 방향은 항상 다대일 관계다. DB 테이블의 일, 다 관계에서 외래키는 항상 다쪽에 있다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.

1) 엔티티 매핑

@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;
    
    ...
}

2) 코드를 통한 객체 매핑

Member의 엔티티

@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);
        }
    }
    
}

Team 엔티티

  • 양방향 연관관계는 항상 서로를 참조해야 한다.
  • 양쪽에 다 작성하면 무한 루프에 빠지므로 주의해야 한다.
@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 중에 하나를 사용해야 한다.

1) 일대다 단방향 [1:N]

  • 일대다 단방향 관계는 약간 특이한데 아래를 보면 팀 엔티티의 members로 회원 테이블의 TEAM_ID 외래키를 관리한다.
  • 보통 자신이 매핑한 테이블의 외래키를 관리하는데, 이 매핑은 반대쪽 테이블에 있는 외래키를 관리한다.
@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는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전약을 기본으로 사용해서 매핑한다.


2) 일대다 단방향 매핑의 단점

단점은 매핑한 객체가 관리하는 외래키가 다른 테이블에 있다는 점이다. 본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 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문

3) 일대다 양방향 [1:N, N:1]


일대다 양방향 매핑은 존재하지 않는다. 키 자체가 다쪽에 있기 때문에 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;
    
}

일대일

일대일 관계는 양쪽이 서로 하나의 관계만 가진다. 일대일 관계는 다음과 같은 특징이 있다.

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

1) 주 테이블에 외래키

일대일 관계를 구성할 때 객체지향 개발자들은 주 테이블에 외래 키가 있는 것을 선호한다. 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;
    
}

2) 대상 테이블에 외래키

단방향: 존재하지 않는다.


양방향

@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개로 다대다 관계를 표현할 수 없다. 그래서 이를 연결해주는 테이블을 추가해야 한다. 그런데 객체는 테이블과 다르게 두 객체로 다대다 관계를 만들 수 있다.

1) 다대다: 단방향 매핑

여기서 중요한 점은 @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());
}

2) 다대다: 양방향 매핑

대상 테이블(연관관계의 주인이 아닌 테이블)에 mappedBy를 지정한다.

@Entity
public class Product {

	@Id
    private String id;
    
    @ManyToMany(mappedBy="products") //영방향 추가
    private List<Member> members;
    
}

3) 다대다: 매핑의 한계와 극복, 연결 엔티티 사용

@ManyToMany 를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 편리해진다. 그러나 실무에서 이 매핑을 사용하기에는 현실적으로 어렵다.

보통은 연결 테이블에 주문 수량 컬럼이나 주문 날짜 같은 컬럼이 더 필요하다. 따라서 엔티티간의 고나계도 테이블 관계처럼 다대다 → 일대다, 다대일 관계로 풀어야 한다.

다대다 연관관계를 일대다 다대일 관계로 풀어내기 위해 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야 한다.

  • 식별 관계: 받아온 식별자를 기본키 + 외래 키로 사용한다.
  • 비식별 관계: 받아온 식별자는 외래키로만 사용하고 새로운 식별자를 사용한다.

아래에서는 권장하는 비식별 관계만 살펴본다.


4) 다대다: 새로운 기본키 사용

@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();

0개의 댓글