ERROR : More than one row with the given identifier was found

이동욱·2023년 10월 10일
post-thumbnail

토이프로젝트를 진행하던 도중 User와 Fund라는 엔티티에서 사용되는 이미지, 비디오, 음악들을 하나의 Content라는 엔티티를 통해 관리하려고 아래와 같이 엔티티들을 만들었다.

@Entity
public class User {

    @Id
    @Column(name = "USER_ID")
    private String id;

    @OneToOne
    @JoinColumn(name = "USER_CONTENT_ID")
    private Content content;
    
    ...
}
@Entity
public class Fund {

    @Id @Column(name = "FUND_ID")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int fundNo;

    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;

    @OneToMany(mappedBy = "fund")
    private List<Content> imgList;

    @OneToMany(mappedBy = "fund")
    private List<Content> videoList;

    @OneToOne(mappedBy = "fund")
    private Content music;

    ...
}
@Entity
public class Content {

    @Id @GeneratedValue(strategy = GenerationType.UUID)
    @Column(name = "CONTENT_ID")
    private UUID id;

    @OneToOne(mappedBy = "content")
    private User user;

    @ManyToOne
    @JoinColumn(name = "CONTENT_FUND_ID")
    private Fund fund;
}

이후 JPA의 EntityManager를 통해 Fund 엔티티를 불러오다 "More than one row with the given identifier was found" 에러가 발생하였다.

원인

Content엔티티에는 하나의 Fund만 연관관계로 맺어져있다 하지만 Fund에서는 3개의 Content 필드를 OneToMany로 Mapping하였기에 실제 DB에는 여러개의 같은 Fund를 외래키로 가지고있는 Content 객체들이 존재하였다.
Fund를 불러오는 과정에서 연관관계인 Content도 불러오게되는데 이 과정에서 하나의 Fund에 대한 여러개의 Content가 조회되어 위와 같은 에러가 발생하였다.

나의 원래 의도는 프로젝트에서 사용되는 이미지, 동영상, 음악들을 하나의 Content 엔티티로 관리하는것이였다. 하지만 위 코드처럼 작성한다면 Content가 이미지인지 동영상인지 혹은 음악인지 판단하기 어렵고 Content 엔티티가 Fund의 어떤 필드에 속하는지 매핑하는것이 불가능하다.

해결

위 문제를 해결하기 위해 JPA에서 객체지향의 상속관계를 가능하게 해주는 @Inheritance를 사용하였다.
Content를 Id만 속성으로 가지는 abstract class로 변경한 뒤 Image, Video, Music 엔티티를 생성한 후 각각의 엔티티에 Fund와의 연관관계를 설정하였다.
이렇게 상속관계로 구현하여 각각 Image, Video, Music에 대한 Repository를 생성하는것이 아닌 Content에 대한 Repository만 생성하여도 된다는 장점이 생겼다.

@Entity
@Getter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Content {

    @Id
    @Column(name = "CONTENT_ID")
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
}
@Entity
@DiscriminatorValue("I")
public class Image extends Content{

    @ManyToOne
    @JoinColumn(name = "IMAGE_FUND_ID")
    private Fund fund;

    @OneToOne
    @JoinColumn(name = "IMAGE_USER_ID")
    private User user;
	...
}
@Entity
@DiscriminatorValue("M")
public class Music extends Content{

    @OneToOne
    @JoinColumn(name = "MUSIC_FUND_ID")
    private Fund fund;
	...
}
@Entity
@DiscriminatorValue("V")
public class Video extends Content{
    @ManyToOne
    @JoinColumn(name = "VIDEO_FUND_ID")
    private Fund fund;
	...
}
@Entity
public class Fund {

    @Id @Column(name = "FUND_ID")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int fundNo;

    @OneToMany(mappedBy = "fund")
    private List<Image> imgList = new ArrayList<>();

    @OneToMany(mappedBy = "fund")
    private List<Video> videoList = new ArrayList<>();

    @OneToOne(mappedBy = "fund")
    private Music music;
	...
}

JPA는 상속관계 구현 시 JOINED, SINGLE_TABLE, TABLE_PER_CLASS 전략을 사용하며 내가 설계한 엔티티는 데이터들을 많이 가지지 않기에 SINGLE_TABLE 전략을 사용하였다. 각각의 전략에 따른 테이블 구조는 아래와 같다.

JOINED

  • 상위 클래스의 테이블을 생성하고 해당 테이블의 pk를 외래키로 가지는 자식 엔티티의 테이블들을 생성하여 상속관계를 구현한다
  • DTYPE을 통해 어떤 하위 테이블의 엔티티인지 구분한다

SINGLE_TABLE

  • 하나의 테이블안에서 상위 클래스와 하위 클래스의 필드를 모두 표현한다.
  • 해당 컬럼이 어떤 클래스의 데이터인지 구분하기 위해 DTYPE이 필수로 필요하다.

TABLE_PER_CLASS

  • 상위 클래스의 데이터를 포함한 하위 클래스의 테이블들을 각각 생성하는 방법이다.
profile
Backend Developer

0개의 댓글