foreign key constraint fails 오류

이정빈·2024년 4월 27일
0

트러블슈팅

목록 보기
1/7
post-thumbnail

오류 상황

스크랩 저장 기능을 테스트 하던 중 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]

오류 원인 코드

  • [ScrapServiceImplTest.java]
    @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);
        }
    }
  • [ScrapServiceImpl]
    @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();
        }
    }
    
  • [Scrap.java]
    @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를 영속화한 뒤 테스트를 진행하도록 코드를 수정한다.


실제 해결 방법

1. user와 notice 객체 영속화

user와 notice를 영속화하기 위해 @Autowired EntityManager em; 를 추가해 엔티티매니저를 생성하고, em.persist(user);em.persist(notice);를 통해 user와 notice를 영속성 컨텍스트에 추가했다.

    @Autowired
    EntityManager em;

    em.persist(user);
	em.persist(notice);

2. EntityExistsException 오류 해결

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번 경우에 해당되었다.

따라서 아래의 과정으로 해결했다.

1) @builder를 user클래스에 추가

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;
}

2) ID값을 직접 부여하는 부분 제거

스크랩 테스트 코드에서 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);
    }

수정 후 테스트 코드는 아래와 같다.

  • [ScrapServiceImplTest.java]
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);
    }
}

이후 테스트를 다시 돌려보았더니 깔끔하게 성공한 것을 볼 수 있다.

참고:

profile
사용자의 입장에서 생각하며 문제를 해결하는 백엔드 개발자입니다✍

0개의 댓글