오늘의 에러 [ detached entity passed to persist]

ds_chanin

·

2019. 11. 14. 01:39


발생한 에러 (2019.11.13)

org.hibernate.PersistentObjectException: detached entity passed to persist

문제 발생시 도메인 상황

        @Entity
        @Getter
        @NoArgsConstructor(access = AccessLevel.PROTECTED)
        public class Champion {

            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            private Long id;

            private String riotId;
            private String key;
            @Column(unique = true)
            private String name;

            @OneToMany(mappedBy = "champion", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
            private List<Stat> stats;

            ...
        }



        @Getter
        @Entity
        @NoArgsConstructor(access = AccessLevel.PROTECTED)
        public class Stat {
            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            private Long id;

            private Integer hp;

            ...

            @ManyToOne(cascade = CascadeType.ALL)
            private LolInfo lolInfo;

            @ManyToOne(cascade = CascadeType.ALL)
            private Champion champion;

            ...
        }



        @Entity
        @Getter
        @NoArgsConstructor(access = AccessLevel.PROTECTED)
        public class LolInfo {
            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            private Long id;

            @Column(unique = true)
            private String patchNoteVersion;

            ...
        }

서비스 코드 상황

    // 챔피언 스탯 정보 저장
    public StatResDto saveStat(StatSaveDto statSaveDto, Long championId, Long lolInfoId) {
        Champion champion = championRepository.findById(championId)
                .orElseThrow(RuntimeException::new);
        LolInfo lolInfo = lolInfoRepository.findById(lolInfoId)
                .orElseThrow(RuntimeException::new);

        Stat stat = statRepository.save(statSaveDto.toEntity(lolInfo, champion));
        return StatResDto.of(stat);
    }

실패한 테스트 코드

    @Test
    public void saveStat_정상저장() {
        ChampionSaveDto championSaveDto = ChampionSaveDto.builder()
                .name("아리")
                .key("Ahri")
                .riotId("109")
                .build();
        Champion champion = championRepository.save(championSaveDto.toEntity());
        Long championId = champion.getId();
        championRepository.flush();

        LolInfoSaveDto lolInfoSaveDto = LolInfoSaveDto.builder()
                .patchNoteVersion("9.22.1")
                .build();
        LolInfo lolInfo = lolInfoRepository.save(lolInfoSaveDto.toEntity());
        Long lolInfoId = lolInfo.getId();
        lolInfoRepository.flush();

        StatSaveDto statSaveDto = StatSaveDto.builder()
                .hp(100)
                .build();
        StatResDto statResDto = lolInfoService.saveStat(statSaveDto, championId, lolInfoId);

        assertThat(statResDto.getHp()).isEqualTo(100);
    }

실패한 이유

테스트 코드에서 Stat 을 save하는 과정에서 Champion 엔티티가 이미 DB 상에 존재하여 에러가 발생했다.

Stat에 에서 Champion 에 대해 영속성을 CascadeType.ALL 로 설정을 해놓았기 때문에 Stat을 저장할때 Champion이 중복 저장되는 상황이 발생한 것이었다!

해결방법은 CascadeType.ALL 을 수정하거나 제거 하는것이다.

결국

Entity의 설계가 잘못되어 CascadeType의 설정이 ALL이 엉뚱한 위치에 있었다!

데이터를 파싱해 오는 곳에서는 Stat을 기준으로 Champion을 만들고 있었는데 반대가 되어야 했던 것이다.

그래서 서비스 코드에서 statRepositoy.save() 가 아닌 championRepository.save() 하는 방식으로 문제를 해결했다.

그래서 서비스코드가 다음과 같이 수정이 되었고

    @Transactional
    public StatResDto saveStat(StatSaveDto statSaveDto, Long championId, Long lolInfoId) {
        Champion champion = championRepository.findById(championId)
                .orElseThrow(RuntimeException::new);
        LolInfo lolInfo = lolInfoRepository.findById(lolInfoId)
                .orElseThrow(RuntimeException::new);

        champion.getStats().add(0, statSaveDto.toEntity(lolInfo, champion));
        return StatResDto.of(champion.getStats().get(0));
    }

도메인은 다음과 같이 수정되었다.

	@Entity
	@Getter
	@NoArgsConstructor(access = AccessLevel.PROTECTED)
	public class Champion {
	
	    @Id
	    @GeneratedValue(strategy = GenerationType.IDENTITY)
	    private Long id;
	
	    @Setter
	    private String riotId;
	    @Setter
	    private String key;
	    @Setter
	    private String name;
	
	    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
	    @JoinColumn(name = "stat_id")
	    private List<Stat> stats = new ArrayList<>();
	    
	    ...
	}
	
	
	@Getter
	@Entity
	@NoArgsConstructor(access = AccessLevel.PROTECTED)
	public class Stat {
	    @Id
	    @GeneratedValue(strategy = GenerationType.IDENTITY)
	    private Long id;
	
	    private Integer hp;
	
	    ...
	
	    @ManyToOne(fetch = FetchType.LAZY)
	    private LolInfo lolInfo;
	
	    @Builder
	    public Stat(Integer hp, ...) {
	        this.hp = hp;
	        ...
	    }
	}

맺으며

CascadeType.ALL 을 사용할때 부모객체가 생성될때 자식객체가 생성되도록 사용해야하고 자식객체가 부모객체를 같이 생성하게 사용하면 안될 것 같다.

써야한다면 부모와 자식의 관계가 1:1이라면 사용해도 되지 않을까?