Back End/Spring Data JPA

[Spring JPA] @OneToMany 에서 @MapsId 사용하기

DevPing9_ 2022. 1. 24. 22:44

>> @OneToMany에서 @MapsId 올바르게 사용하는법 보러가기

 

>> @OneToOne에서 @MapsId 로 컬럼갯수 1개 줄여보기

 

 


@MapsId  에 관하여...

 

@MapsId는 FK를 PK로 지정할 때 사용하는 어노테이션이다.

 

이 말은 즉슨, @MapsId를 통해 엔티티의 PK를 FK로 매핑한다면, 데이터베이스 컬럼하나를 줄일 수 있다는 이야기이다...!

 

@MapsId를 검색하다가 @OneToMany 와 같이 검색어가 자동완성되기에, 이거 되는거구나!! 대박인걸!! 하고 웹상에 존재하는 예시코드를 따라해보았다.

 

결론부터 말하면, @OneToMany에서의 @MapsId 사용은 PK가 복합키일때만 사용가능하며, 위의 데이터베이스 컬럼하나를 줄일 수 있는 효과는 없다.

 

@OneToOne에서는 @MapsId 사용으로 @MapsId 를 사용하지 않았을 때, 디폴트로 생성되는 조인컬럼 필드를 하나 줄일 수 있다.

 

주의할점은, @OneToMany에서 @MapsId를 사용할때 PK가 복합키가 아니더라도 컴파일오류 및 DDL 오류는 발생하지 않는다. 

오히려 멋지게 잘 동작한다. 다만 데이터 삽입시 PK를 지정하라는 오류 문구를 내뱉는다.

 

 


@MapsId 를 사용한 올바르지 않은 코드 (삽질)

 

바보같은 나는 또 삽질을 하였다.

 

이러한 상상을 했다.

 

DDD(Domain-Driven-Development) 느낌으로 설계를 하고싶어..!!!!

그러므로 상위엔티티에서 하위엔티티들을 관리할꺼야...!!!

하위엔티티 테이블 필드를 최대한 줄이고 싶어...!!!

하위엔티티의 단일 PK를 FK로 지정하면 디폴트로 생성되던 조인칼럼 하나 줄일 수 있겠구나!!!

=> Hibernate가 ORM인 이상 진짜 진짜 진짜 말도 안되는 소리다. 

=> 물론 PK를 복합키로 사용하고 있다면 말이 되긴 한다.

 

DB에서 PK란 Unique한 값이며 Index로 지정되는 값이다. 이로인해 PK로 조회시 검색성능이 높아진다.

 

@OneToMany의 상황에서 단일 PK를 FK로 지정하는 것 자체가 모순이다.

하나의 부모를 가르키는 여러개의 자식이 있을건데 전혀 Unique하지 않다. 

 

아래는 이 사실을 깨닫기 전에 필자가 작성한 코드이다. (개인기록용) 

 

 

@OneToMany에서 단일 PK를  @MapsId로 매핑해보자!

 

# Item 엔티티 (Tags 를 OneToMany로 매핑중)

@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn @Getter
@AllArgsConstructor @NoArgsConstructor
@ToString(callSuper = true)
@Entity
public class Item extends BaseEntityWithId {
    protected Long itemId; //FolderId & BookmarkId
    protected String name;
    protected Long parentId;
    protected BigDecimal visitCount;
    protected Long userId;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "item", cascade = CascadeType.ALL)
    protected List<Tag> tags;

    public void setTags(List<Tag> tags){
        this.tags=tags;
    }
}

 

# Tag 엔티티 (Item을 @ManyToOne으로 매핑중)

@Entity
@AllArgsConstructor
@Builder
@NoArgsConstructor
@Getter
@ToString
public class Tag {
    @Id
    @Column(name="item_id")
    private Long itemId;
    private String tag;

    @MapsId("item_id")
    @ManyToOne(fetch = FetchType.LAZY,optional = false)
    @JoinColumn(name= "item_id")
    @ToString.Exclude
    Item item;

    public void setItem(Item item){
        this.item = item;
    }
}

 

#  DDL 실행결과 

 

 

원하는대로 tag 테이블에서는 FK로 생성되는 다른 컬럼이 없다..!

 

 

하지만 저렇게 테이블 매핑은 되지만, Item.setTags() 와 Tags.setItem()으로 연관관계를 묶은 후 Insert를 하면 오류가 발생한다.

 

오류의 표면적이유는 " PK를 Manual 하게 지정하셔야 되는데 하지 않으셨습니다. " 인데... 

 

생각해보면 당연하게도, Tag의 ItemId 를 PK로 지정했고, 그로 인해 PK는 Unique해야하는데,

일대다 관계기 때문에 Item의 PK를 Tag의 ItemId로 지정한다면 PK는 Unique해지지 못하므로

Hibernate가 @MapsId의 PK 주입을 허용하지 않는 것이다.

 

그럼 왜 오류문구가 " @MapsId를 잘못사용하셨습니다 " 가 아닐까?

실제 DB 에서는 PK 없이 테이블을 구성할 수 있다고 한다.

그리하여 Hibernate는 ORM 의 기능인 테이블에 매핑을 하기위한 @MapsId 는 동작을 허용하지만,

Insert 때 @MapsId로 연결된 컬럼을 자동삽입하는 기능은 모순되기에 동작을 허용하지 않는 것이다.

그리고 그 동작이 허용되지않으니, @GeneratedValue 가 아닌 PK는 값을 가지지 않게되고

그로 인해 PK를 Manual하게 지정해달라고 오류메세지를 띄우게 되는 것이다.

 

Hibernate의 움직임의 기초인 Proxy는 PK를 기반으로 움직이기 때문에

DB에서 PK 없이 테이블을 구성했을 때 매핑할 수 있는 방법 또한 아래와 같이 임의로 PK를 지정하는 방법이다. 

 

 

PK없이 테이블을 구성했을 때, DB에서도 삭제시에 여러가지 아이템이 지워질 수 있다는 오류문구를 띄우기도 한다.

결국은 칼럼1개를 절약하기 위해 PK 없이 테이블을 구성하는 것은 구성원들의 실수로 데이터가 날라갈 수도 있는 상황을 만들 수 있기에 지양하는 것이 옳다.

 

칼럼 1개의 Trade-off 비용은 JPA에서 적용의 까다로움 + 유지보수 + 데이터손실의 위험이다.

 


OneToMany 에서 @MapsId 를 올바르게 사용하기

 

복합키를 만들어서 매핑한다.

복합 PK의 검색성능에 대해서는 아직 공부가 덜되서 확답을 할 수 없다.

 

 

위와 같이 테이블구조를 만들면 PK가 중복되지 않으므로 @MapsId의 필드자동주입 기능도 활성화 된다. 

근데 언제 이런 구조를 가져가야하는지는 경험도 없고 지식도 없다... (추후 공부 필요)

 

 

# Tag 엔티티

@Entity @AllArgsConstructor
@Builder @NoArgsConstructor
@Getter @ToString
@IdClass(CompositeId.class) // 복합키설정
public class Tag {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Id
    @Column(name="item_id")
    private Long itemId;

    public class CompositeId implements Serializable{ // 복합키는 Serializable 
        private Long id;
        private Long itemId;
    }

    private String tag;

    @MapsId("item_id")
    @ManyToOne(fetch = FetchType.LAZY,optional = false)
    @JoinColumn(name= "item_id")
    @ToString.Exclude
    Item item;

}

 

 

728x90