-> 블로그 이전

[JPA] 식별 관계 & 복합 키

2022. 7. 5. 16:50Language`/JPA

DB에서 두 테이블간에 관계를 나타낼때는 총 2가지로 분류할 수 있다

1. 상대방의 PK를 자신의 PK이자 FK로 사용 = 식별관계

2. 상대방의 PK를 자신의 FK로 사용 = 비식별관계

  • 필수적 비식별 관계(Mandatory) : FK에 NULL 허용 X
  • 선택적 비식별 관계(Optional) : FK에 NULL을 허용 O

 

비식별 관계 (복합키)

현재 PARENT Table은 PK가 "복합키"로 구성되어있는 상태이고 CHILD와 1:N 관계이다

따라서 CHILD는 PARENT의 PK : 복합키를 자신의 FK로 활용함으로써 둘의 관계는 비식별 관계이다

@Entity
@Table(name = "parent")
public class Parent {

    @Id
    @Column(name = "parent_id1")
    private String id1;

    @Id
    @Column(name = "parent_id2")
    private String id2;

    private String name;
}

이렇게 단순하게 @Id를 각 PK에 붙여주면 실행 시점에 "매핑 예외"가 발생하게 된다

따라서 JPA에서 식별자(@Id)를 둘 이상 사용하기 위해서는 "별도의 식별자 클래스"가 필요하다

 

JPA는 영속성 컨텍스트에서 Entity를 관리할 때 "@Id"를 통해서 관리를 한다. 그리고 각 Entity를 구별하기 위해서 {equals() & hashCode()}를 사용한다

 

일반적으로 식별자가 하나면 자바 기본 타입을 사용하기 떄문에 {equals() & hashCode()}를 그냥 있는 그대로 사용하면 되지만 식별자가 둘 이상이 되면 그에 따른 별도의 식별자 클래스를 작성해줘야 하고, 여기서 {equals() & hashCode()}를 재정의해줘야 한다

>> JPA에서는 복합키를 지원하기 위해서 @IdClass & @EmbeddedId를 제공한다

  • @IdClass는 RDB에 가까운 방법이고 @EmbeddedId는 객체 지향에 가까운 방법이다

 

비식별 : @IdClass

식별자 클래스를 정의할 떄는 다음 조건을 반드시 만족해야 한다

  1. 식별자 클래스의 속성명 == 엔티티의 식별자 속성명
  2. Serializable 인터페이스 구현
  3. equals & hashCode 재정의
  4. 기본 생성자 필요
  5. 식별자 클래스는 public

 

ParentId (복합키 처리를 위한 식별자 클래스)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class ParentId implements Serializable {
    private String id1;

    private String id2;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ParentId parentId = (ParentId) o;
        return Objects.equals(getId1(), parentId.getId1()) && Objects.equals(getId2(), parentId.getId2());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId1(), getId2());
    }
}

Parent (1 - 복합키 사용)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "parent")
@IdClass(ParentId.class)
public class Parent {
    @Id
    @Column(name = "parent_id1", length = 45)
    private String id1;

    @Id
    @Column(name = "parent_id2", length = 45)
    private String id2;

    @Column(length = 45)
    private String name;
}
  • ParentId라는 복합키 클래스를 @IdClass(ParentId.class)로 사용하고 이제 각 PK에 @Id를 붙여도 매핑 에러가 발생하지 않고 제대로 매핑이 된다

Child (N)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "child")
public class Child {
    @Id
    @Column(name = "child_id", length = 45)
    private String id;

    @Column(length = 45)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({
            @JoinColumn(name = "parent_id1"),
            @JoinColumn(name = "parent_id2")
    })
    private Parent parent;
}

 

@IdClass를 활용한 저장 & 조회

ParentId id = new ParentId("parentId1", "parentId2");
Parent parent = new Parent(id.getId1(), id.getId2(), "Parent");
Child child = new Child("childId", "Child", parent);

em.persist(parent);
em.persist(child);

 

@IdClass는 어쨌든 복합키 각각에 대해서 PK로 매핑을 해주는 복합키 설정 클래스이므로 실제로 복합키를 insert할때도 PK에 value를 각각 넣어줘야 한다

ParentId id = new ParentId("parentId1", "parentId2");
Parent findParent = em.find(Parent.class, id);
System.out.println(findParent);

조회할때도 마찬가지로 PK가 복합키로 구성되어 있기 때문에 "복합키"를 활용해서 em.find로 조회해야 한다

Hibernate: 
    select
        parent0_.parent_id1 as parent_i1_1_0_,
        parent0_.parent_id2 as parent_i2_1_0_,
        parent0_.name as name3_1_0_ 
    from
        parent parent0_ 
    where
        parent0_.parent_id1=? 
        and parent0_.parent_id2=?
        
Parent{id1='parentId1', id2='parentId2', name='Parent'}

 

 

비식별 : @EmbeddedId

@EmbeddedId를 위한 식별자 클래스를 정의할 때는 다음 조건을 만족해야 한다 (@IdClass를 사용할때의 조건과 거의 동일하다)

  1. @Embeddable 애노테이션 붙여주기
  2. Serializable 인터페이스 구현
  3. equals() & hashCode() 재정의
  4. 기본 생성자 필요
  5. 식별자 클래스는 public

 

ParentId (복합키 처리를 위한 식별자 클래스)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class ParentId implements Serializable {
    @Column(name = "parent_id1", length = 45)
    private String id1;

    @Column(name = "parent_id2", length = 45)
    private String id2;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ParentId parentId = (ParentId) o;
        return Objects.equals(getId1(), parentId.getId1()) && Objects.equals(getId2(), parentId.getId2());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId1(), getId2());
    }
}
  • Parent에서 ParentId라는 복합키를 사용할 때 매핑을 ParentId로 하기 때문에 @Embeddable 애노테이션을 붙여준다

Parent (1 - 복합키 사용)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "parent")
public class Parent {
    @EmbeddedId
    private ParentId id;

    @Column(length = 45)
    private String name;
}
  • ParentId를 그대로 받은다음에 @EmbeddedId 애노테이션을 설정해준다
  • 그러면 ParentId의 필드인 [String id1, String id2]가 PK로 설정이 된다

Child (N)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "child")
public class Child {
    @Id
    @Column(name = "child_id", length = 45)
    private String childId;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({
            @JoinColumn(name = "parent_id1"),
            @JoinColumn(name = "parent_id2")
    })
    private Parent parent;
}

 

@EmbeddedId를 활용한 저장 & 조회

조회의 경우 @IdClass와 마찬가지로 관리할 때 복합키로 관리되기 때문에 em.find로 조회할때 복합키를 넣어줘야 한다

하지만 persist의 경우 @Embeddable 타입의 클래스를 그대로 식별자로써 사용하기 때문에 @Embeddable 타입의 클래스를 id로 넣어줘야 한다

ParentId id = new ParentId("parentId1", "parentId2");
Parent parent = new Parent(id, "Parent");
Child child = new Child("childId", "Child", parent);

em.persist(parent);
em.persist(child);

이전에 @IdClass를 통한 저장은 PK가 각각 필드로 구성되어 있고 이를 @IdClass를 통해서 매핑하기 때문에 각 PK에 value를 넣어줘야 한다

 

하지만 @EmbeddeId의 경우 @Embeddable 자체를 복합키로써 "객체화"시켜서 사용하기 때문에 각 PK에 value를 넣는게 아니라 @Embeddable 자체를 PK로 넣어주면 된다

ParentId id = new ParentId("parentId1", "parentId2");
Parent findParent = em.find(Parent.class, id);
System.out.println(findParent);

조회는 @IdClass와 마찬가지로 복합키 자체로 조회해야 한다

Hibernate: 
    select
        parent0_.parent_id1 as parent_i1_1_0_,
        parent0_.parent_id2 as parent_i2_1_0_,
        parent0_.name as name3_1_0_ 
    from
        parent parent0_ 
    where
        parent0_.parent_id1=? 
        and parent0_.parent_id2=?
        
Parent{id=ParentId{id1='parentId1', id2='parentId2'}, name='Parent'}
  • 출력 양식은 @IdClass와 약간 다른것을 확인할 수 있다
  • @IdClass는 PK각각이 필드로 구현되어 있고, @EmbeddedId는 @Embeddable 자체를 하나의 PK로 보기 때문에 양식이 서로 다르다

 

복합키에는 @GeneratedValue를 사용할 수 없고, 복합 키를 구성하는 여러 Column 중 하나에도 사용할 수 없다

 

식별 관계 (복합키)

이처럼 부모 → 자식 → 손자로 내려가면서 각각 상위의 PK를 받아서 자신의 PK로 활용한 식별 관계를 나타내었다

parent : parent_id
child : {child_id, parent_id}
grandchild : {grandchild_id, child_id, parent_id}

 

식별 : @IdClass

Parent (1)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "parent")
public class Parent {
    @Id
    @Column(name = "parent_id", length = 45)
    private String id;

    @Column(length = 45)
    private String name;
}

ChildId (Child의 복합키를 위한 클래스)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChildId implements Serializable {
    private String id; // Child.id에 매핑

    private Parent parent; // Child.parent에 매핑

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ChildId childId = (ChildId) o;
        return Objects.equals(getId(), childId.getId()) && Objects.equals(getParent(), childId.getParent());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId(), getParent());
    }
}
  • Child 클래스에서는 child_id와 parent_id(FK)를 묶어서 PK로 활용하고자 한다. 따라서 이 ChildId라는 식별자 클래스에서 각 필드에 맞게 매핑시켜주면 된다

Child (N - 복합키 사용[child_id(PK), parent_id(PK/FK)]

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "child")
@IdClass(ChildId.class)
public class Child {
    @Id
    @Column(name = "child_id", length = 45)
    private String id;

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Parent parent;

    @Column(length = 45)
    private String name;
}

GrandChildId (GrandChild의 복합키를 위한 클래스)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class GrandChildId implements Serializable {
    private String id; // GrandChild.id에 매핑

    private Child child; // GrandChild.child에 매핑

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        GrandChildId that = (GrandChildId) o;
        return Objects.equals(getId(), that.getId()) && Objects.equals(getChild(), that.getChild());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId(), getChild());
    }
}

GrandChild (N - 복합키 사용[grandchild_id(PK), child_id(PK/FK), parent_id(PK/FK)])

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "grandchild")
@IdClass(GrandChildId.class)
public class GrandChild {
    @Id
    @Column(name = "grandchild_id", length = 45)
    private String id;

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({
            @JoinColumn(name = "child_id"),
            @JoinColumn(name = "parent_id")
    })
    private Child child;

    @Column(length = 45)
    private String name;
}
  • 여기서 Child라는 필드는 사실 [child_id(PK), parent_id(PK/FK)] 복합키로 이루어져 있기 때문에 @JoinColumns를 사용해서 둘다 매핑시켜줘야 한다

 

Parent parent = new Parent("parentId", "Parent");
Child child = new Child("childId", parent, "Child");
GrandChild grandChild = new GrandChild("grandchildId", child, "GrandChild");

em.persist(parent);
em.persist(child);
em.persist(grandChild);

 

 

식별 : @EmbeddedId

@EmbeddedId로 식별관계를 구성할 때는 "@MapsId"를 사용해야 한다

 

Parent (1)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "parent")
public class Parent {
    @Id
    @Column(name = "parent_id", length = 45)
    private String id;

    @Column(length = 45)
    private String name;
}

ChildId (Child의 복합키를 위한 클래스)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class ChildId implements Serializable {
    @Column(name = "child_id", length = 45)
    private String id;

    private String parentId; // @MapsId("parentId")로 Child에서 매핑

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ChildId childId = (ChildId) o;
        return Objects.equals(getId(), childId.getId()) && Objects.equals(getParentId(), childId.getParentId());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId(), getParentId());
    }
}

Child (N - 복합키 사용[child_id(PK), parent_id(PK/FK)])

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "child")
public class Child {
    @EmbeddedId
    private ChildId id;

    @MapsId("parentId")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id", columnDefinition = "varchar(45)")
    private Parent parent;

    @Column(length = 45)
    private String name;
}
  • @MapsId("parentId")를 통해서 ChildId의 'String parentId'에 매핑시켜준다

GrandChildId (GrandChild의 복합키를 위한 클래스)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class GrandChildId implements Serializable {
    @Column(name = "grandchild_id", length = 45)
    private String id;

    @Embedded
    private ChildId childId; // @MapsId("childId")로 GrandChild에서 매핑

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        GrandChildId that = (GrandChildId) o;
        return Objects.equals(getId(), that.getId()) && Objects.equals(getChildId(), that.getChildId());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId(), getChildId());
    }
}

GrandChild (N - 복합키 사용[grandchild_id(PK), child_id(PK/FK), parent_id(PK/FK)])

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "grandchild")
public class GrandChild {
    @EmbeddedId
    private GrandChildId id;

    @MapsId("childId")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({
            @JoinColumn(name = "child_id", columnDefinition = "varchar(45)"),
            @JoinColumn(name = "parent_id", columnDefinition = "varchar(45)")
    })
    private Child child;

    @Column(length = 45)
    private String name;
}
  • @MapsId("childId")를 통해서 GrandChildId의 'Child childId'에 매핑시킨다

 

 

일대일 식별 관계

 

일대일 식별관계는 자식 테이블의 PK는 부모 테이블의 PK를 사용하기 때문에 부모 테이블의 PK가 복합키가 아니라면 자식 테이블의 PK도 복합키로 구성하지 않아도 된다

 

Board Entity

@Entity
@Table(name = "board")
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "board_id")
    private Long id;

    private String title;

    @OneToOne(mappedBy = "board")
    private BoardDetail boardDetail;
}

BoardDetail Entity

@Entity
@Table(name = "board_detail")
public class BoardDetail {
    @Id
    private Long boardId;

    private String content;

    @MapsId
    @OneToOne
    @JoinColumn(name = "board_id")
    private Board board;
}

BoardDetail처럼 단순히 식별자가 컬럼 하나로 구성되어 있다면 @MapsId로 매핑해주면 된다

  • 위의 @MapsId는 BoardDetail.boardId와 매핑된다