스크랩 저장 기능을 테스트 하던 중 Cannot add or update a child row: a foreign key constraint fails
오류가 발생하였다.
사진 코드의 내용은 아래와 같다.
org.springframework.dao.DataIntegrityViolationException: could not execute statement [Cannot add or update a child row: a foreign key constraint fails (notify.scrap, CONSTRAINT FKj4mug3yrv9g9k6ucxc19ktsbj FOREIGN KEY (notice_id) REFERENCES notice (notice_id))] [insert into scrap (notice_id,user_id) values (?,?)]; SQL [insert into scrap (notice_id,user_id) values (?,?)]; constraint [null]
@Transactional
@SpringBootTest
class ScrapServiceImplTest {
@Autowired
ScrapService scrapService;
@Autowired
ScrapRepository scrapRepository;
@Test
void doScrap() {
User user = new User(1L,"구글아이디", "닉네임", "이메일", "bus");
Notice notice = Notice.builder()
.noticeId(1L)
.noticeTitle("공지 제목")
.noticeType(NoticeType.COM)
.noticeUrl("공지 url")
.build();
long l = scrapService.doScrap(user, notice);
Assertions.assertThat(l).isEqualTo(1L);
}
}
@Service
public class ScrapServiceImpl implements ScrapService{
@Autowired
private ScrapRepository scrapRepository;
@Override
public long doScrap(User user, Notice notice) {
Scrap scrap = new Scrap(user, notice);
Scrap savedScrap = scrapRepository.save(scrap);
return savedScrap.getScrapId();
}
}
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Scrap {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long scrapId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "userId")
@NotNull
private User user;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "noticeId")
@NotNull
private Notice notice;
public Scrap(User user, Notice notice){
this.user = user;
this.notice = notice;
}
}
현재 문제가 외부키 제약 조건을 만족하지 못한 것임으로 notice와 user를 미리 저장 후 그 값을 가져와야하는데, 해당 테스트에서는 notice와 user를 생성만 하고 DB에 저장하거나 영속성 컨텍스트에 영속화하는 작업이 없다. noticeRepository 코드를 작성하는 것은 나의 업무가 아니어서 내가 작성하고 pull request 시 충돌이 발생할 수 있다.
따라서 notice 저장 기능을 구현하지 않고 테스트를 해보고 싶었다.
entityManager를 생성하여 user와 notice를 영속화한 뒤 테스트를 진행하도록 코드를 수정한다.
user와 notice를 영속화하기 위해 @Autowired EntityManager em;
를 추가해 엔티티매니저를 생성하고, em.persist(user);
와 em.persist(notice);
를 통해 user와 notice를 영속성 컨텍스트에 추가했다.
@Autowired
EntityManager em;
em.persist(user);
em.persist(notice);
user와 notice를 영속성 컨텍스트에 추가한 뒤 테스트를 돌렸더니 또 다른 오류가 발생했다.
jakarta.persistence.EntityExistsException: detached entity passed to persist: com.example.notifyserver.user.domain.User
찾아보니 위와 같은 오류는 2가지 이유에서 발생했다.
1) @CASCADE
옵션으로 인해 중복으로 영속화 되는 경우
2) 엔티티 클래스에 @Id
를 부여한 필드에 @GeneratedValue
를 작성하여 AUTO, SEQUENCE, IDENTITY
전략 등 데이터베이스에게 key 값을 자동 생성하도록 하는 전략을 선택하였는데 엔티티 객체 생성 시 Id에 해당하는 필드에 직접 값을 입력한 경우
나는 @CASCADE
옵션을 주지 않아서 2번 경우에 해당되었다.
따라서 아래의 과정으로 해결했다.
ID가 없이도 클래스를 만들 수 있도록 @builder를 user클래스에 추가했다.
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder // 추가
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
@NotNull
private String googleId;
@NotNull
private String nickName;
@NotNull
private String email;
@NotNull
private String userMajor;
}
스크랩 테스트 코드에서 notice 클래스와 user 클래스의 ID값을 직접 부여하는 부분을 제거했다.
@Test
void doScrap() {
// 제거 User user = new User(1L,"구글아이디", "닉네임", "이메일", "bus");
User user = User.builder()
.googleId("구글아이디")
.nickName("닉네임")
.email("이메일")
.userMajor("bus")
.build();
Notice notice = Notice.builder()
// 제거 .noticeId(1L)
.noticeTitle("공지 제목")
.noticeType(NoticeType.COM)
.noticeUrl("공지 url")
.build();
em.persist(user);
em.persist(notice);
long l = scrapService.doScrap(user, notice);
Assertions.assertThat(l).isEqualTo(1L);
}
수정 후 테스트 코드는 아래와 같다.
package com.example.notifyserver.scrap.service;
import com.example.notifyserver.common.domain.Notice;
import com.example.notifyserver.common.domain.NoticeType;
import com.example.notifyserver.scrap.domain.Scrap;
import com.example.notifyserver.scrap.repository.ScrapRepository;
import com.example.notifyserver.user.domain.User;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Transactional
@SpringBootTest
class ScrapServiceImplTest {
@Autowired
ScrapService scrapService;
@Autowired
ScrapRepository scrapRepository;
@Autowired
EntityManager em;
@Test
void doScrap() {
User user = User.builder()
.googleId("구글아이디")
.nickName("닉네임")
.email("이메일")
.userMajor("bus")
.build();
Notice notice = Notice.builder()
.noticeTitle("공지 제목")
.noticeType(NoticeType.COM)
.noticeUrl("공지 url")
.build();
em.persist(user);
em.persist(notice);
long findId = scrapService.doScrap(user, notice);
Scrap scrappedNotice = scrapRepository.findById(findId).orElse(null);
Assertions.assertThat(scrappedNotice.getScrapId()).isEqualTo(findId);
}
}
이후 테스트를 다시 돌려보았더니 깔끔하게 성공한 것을 볼 수 있다.
참고: