2022-06-05 [error] javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session
ErrorMarking 2022. 6. 6. 15:03개요
- 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 인 값이 있다는 의미고 저 값도 에러 추적하는데 일부 도움이 될거라 생각한다.