Подтвердить что ты не робот

Предотвратить спящий режим для удаления сиротских объектов при объединении объекта, имеющего ассоциации сущностей с orphanRemoval, установленным в true

Принимая очень простой пример отношения "один ко многим" (состояние страны ->).

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

@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://stackoverflow.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);
    }
}

суб >


Обновление:

Спящий режим может выдавать только ожидаемый оператор 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(), то неожиданно выдается a DELETE выражение DML.

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

Я понимаю основное различие между EntityManager#getReference() и EntityManager#find().

4b9b3361

Ответ 1

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

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:

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

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

orphanRemoval JPA javadoc:

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

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

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

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

  • При наличии дополнительной строки (hashCode):

    • Спящий режим синхронизирует oldCountry с БД. Он делает это перед обработкой newCountry, потому что сначала был загружен oldCountry (инициация прокси-сервера принудительно вызвана вызовом hashCode).
    • Hibernate видит, что экземпляр StateTable удален из коллекции oldCountry, тем самым помещая экземпляр StateTable как удаленный.
    • Спящий режим синхронизирует newCountry с БД. Операция PERSIST каскадируется до stateTableList, которая теперь содержит удаленный экземпляр объекта StateTable.
    • Удаленный экземпляр StateTable теперь снова управляется (раздел 3.2.2 спецификации JPA, приведенный выше).
  • Если дополнительная строка (hashCode) отсутствует:

    • Спящий режим синхронизирует newCountry с БД. Он делает это перед обработкой oldCountry, потому что сначала был загружен newCountryentityManager.find).
    • Спящий режим синхронизирует oldCountry с БД.
    • Hibernate видит, что экземпляр StateTable удален из коллекции oldCountry, тем самым помещая экземпляр StateTable как удаленный.
    • Удаление экземпляра StateTable синхронизируется с базой данных.

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

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

Почему это не переносится?

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

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

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

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

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

Ответ 2

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

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

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

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