Запрет Hibernate от удаления потерянных сущностей при объединении сущностей, у которых ассоциации сущностей установлены с установленным значением true для orphanRemoval

Возьмем очень простой пример отношений один-ко-многим (страна -> государство).

Страна (обратная сторона):

@OneToMany(mappedBy = "country", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<StateTable> stateTableList=new ArrayList<StateTable>(0);

StateTable (владеющая сторона):

@JoinColumn(name = "country_id", referencedColumnName = "country_id")
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH})
private Country country;

Метод, пытающийся обновить предоставленный (отсоединенный) StateTable сущность в активной транзакции базы данных (JTA или ресурс локальный):

public StateTable update(StateTable stateTable) {

    // Getting the original state entity from the database.
    StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId());
    // Get hold of the original country (with countryId = 67, for example).
    Country oldCountry = oldState.getCountry();
    // Getting a new country entity (with countryId = 68) supplied by the client application which is responsible for modifying the StateTable entity.
    // Country has been changed from 67 to 68 in the StateTable entity using for example, a drop-down list.
    Country newCountry = entityManager.find(Country.class, stateTable.getCountry().getCountryId());
    // Attaching a managed instance to StateTable.
    stateTable.setCountry(newCountry);

    // Check whether the supplied country and the original country entities are equal.
    // (Both not null and not equal - http://stackru.com/a/31761967/1391249)
    if (ObjectUtils.notEquals(newCountry, oldCountry)) {
        // Remove the state entity from the inverse collection held by the original country entity.
        oldCountry.remove(oldState);
        // Add the state entity to the inverse collection held by the newly supplied country entity
        newCountry.add(stateTable);
    }

    return entityManager.merge(stateTable);
}

Следует отметить, что orphanRemoval установлен в true, StateTable сущность предоставляется клиентским приложением, которое заинтересовано в изменении связи сущности Country (countryId = 67) в StateTable к чему-то другому (countryId = 68) (таким образом, с обратной стороны в JPA, перенос дочерней сущности из ее родителя (коллекции) в другой родитель (коллекцию), который orphanRemoval=true будет в свою очередь противостоять).

Поставщик Hibernate выдает DELETE Оператор DML, вызывающий строку, соответствующую StateTable сущность, которая будет удалена из базовой таблицы базы данных.

Несмотря на то, что orphanRemoval установлен в true, Я ожидаю, что Hibernate выпустит регулярный UPDATE Оператор DML, вызывающий эффект orphanRemoval быть полностью приостановленным, потому что ссылка связи переносится (а не просто удаляется).

EclipseLink делает именно эту работу. Выдает UPDATE утверждение в заданном сценарии (имеющие такие же отношения с orphanRemoval установлен в true).

Какой из них ведет себя в соответствии со спецификацией? Можно ли заставить Hibernate выдавать UPDATE заявление в этом случае, кроме удаления orphanRemoval с обратной стороны?


Это всего лишь попытка сделать двусторонние отношения более последовательными с обеих сторон.

Методы управления защитной связью, а именно add() а также remove() используемые в приведенном выше фрагменте, при необходимости, определены в Country сущность следующим образом.

public void add(StateTable stateTable) {
    List<StateTable> newStateTableList = getStateTableList();

    if (!newStateTableList.contains(stateTable)) {
        newStateTableList.add(stateTable);
    }

    if (stateTable.getCountry() != this) {
        stateTable.setCountry(this);
    }
}

public void remove(StateTable stateTable) {
    List<StateTable> newStateTableList = getStateTableList();

    if (newStateTableList.contains(stateTable)) {
        newStateTableList.remove(stateTable);
    }
}


Обновить:

Hibernate может выдавать только ожидаемый UPDATE Оператор DML, если данный код изменен следующим образом.

public StateTable update(StateTable stateTable) {
    StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId());
    Country oldCountry = oldState.getCountry();
    // DELETE is issued, if getReference() is replaced by find().
    Country newCountry = entityManager.getReference(Country.class, stateTable.getCountry().getCountryId());

    // The following line is never expected as Country is already retrieved 
    // and assigned to oldCountry above.
    // Thus, oldState.getCountry() is no longer an uninitialized proxy.
    oldState.getCountry().hashCode(); // DELETE is issued, if removed.
    stateTable.setCountry(newCountry);

    if (ObjectUtils.notEquals(newCountry, oldCountry)) {
        oldCountry.remove(oldState);
        newCountry.add(stateTable);
    }

    return entityManager.merge(stateTable);
}

Обратите внимание на следующие две строки в новой версии кода.

// Previously it was EntityManager#find()
Country newCountry = entityManager.getReference(Country.class, stateTable.getCountry().getCountryId());
// Previously it was absent.
oldState.getCountry().hashCode();

Если последняя строка отсутствует или EntityManager#getReference() заменяется EntityManager#find(), затем DELETE Заявление DML неожиданно выдано.

И так, что здесь происходит? Особенно подчеркиваю мобильность. Отсутствие переноса этого вида базовой функциональности на разных поставщиков JPA серьезно затрудняет использование сред ORM.

Я понимаю принципиальную разницу между EntityManager#getReference() а также EntityManager#find() ,

2 ответа

Решение

Во-первых, давайте изменим ваш исходный код на более простую форму:

StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId());
Country oldCountry = oldState.getCountry();
oldState.getCountry().hashCode(); // DELETE is issued, if removed.

Country newCountry = entityManager.find(Country.class, stateTable.getCountry().getCountryId());
stateTable.setCountry(newCountry);

if (ObjectUtils.notEquals(newCountry, oldCountry)) {
    oldCountry.remove(oldState);
    newCountry.add(stateTable);
}

entityManager.merge(stateTable);

Обратите внимание, что я только добавил oldState.getCountry().hashCode() в третьей строке. Теперь вы можете воспроизвести вашу проблему, удалив только эту строку.

Прежде чем мы объясним, что здесь происходит, сначала несколько выдержек из спецификации JPA 2.1.

Раздел 3.2.4:

Семантика операции сброса, примененной к сущности X, следующая:

  • Если X является управляемым объектом, он синхронизируется с базой данных.
    • Для всех объектов Y, на которые ссылается отношение из X, если отношение к Y было аннотировано значением элемента каскада cascade=PERSIST или cascade=ALL, операция постоянства применяется к Y

Раздел 3.2.2:

Семантика сохраняемой операции, применяемой к сущности X, следующая:

  • Если X является удаленным объектом, он становится управляемым.

orphanRemoval JPA Javadoc:

(Необязательно) Применять ли операцию удаления к объектам, которые были удалены из отношения, и каскадно удалять операцию для этих объектов.

Как мы можем видеть, orphanRemoval определяется с точки зрения remove операция, поэтому все правила, которые применяются для remove должен подать заявку на orphanRemoval также.

Во-вторых, как объясняется в этом ответе, порядок обновлений, выполняемых Hibernate, является порядком, в котором объекты загружаются в контексте постоянства. Чтобы быть более точным, обновление объекта означает синхронизацию его текущего состояния (грязная проверка) с базой данных и каскадирование PERSIST операция с его ассоциациями.

Теперь, это то, что происходит в вашем случае. В конце транзакции Hibernate синхронизирует контекст персистентности с базой данных. У нас есть два сценария:

  1. Когда дополнительная строка (hashCode) настоящее:

    1. Hibernate синхронизируется oldCountry с БД. Делает это перед обработкой newCountry, так как oldCountry был загружен первым (инициализация прокси была вызвана вызовом hashCode).
    2. Hibernate видит, что StateTable Экземпляр был удален из oldCountryколлекции, таким образом отмечая StateTable экземпляр как удаленный.
    3. Hibernate синхронизируется newCountry с БД. PERSIST каскады операций к stateTableList который теперь содержит удаленный StateTable экземпляр объекта.
    4. Удаленный StateTable экземпляр теперь снова управляется (раздел 3.2.2 спецификации JPA, приведенной выше).
  2. Когда дополнительная строка (hashCode) отсутствует:

    1. Hibernate синхронизируется newCountry с БД. Делает это перед обработкой oldCountry, так как newCountry был загружен первым (с entityManager.find).
    2. Hibernate синхронизируется oldCountry с БД.
    3. Hibernate видит, что StateTable Экземпляр был удален из oldCountryколлекции, таким образом отмечая StateTable экземпляр как удаленный.
    4. Удаление StateTable Экземпляр синхронизируется с базой данных.

Порядок обновлений также объясняет ваши выводы, в которых вы в основном вынуждены oldCountry инициализация прокси перед загрузкой newCountry из БД.

Итак, это в соответствии со спецификацией JPA? Очевидно, да, никакое спецификационное правило JPA не нарушено.

Почему это не портативный?

Спецификация JPA (как и любая другая спецификация в конце концов) предоставляет провайдерам свободу определять многие детали, не охватываемые спецификацией.

Кроме того, это зависит от вашего взгляда на "мобильность". orphanRemoval Функция и любые другие функции JPA являются переносимыми, когда дело доходит до их формальных определений. Однако это зависит от того, как вы используете их в сочетании со спецификой вашего провайдера JPA.

Кстати, раздел 2.9 спецификации рекомендует (но не дает четкого определения) для orphanRemoval:

В противном случае переносимые приложения не должны зависеть от определенного порядка удаления и не должны переназначать объект, который потерял связь с другим, или иным образом пытаться его сохранить.

Но это всего лишь пример расплывчатых или недостаточно четко определенных рекомендаций в спецификации, поскольку сохранение удаленных объектов допускается другими операторами в спецификации.

Как только ваша сущность, на которую вы ссылаетесь, может использоваться другими родителями, она все равно усложняется. Чтобы действительно очистить его, ORM должен был искать в базе данных любые другие случаи использования удаленного объекта перед его удалением (постоянная сборка мусора). Это отнимает много времени и поэтому не очень полезно и поэтому не реализовано в Hibernate.

Удаление сирот работает, только если ваш ребенок используется для одного родителя и никогда не используется повторно где-либо еще. Вы можете даже получить исключение при попытке повторно использовать его, чтобы лучше обнаружить злоупотребление этой функцией.

Решите, хотите ли вы удалять сирот или нет. Если вы хотите сохранить его, вам нужно создать нового потомка для нового родителя, а не перемещать его.

Если вы откажетесь от удаления сирот, вы должны удалить детей самостоятельно, как только на них больше не будут ссылаться.

Другие вопросы по тегам