회사 코드에 작업중 일부 코드를 수정해야했다. 하지만 당연히 잘 동작할 것이라고 생각했던게 뜬금없이 JpaRepository#save()
에서 예상치 못한 예외를 뱉어내는게 아닌가? 그래서 이를 해결하는 과정에서 라이브러리를 뜯어보게 됐다. 하나씩 뜯어보다가 문득 여기에 시간을 쏟게된게 너무 억울해서(내가 짠 코드였으면 억울하지라도 않지) 이렇게 글을 남긴다.
변경의 여지가 있는 Entity 내부의 List
타입의 프로퍼티에는 MutableList
를 사용하자.
공식 문서만 읽어봐도 쉽게 알 수 있다(...).
JPA는 내부적으로 Entity의 List
를 변경한 이후 save를 진행할 때, 이미 List
에 데이터가 존재한다면 List
내부를 변경하는게 아니라 아예 새로 할당하는 것인데도 List#clear()
함수를 통해 내부 요소들을 하나씩 remove()
하고 있다는 것이다. 덕분에 내부 동작을 좀 더 알게 되었다. List
의 크기가 크다면.. 좀 위험할지도?
대충 느낌만 냈다. main
함수를 살펴보시라.
package ...
import org.springframework.data.jpa.repository.JpaRepository
import javax.persistence.CascadeType
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.OneToMany
@Entity
class School(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@JoinColumn(name = "schoolId")
@OneToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
var students: List<Student>? = null
) {
fun updateStudents(newStudents: List<Student>) {
this.students = newStudents
}
}
@Entity
class Student(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
var name: String? = null
)
interface SchoolRepository : JpaRepository<School, Long>
fun main() {
// 실제로는 Spring의 ApplicationContext 로부터 DI 받은 SchoolRepositoy를 사용.
val schoolRepository = object : SchoolRepository {
// implements
}
val oldStudents = listOf(Student(id = 1L, name = "banjjoknim"))
val school = School(id = 1L, oldStudents) // 실제로 조회해왔을 때의 상태라고 가정.
val newStudents = listOf(Student(id = 2L, name = "colt")) // 조회해온 school의 학생 목록을 새로운 학생 목록으로 교체한다.
school.updateStudents(newStudents)
schoolRepository.save(school) // throw UnSupportedException.......
}
순서는 다음과 같다.
List
타입의 객체에 새로운 List
값을 할당하면 org.hibernate.type.CollectionType#replace()
함수 내부에서 replaceElements()
함수가 호출된다.package org.hibernate.type;
// ...
/**
* A type that handles Hibernate <tt>PersistentCollection</tt>s (including arrays).
*
* @author Gavin King
*/
public abstract class CollectionType extends AbstractType implements AssociationType {
// ...
@Override
public Object replace(
final Object original,
final Object target,
final SharedSessionContractImplementor session,
final Object owner,
final Map copyCache) throws HibernateException {
if ( original == null ) {
return null;
}
if ( !Hibernate.isInitialized( original ) ) {
if ( ( (PersistentCollection) original ).hasQueuedOperations() ) {
if ( original == target ) {
// A managed entity with an uninitialized collection is being merged,
// We need to replace any detached entities in the queued operations
// with managed copies.
final AbstractPersistentCollection pc = (AbstractPersistentCollection) original;
pc.replaceQueuedOperationValues( getPersister( session ), copyCache );
}
else {
// original is a detached copy of the collection;
// it contains queued operations, which will be ignored
LOG.ignoreQueuedOperationsOnMerge(
MessageHelper.collectionInfoString(
getRole(),
( (PersistentCollection) original ).getKey()
)
);
}
}
return target;
}
// for a null target, or a target which is the same as the original, we
// need to put the merged elements in a new collection
Object result = ( target == null ||
target == original ||
target == LazyPropertyInitializer.UNFETCHED_PROPERTY ) ?
instantiateResult( original ) : target;
//for arrays, replaceElements() may return a different reference, since
//the array length might not match
result = replaceElements( original, result, owner, copyCache, session );
if ( original == target ) {
// get the elements back into the target making sure to handle dirty flag
boolean wasClean = PersistentCollection.class.isInstance( target ) && !( ( PersistentCollection ) target ).isDirty();
//TODO: this is a little inefficient, don't need to do a whole
// deep replaceElements() call
replaceElements( result, target, owner, copyCache, session );
if ( wasClean ) {
( ( PersistentCollection ) target ).clearDirty();
}
result = target;
}
return result;
}
/**
* Replace the elements of a collection with the elements of another collection.
*
* @param original The 'source' of the replacement elements (where we copy from)
* @param target The target of the replacement elements (where we copy to)
* @param owner The owner of the collection being merged
* @param copyCache The map of elements already replaced.
* @param session The session from which the merge event originated.
* @return The merged collection.
*/
public Object replaceElements(
Object original,
Object target,
Object owner,
Map copyCache,
SharedSessionContractImplementor session) {
// TODO: does not work for EntityMode.DOM4J yet!
java.util.Collection result = ( java.util.Collection ) target;
result.clear();
// copy elements into newly empty target collection
Type elemType = getElementType( session.getFactory() );
Iterator iter = ( (java.util.Collection) original ).iterator();
while ( iter.hasNext() ) {
result.add( elemType.replace( iter.next(), null, session, owner, copyCache ) );
}
// if the original is a PersistentCollection, and that original
// was not flagged as dirty, then reset the target's dirty flag
// here after the copy operation.
// </p>
// One thing to be careful of here is a "bare" original collection
// in which case we should never ever ever reset the dirty flag
// on the target because we simply do not know...
if ( original instanceof PersistentCollection ) {
if ( result instanceof PersistentCollection ) {
final PersistentCollection originalPersistentCollection = (PersistentCollection) original;
final PersistentCollection resultPersistentCollection = (PersistentCollection) result;
preserveSnapshot( originalPersistentCollection, resultPersistentCollection, elemType, owner, copyCache, session );
if ( ! originalPersistentCollection.isDirty() ) {
resultPersistentCollection.clearDirty();
}
}
}
return result;
}
// ...
}
org.hibernate.type.CollectionType#replaceElements()
함수 내부에서 result.clear()
가 호출된다.
이때 result.clear()
는 실제로는 package.org.hibernate.collection.internal.PersistentBag#clear()
함수이다(result.clear()
가 실제로 문제가 되는 부분임).
package org.hibernate.collection.internal;
/**
* An unordered, unkeyed collection that can contain the same element
* multiple times. The Java collections API, curiously, has no <tt>Bag</tt>.
* Most developers seem to use <tt>List</tt>s to represent bag semantics,
* so Hibernate follows this practice.
*
* @author Gavin King
*/
public class PersistentBag extends AbstractPersistentCollection implements List {
// ...
protected List bag;
@Override
@SuppressWarnings("unchecked")
public void clear() {
if ( isClearQueueEnabled() ) {
queueOperation( new Clear() );
}
else {
initialize( true );
if ( !bag.isEmpty() ) {
bag.clear();
dirty();
}
}
}
// ...
}
해당 함수 내에서 bag.clear()
를 호출하는데, 이는 java.util.List#clear()
함수이다.
이때 실제로 호출 되는 함수는 AbstractList#clear()
이다.
package java.util;
public interface Collection<E> extends Iterable<E> {
// ...
/**
* Removes all of the elements from this collection (optional operation).
* The collection will be empty after this method returns.
*
* @throws UnsupportedOperationException if the {@code clear} operation
* is not supported by this collection
*/
void clear();
// ...
}
package java.util;
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
// ...
/**
* Removes all of the elements from this list (optional operation).
* The list will be empty after this call returns.
*
* @implSpec
* This implementation calls {@code removeRange(0, size())}.
*
* <p>Note that this implementation throws an
* {@code UnsupportedOperationException} unless {@code remove(int
* index)} or {@code removeRange(int fromIndex, int toIndex)} is
* overridden.
*
* @throws UnsupportedOperationException if the {@code clear} operation
* is not supported by this list
*/
public void clear() {
removeRange(0, size());
}
// ...
/**
* Removes from this list all of the elements whose index is between
* {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.
* Shifts any succeeding elements to the left (reduces their index).
* This call shortens the list by {@code (toIndex - fromIndex)} elements.
* (If {@code toIndex==fromIndex}, this operation has no effect.)
*
* <p>This method is called by the {@code clear} operation on this list
* and its subLists. Overriding this method to take advantage of
* the internals of the list implementation can <i>substantially</i>
* improve the performance of the {@code clear} operation on this list
* and its subLists.
*
* @implSpec
* This implementation gets a list iterator positioned before
* {@code fromIndex}, and repeatedly calls {@code ListIterator.next}
* followed by {@code ListIterator.remove} until the entire range has
* been removed. <b>Note: if {@code ListIterator.remove} requires linear
* time, this implementation requires quadratic time.</b>
*
* @param fromIndex index of first element to be removed
* @param toIndex index after last element to be removed
*/
protected void removeRange(int fromIndex, int toIndex) {
ListIterator<E> it = listIterator(fromIndex);
for (int i=0, n=toIndex-fromIndex; i<n; i++) {
it.next();
it.remove();
}
}
}
package java.util;
public interface ListIterator<E> extends Iterator<E> {
// ...
/**
* Removes from the list the last element that was returned by {@link
* #next} or {@link #previous} (optional operation). This call can
* only be made once per call to {@code next} or {@code previous}.
* It can be made only if {@link #add} has not been
* called after the last call to {@code next} or {@code previous}.
*
* @throws UnsupportedOperationException if the {@code remove}
* operation is not supported by this list iterator
* @throws IllegalStateException if neither {@code next} nor
* {@code previous} have been called, or {@code remove} or
* {@code add} have been called after the last call to
* {@code next} or {@code previous}
*/
void remove();
// ...
}
package java.util;
public interface Iterator<E> {
// ...
/**
* Removes from the underlying collection the last element returned
* by this iterator (optional operation). This method can be called
* only once per call to {@link #next}.
* <p>
* The behavior of an iterator is unspecified if the underlying collection
* is modified while the iteration is in progress in any way other than by
* calling this method, unless an overriding class has specified a
* concurrent modification policy.
* <p>
* The behavior of an iterator is unspecified if this method is called
* after a call to the {@link #forEachRemaining forEachRemaining} method.
*
* @implSpec
* The default implementation throws an instance of
* {@link UnsupportedOperationException} and performs no other action.
*
* @throws UnsupportedOperationException if the {@code remove}
* operation is not supported by this iterator
*
* @throws IllegalStateException if the {@code next} method has not
* yet been called, or the {@code remove} method has already
* been called after the last call to the {@code next}
* method
*/
default void remove() {
throw new UnsupportedOperationException("remove");
}
// ...
}
AbstractList#clear()
의 내부 구현을 살펴보면, removeRange()
함수를 호출하고 그 내부를 살펴보면 remove()
함수를 호출하고 있다.
이때 호출되는 remove()
함수는 ListIterator#remove()
인데, 설명을 읽어보면 현재 호출자 List 객체에 remove()
함수가 지원되지 않는 경우 UnSupportedException
을 던진다(Iterator#remove()
가 조상인데, 기본 구현으로 UnSupportedException
을 던지게 되어있다).
package kotlin.collections
/**
* A generic ordered collection of elements. Methods in this interface support only read-only access to the list;
* read/write access is supported through the [MutableList] interface.
* @param E the type of elements contained in the list. The list is covariant in its element type.
*/
public interface List<out E> : Collection<E> {
// Query Operations
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
// Bulk Operations
override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
// Positional Access Operations
/**
* Returns the element at the specified index in the list.
*/
public operator fun get(index: Int): E
// Search Operations
/**
* Returns the index of the first occurrence of the specified element in the list, or -1 if the specified
* element is not contained in the list.
*/
public fun indexOf(element: @UnsafeVariance E): Int
/**
* Returns the index of the last occurrence of the specified element in the list, or -1 if the specified
* element is not contained in the list.
*/
public fun lastIndexOf(element: @UnsafeVariance E): Int
// List Iterators
/**
* Returns a list iterator over the elements in this list (in proper sequence).
*/
public fun listIterator(): ListIterator<E>
/**
* Returns a list iterator over the elements in this list (in proper sequence), starting at the specified [index].
*/
public fun listIterator(index: Int): ListIterator<E>
// View
/**
* Returns a view of the portion of this list between the specified [fromIndex] (inclusive) and [toIndex] (exclusive).
* The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa.
*
* Structural changes in the base list make the behavior of the view undefined.
*/
public fun subList(fromIndex: Int, toIndex: Int): List<E>
}
List
를 살펴보자. Kotlin의 List
는 Collection
인터페이스 타입을 상속받았고 remove()
함수가 없다(설명에도 대놓고 read-only
라고 적혀있고 write
까지 하고 싶으면 MutableList
를 사용하라고 친절하게 적혀있다).package kotlin.collections
/**
* A generic collection of elements. Methods in this interface support only read-only access to the collection;
* read/write access is supported through the [MutableCollection] interface.
* @param E the type of elements contained in the collection. The collection is covariant in its element type.
*/
public interface Collection<out E> : Iterable<E> {
// Query Operations
/**
* Returns the size of the collection.
*/
public val size: Int
/**
* Returns `true` if the collection is empty (contains no elements), `false` otherwise.
*/
public fun isEmpty(): Boolean
/**
* Checks if the specified element is contained in this collection.
*/
public operator fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
// Bulk Operations
/**
* Checks if all elements in the specified collection are contained in this collection.
*/
public fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
}
List
타입이 상속받은 Collection
을 살펴보자. 여기에도 마찬가지로 remove()
함수가 없다.List
타입은 remove()
와 같은 write
용도의 함수를 지원하지 않기 때문에 UnSupportedException
이 발생하게 된다.