Spring/jpa

[JPA] 다대일, 일대다, 일대일, 대다대

라임온조 2023. 2. 14. 11:23

1. 연관관계 매핑할 때 고려해야 할 것

1) 다중성

다중성 종류

  • 다대일
  • 일대다
  • 일대일
  • 다대다

2) 단방향, 양방향

두 엔티티 중 한쪽만 참조하는 단방향 관계인지, 서로 참조하는 양방향 관계인지

3) 연관관계의 주인

양방향 관계면 연관관계의 주인을 정해야 한다

 

전제 조건: 다중성은 왼쪽을 연관관계의 주인으로 정했다. 예를 들어 일대다면 일 쪽이 연관관계의 주인인 것.

 

2. 다대일

1) 다대일 단방향

Member(다) Team(일)
id id
Team team name
username  

 

MEMBER TEAM
MEMBER_ID(PK) TEAM_ID(PK)
TEAM_ID(FK) NAME
USERNAME  
@Entity
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 Team{
	
    @Id
    @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    
    private String name;
    
}

2) 다대일 양방향

Member Team
id id
Team team List member
username name
MEMBER TEAM
MEMBER_ID(PK) TEAM_ID(PK)
TEAM_ID(FK) NAME
USERNAME  
@Entity
public class Member{
	@Id
    @GeneratedValue
    @Column(name = "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<Member>();
    
    public void addMember(Member member){
    	this.members.add(member)
        
        // 무한 루프에 빠지지 않게 검사
        if(member.getTeam() != this){
        	member.setTeam(this);
        }
    }
}

특징

  • 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다
  • 양방향 연관관계는 항상 서로를 참조한다
    • 연관관계 편의 메소드(addMember(), setTeam())를 사용

3. 일대다

1) 일대다 단방향

Team Member
id id
name username
List members  
TEAM MEMBER
TEAM_ID(PK) MEMBER_ID(PK)
NAME TEAM_ID(FK)
  USERNAME
@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<Member>();
}
@Entity
public class Member{
	@Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    
    private String username;
}

특징

  • 엔티티를 하나 이상 사용할  수 있으므로 자바 컬렉션 중 하나를 사용해야 한다
  • 반대쪽 테이블에 있는 외래 키를 관리한다
    • 일대다 관계에서 외래 키는 항상 다쪽 테이블에 있다. 하지만 다 쪽인 Memer 엔티티에는 외래 키를 매핑할 수 있는 참조 필드가 없다. 대신에 반대쪽인 Team 엔티티에만 참조 필드인 members가 있다. 따라서 반대편 테이블의 외래 키를 관리하는 특이한 모습이 나타난다.
  • @JoinColumn을 무조건 명시해야 한다.

 

단점

  • 만약 본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT SQL 한 번으로 끝낼 수 있지만, 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있어서 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.
  • 위와 같은 문제로 인해 성능 문제도 발생가능 하고, 관리도 부담스럽다.
    • 그러니 일대다 단방향보다는 다대일 양방향 매핑을 사용하자

2) 일대다 양방향

Team Member
id id
name username
List members Team team
TEAM MEMBER
TEAM_ID(PK) MEMBER_ID(PK)
NAME TEAM_ID(FK)
  USERNAME
@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<Member>();
}
@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) // Team과 같은 외래키를 관리하게 되어서 읽기만 가능하게 속성 설정
    private Team team;
}

특징

  • 일대다 양방향 매핑은 존재하지 않는다. 대신 다대일 양방향 매핑을 사용해야 한다. 데베 특성상 일대다, 다대일 관계는 항상 다 쪽에 외래 키가 있다. 따라서, @ManyToOne에는 mappedBy 속성이 없다.
  • 하지만, 일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 추가하면 가능하긴 하다.
  • 일대다 단방향 매핑이 가지는 단점을 그대로 가진다. 그러니 될 수 있으면 다대일 양방향 매핑을 사용해야 한다.

4. 일대일

특징

  • 일대일 관계의 반대도 일대일 관계다.
  • 테이블 관계에서 일대다, 다대일은 항상 다쪽이 외래 키를 가진다. 반면에 일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래 키를 가질 수 있다. 따라서 주 테이블이나 대상 테이블 중에 누가 외래 키를 가질지 서택해야 한다.

1) 주 테이블에 외래 키

특징

  • 주 테이블에 외래 키를 두고 대상 테이블을 참조한다.
  • 외래 키를 객체 참조와 비슷하게 사용할 수 있어서 객체지향 개발자들이 선호한다.
  • 주 테이블이 외래 키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.

단방향

Member Locker
id id
Locker locker name
username  
MEMBER LOCKER
MEMBER_ID(PK) LOCKER_ID(PK)
LOCKER_ID(FK, UNI) NAME
USERNAME  
@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;
}

양방향

Member Locker
id id
Locker locker name
username Member member
MEMBER LOCKER
MEMBER_ID(PK) LOCKER_ID(PK)
LOCKER_ID(FK, UNI) NAME
USERNAME  
@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;
    
    @OneToOne(mappedBy = "locker")
    private Member member;
}

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

 

2) 대상 테이블에 외래 키

