[JPA] Kotlin + JPA ID 이슈

SeungHyuk Shin·2022년 11월 4일
0
post-thumbnail

실무에서 코틀린 + 스프링을 처음 사용하면서 겪은 이슈 아닌 이슈이다.

흔히 코틀린 JPA ENTITY 설정 시 다음과 같이 작성한다.

class Parent(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    override val id: Long = -1,
    ...
) : BaseEntity(id)

보면 특이한 점이 있을 것이다. 바로 ID가 기본 값으로 -1로 적용 되어 있다는 것이다. 겪은 이슈는 단일 엔티티를 저장할때는 해당 부분이 오류가 나지 않는데, 자식 관계에 엮이게 되면 오류가 나게 된다는 것이다.

class Parent(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    override val id: Long = -1,
    ...
    
    @OneToMany
	@JoinColumn(name = "child_id")
    val child: Child
) : BaseEntity(id)

class Child(
	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    override val id: Long = -1,
)

우선 JPARepository의 구현체의 save 함수를 살펴보면

//SimpleJpaRepository.java
    @Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null.");
        if (this.entityInformation.isNew(entity)) {
            this.em.persist(entity);
            return entity;
        } else {
            return this.em.merge(entity);
        }
    }

//AbstractEntityInformation.java
	public boolean isNew(T entity) {

		ID id = getId(entity);
		Class<ID> idType = getIdType();

		if (!idType.isPrimitive()) {
			return id == null;
		}

		if (id instanceof Number) {
			return ((Number) id).longValue() == 0L;
		}

		throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
	}

다음과 같이 되어있다. isNew 함수를 통해 엔티티를 판단할떄 ID가 0이거나 NULL이면 신규 엔티티로, ID 값이 있으면 이미 있는 엔티티로 판단한다. 실제로 ID가 -1로 설정이 되어있으면 isNew에서 false 값을 얻게되고 em.persist가 아닌 em.merge 함수가 실행된다. 근데 이게 단일 객체일때는 그냥 저장이 되는 거였다. 그래서 더 파고 들어가 보았다.

//DefualtMergeEventListener.java
final Object result = source.get( entityName, clonedIdentifier )

if ( result == null ) {
	//TODO: we should throw an exception if we really *know* for sure
	//      that this is a detached instance, rather than just assuming
	//throw new StaleObjectStateException(entityName, id);

	// we got here because we assumed that an instance
	// with an assigned id was detached, when it was
	// really persistent
	entityIsTransient( event, copyCache );
}

DefualtMergeEventListener에서 ID 값이 있으면 Select을 통해 엔티티를 들고오는데 result가 null일 경우 entityIsTransient을 통해 entity를 저장을 해준다... 주석을 읽어보면 JPA 개발자들도 merge 이벤트시 엔티티가 데이터베이스에 없으면 해당 엔티티가 진짜 detached 된 엔티티인지 판단할 수 없어서 그냥 세이브를 하는것 같다.

그래서 단일 엔티티가 저장될때는 오류가 나지 않았던 것이다.

그렇다면 자식 관계일 경우에는 왜 오류가 날까?

자식의 경우에는 ID 값이 있을 경우 이미 있는 엔티티라고 판단하고 DefualtMergeEventListener에서 copyValues를 통해 자식 엔티티를 가져오려 하는데 데이터베이스에 있을리가 없으니 오류가 나는 것이다.

	protected void copyValues(
			final EntityPersister persister,
			final Object entity,
			final Object target,
			final SessionImplementor source,
			final Map copyCache) {
		final Object[] copiedValues = TypeHelper.replace(
				persister.getPropertyValues( entity ),
				persister.getPropertyValues( target ),
				persister.getPropertyTypes(),
				source,
				target,
				copyCache
		);

		persister.setPropertyValues( target, copiedValues );
	}

끝.

틀린 부분이 있다면 지적해주세요!

0개의 댓글