개요

  • org.springframework.dao.DataIntegrityViolationException
  • javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session

위 에러를 만나서, 원인을 파악하고 해결한다.

 

 

원인

디비에 식별자가 있는 엔티티가 있음에도 불구하고 동일한 식별자로 한번 더 저장하려고 생긴 문제였다. 아래와 같이 1:1 관계로 맺어진 엔티티가 있었다. Post : PostDetail = 1 : 1 관계다.

 

 

코드로 표현하면 아래처럼 표현할 수 있다. (최대한 간단간단하게..)

// ####### Post
@Entity
@Table(name = "post")
class Post(
	@Column(name = "contents", columnDefinition = "TEXT", nullable = true)
    var contents: String? = null
) {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null

    @NotAudited
    @OneToOne(mappedBy = "post", cascade = [CascadeType.ALL])
    var postDetail: PostDetail? = null

    fun setBy(postDetail: PostDetail) {
        this.postDetail = postDetail
        postDetail.setBy(this)
    }
}

// ####### PostDetail
@Entity
@Table(name = "post_detail")
class PostDetail(
	paramTags: List<String> = emptyList()
) {

    /**
     * 필드만 설정되어있고, 실제 테이블에 컬럼 미존재.
     */
    @Id
    var id: Long? = null

    @MapsId
    @OneToOne
    @JoinColumn(name = "post_id")
    var post: Post? = null

    fun setBy(post: Post) {
        this.post = post
    }
}

 

@MapsId 라는 애노테이션을 통해서 PostDetail 의 PK 는 id 컬럼이 아닌 post_id 컬럼을 가지게 된다. 사실 FK 면서 PK 의 형태로 Post 와 PostDetail 은 식별관계이다. (PostDetail 은 Post 없이는 만들 수 없는 상태) 위 관계에서 에러가 발생했다. 어떻게 로직을 짜면 에러가 발생할까.. 코드를 구현해보자.

 

 

에러를 강제로 발생시키는 코드

internal class PostRepositoryTest(
    private val entityManager: TestEntityManager,
    private val postRepository: PostRepository,
    private val postDetailRepository: PostDetailRepository
) {

    @Test
    @DisplayName("post 를 저장, postDetail 의 식별자가 존재함에도 불구하고 저장하려고 해서 에러 발생")
    fun saveAndPostDetailDupSaveErrorTest() {

        val post = Post("안녕하세요.")
        postRepository.saveAndFlush(post)
        entityManager.clear()

        val postDetail = PostDetail(listOf("tag1", "tag2"))
        post.setBy(postDetail)

        postRepository.saveAndFlush(post)
        entityManager.clear()

        val newPostDetail = PostDetail(listOf("tag3", "tag4"))
        post.setBy(newPostDetail)

        val exception = assertThrows<DataIntegrityViolationException> {
            postRepository.saveAndFlush(post)
        }
        entityManager.clear()

        exception.asClue {
            (it.cause is EntityExistsException) shouldBe true
            it.message shouldContain "A different object with the same identifier value was already associated with the session"
        }

        postRepository.findAll().size shouldBe 1
        postDetailRepository.findAll().size shouldBe 1
    }
}

위 코드는 에러가 발생된 것이 확인되고, 익셉션에 대한 테스트 통과가 된다.

  • Post("안녕하세요.") 라는 post 객체를 만들고 저장한다. 
  • PostDetail(listOf("tag1", "tag2")) 라는 postDetail 객체를 만들고 기존 post 와 연관관계를 맺고 저장한다. : 1회
  • PostDetail(listOf("tag3", "tag4")) 라는PostDetail 객체를 만들어 기존 post 와 연관관계를 맺고 저장한다. : 2회
  • postDetail 의 PK 로 설정된 post.id 가 기존 postDetail 테이블에 있어서 에러가 발생한다. : EntityExistException

다시 반복해서 말하면 디비에 해당 식별자로 데이터가 저장되어있음에도 불구하고 또 저장해서 발생한 문제다. 그럼 어떻게 해결을 해야하는가? 디비에 해당 식별자로 있는지 한번 조회하고, 반환된 객체를 기준으로 관계를 그대로 맺어주면 된다.

 

 

에러가 발생하지 않는 코드 : 일부만 수정

internal class PostRepositoryTest(
    private val entityManager: TestEntityManager,
    private val postRepository: PostRepository,
    private val postDetailRepository: PostDetailRepository
) {

    @Test
    @DisplayName("post 를 저장, postDetail 의 식별자가 존재하는지 확인 후 다시 저장. 에러는 발생 안함")
    fun saveAndPostDetailDupSaveTest() {

        val post = Post("안녕하세요.")
        postRepository.saveAndFlush(post)
        entityManager.clear()

        val postDetail = PostDetail(listOf("tag1", "tag2"))
        post.setBy(postDetail)
        postRepository.saveAndFlush(post)
        entityManager.clear()

        val newPostDetail = postDetailRepository.findByPostId(post.id!!) ?: PostDetail(listOf("tag3", "tag4"))
        post.setBy(newPostDetail)

        postRepository.saveAndFlush(post)
        entityManager.clear()

        postRepository.findAll().size shouldBe 1
        postDetailRepository.findAll().size shouldBe 1
    }
}

한번 디비에서 조회해서 세팅해주는 코드는 에러가 발생하지 않은 상태로 통과된다.

post.id!! 가 pk 로 설정된 postDetail 을 찾아서 다시한번 관계를 맺어준다. 디비에 없을때만 PostDetail 객체를 새롭게 만든다.

 

 

다른 해결책?

  • FK 형태로 식별관계를 만들지 않고 auto_increment 대리키를 PK 로 이용한다. 새롭게 만드는 값에 대해서는 pk 가 겹칩일 이 없었을 것이다. join 을 위한 컬럼만 겹쳤을뿐. 해결이 됬을거라 생각한다.
  • FK 로 식별관계를 이용하는 경우 코드 검증을 철저히 한다. (+테스트코드) 사실 이번에 겪은부분이 여기에 해당한다. 특정한 상태값을 기준으로 관계를 맺어주는데 그 이외의 상태값에도 엔티티간 관계가 맺어질거라 생각을 하지 않았기 때문이다. 항상 예외적인 경우를 생각해야 한다..

 

 

+ 익셉션 메시지

org.springframework.dao.DataIntegrityViolationException: 
A different object with the same identifier value was already associated with the session : 
[com.example.springbootjpabasis.domain.postdetail.model.PostDetail#1]; 
nested exception is javax.persistence.EntityExistsException: 
A different object with the same identifier value was already associated with the session : 
[com.example.springbootjpabasis.domain.postdetail.model.PostDetail#1]

 

에러가 발생한 익셉션 메시지를 보면 PostDetail#{number} 숫자가 오는 것을 볼 수 있다. 처음에 저게 그냥 별 의미없는 숫자인줄 알았는데 PK 값이다.. PostDetail 의 PK = 1 인 값이 있다는 의미고 저 값도 에러 추적하는데 일부 도움이 될거라 생각한다.

Posted by doubler
,