특징

  • 전통적인 데베 개발자들은 대상 테이블에 외래 키 두는 것을 선호한다.
  • 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.

단방향

일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않고, 이런 모양으로 매핑할 수 있는 방법도 없다.

 

양방향

Member Locker
id id
Locker locker name
username Member member
MEMBER LOCKER
MEMBER_ID(PK) LOCKER_ID(PK)
USERNAME NAME
  MEMBER_ID(FK, UNI)
@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;
}
  • 일대일 매핑에서 대상 테이블에 외래 키를 두고 싶으면 양방향으로 매핑한다.
  • 주 엔티티인 Member 엔티티 대신에 대상 엔티티인 Locker를 연관관계의 주인으로 만들어서 LOCKER 테이블의 외래 키를 관리하도록 했다.

5. 다대다

  • 관계형 데베는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
  • 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다. 회원 객체는 컬렉션을 사용해서 상품들을 참조하면 되고, 상품들도 컬렉션을 사용해서 회원들을 참조하면 된다.

1) 다대다 단방향

@Entity
public class Member{
	@Id
    @Column(name = "member_id")
    private String id;
    
    private String username;
    
    @ManyToMany
    // @JoinTable은 연결 테이블을 만드는 어노테이션이라고 할 수 있다.
    @JoinTable(name = "member_product", joinColumns = @JoinJolumn(name = "member_id"), inverseJoinColumns = @JoinColumn(name = "product_id"))
    private List<Product> products = new ArrayList<Product>();
}
@Entity
public class Product{
	@Id
    @Column(name = "product_id")
    private String id;
    
    private String name;
}

2) 다대다 양방향

@Entity
public class Member{
	@Id
    @Column(name = "member_id")
    private String id;
    
    private String username;
    
    @ManyToMany
    @JoinTable(name = "member_product", joinColumns = @JoinJolumn(name = "member_id"), inverseJoinColumns = @JoinColumn(name = "product_id"))
    private List<Product> products = new ArrayList<Product>();
    
    public void addProduct(Product product){ // 연관관계 편의 메소드
    	products.add(product);
        products.getMembers().add(this);
    }
}
@Entity
public class Product{
	@Id
    @Column(name = "product_id")
    private String id;
    
    private String name;
    
    @ManyToMany(mappedBy = "products")
    private List<Member> members;
}

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

  • @ManyToMany를 사용한 매핑을 실무에서 사용하기에는 한계가 있다. 연결 테이블에는 단순히 아이디만 담기는 것이 아니라 추가적인 컬럼이 더 필요한 경우가 많은데, 이렇게 추가하면 매핑이 힘들기 때문이다. 
  • 그래서 추가로 연결 테이블에 해당하는 엔티티를 만든다.
Member MemberProduct Product
id member id
memberProducts product name
username orderAmount  
  orderDate  
MEMBER MEMBER_PRODUCT PRODUCT
MEMBER_ID(PK) MEMBER_ID(PK, FK) PRODUCT_ID(PK)
USERNAME PRODUCT_ID(PK, FK) NAME
  ORDERAMOUNT  
  ORDERDATE  
@Entity
public class Member{
	@Id
    @Column(name = "member_id")
    private String id;
    
    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts;
}
@Entity
public class Product{
	@Id
    @Column(name = "product_id")
    private String id;
    
    private String name;
}
@Entity
@IdClass(MemberProductId.class)
public class MemberProduct{
	@Id
    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member; //MemberProductId.member와 연결
    
    @Id
    @ManyToOne
    @JoinColumn(name = "product_id")
    private Product product; //MemberProductId.product와 연결
    
    private int orderAmount;
    
    private Date orderDate;
}
public class MemberProductId implements Serializable{
	private String member; //MemberProduct.member와 연결
    private String product; //MemberProduct.product와 연결
    
    @Override
    public boolean equals(Object o){
    }
}

복합 기본 키

두 개의 키가 합쳐져서 기본 키를 구성하는 경우를 의미한다

 

식별자 클래스

  • JPA에서 복합 키를 사용하려면 별도의 식별자 클래스를 만들어야 한다.
  • Serializable을 구현해야 한다
  • equals와 hasCode 메소드를 구현해야 한다
  • 기본 생성자가 있어야 한다
  • 식별자 클래스는 public이어야 한다
  • @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다

식별 관계

부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것

 

비식별 관계(이걸 더 추천)

부모 테이블의 기본 키를 단순히 외래 키로만 사용하는 것

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

복합키를 사용하지 않고 데베에서 자동으로 생성해주는 대리 키를 Long값으로 사용한다

  • 간편하다
  • 영구히 쓸 수 있다
  • 비즈니스에 의존하지 않는다
  • ORM매핑 시에 복합 키를 만들지 않아도 되므로 간단히 매핑을 완성할 수 있다.
MEMBER ORDERS PRODUCT
MEMBER_ID(PK) ORDER_ID(PK) PRODUCT_ID(PK)
USERNAME MEMBER_ID(FK) NAME
  PRODUCT_ID(FK)  
  ORDERAMOUNT  
  ORDERDATE  
@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;
    
    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts;
}
@Entity
public class Product{
	@Id
    @Column(name = "product_id")
    private String id;
    
    private String name;
}