Может ли Hibernate удалять потерянные коллекции при обновлении отдельного объекта?

Я знаю, что удаление дочерних объектов-сирот - это распространенный вопрос о SO и общая проблема для людей, новых для Hibernate, и что довольно стандартный ответ заключается в том, чтобы вы имели некоторые изменения cascade=all,delete-orphan или cascade=all-delete-orphan на детская коллекция.

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

Parent parent = session.get(...);
parent.getChildren().clear();
session.update(parent);

Мое текущее сопоставление для класса Parent выглядит следующим образом:

<bag name="children" cascade="all-delete-orphan">
    <key column="parent_id" foreign-key="fk_parent_id"/>
    <one-to-many class="Child"/>
</bag>

Это работает отлично для меня при обновлении прикрепленного объекта, но у меня есть прецедент, в котором мы хотели бы иметь возможность отделить отдельный объект (который был отправлен нашему API-методу удаленным клиентом по HTTP/JSON) и передать его непосредственно на сеанс Hibernate - чтобы клиенты могли манипулировать родительским объектом в зависимости от того, что им нравится, и чтобы изменения сохранялись.

При вызове session.update(parent) в моем отдельном объекте строки в дочерней таблице становятся сиротами (для столбца FK установлено значение null), но не удаляются. Обратите внимание, что когда я звоню session.update(), это первый сеанс Hibernate Session, который видит этот экземпляр объекта - я не присоединяю или не скрепляю объект с сеансом каким-либо другим способом. Я полагаюсь на клиента, чтобы передать объекты, идентификаторы которых соответствуют реальным объектам в базе данных. Например, логика в моем сервис-методе API выглядит примерно так:

String jsonString = request.getParameter(...);
Parent parent = deserialize(jsonString);
session.update(parent);

Возможно ли, чтобы Hibernate обнаружил дочерние коллекции для детей-сирот в изолированных родительских объектах при передаче в session.update(parent)? Или я неправильно использую отдельный объект?

Моя надежда заключалась в том, что я мог избежать любых сложных взаимодействий с Hibernate, чтобы сохранить изменения в отдельном экземпляре. Мой API-метод не нуждается в дальнейшем изменении отдельного объекта после вызова session.update(parent), этот метод просто отвечает за сохраняющиеся изменения, сделанные удаленными клиентскими приложениями.

Ответы

Ответ 1

Ваше сопоставление (упрощенное)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="br.com._3988215.model.domain">
    <class name="Parent" table="PARENT">
        <id name="id">
            <generator class="native"/>
        </id>
        <bag cascade="all,delete-orphan" name="childList">
            <key column="PARENT_ID" not-null="false"/>
            <one-to-many class="Child"/>
        </bag>
    </class>
    <class name="Child" table="CHILD">
        <id name="id" column="CHILD_ID">
            <generator class="native"/>
        </id>
    </class>
</hibernate-mapping>

производит

PARENT
    ID

CHILD
    CHILD_ID
    PARENT_ID

В соответствии с тем, что вы сказали

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

Что-то вроде

Parent parent = session.get(...);
parent.getChildren().clear();

session.update(parent);

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

Теперь посмотрим следующий (Notice Assert.assertNull(второй))

public class WhatYouWantTest {

    private static SessionFactory sessionFactory;

    private Serializable parentId;

    private Serializable firstId;
    private Serializable secondId;

    @BeforeClass
    public static void setUpClass() {
        Configuration c = new Configuration();
        c.addResource("mapping.hbm.3988215.xml");

        sessionFactory = c.configure().buildSessionFactory();
    }

    @Before
    public void setUp() throws Exception {
        Parent parent = new Parent();
        Child first   = new Child();
        Child second  = new Child();

        Session session = sessionFactory.openSession();
        session.beginTransaction();

        parentId = session.save(parent);
        firstId  = session.save(first);
        secondId = session.save(second);

        parent.getChildList().add(first);
        parent.getChildList().add(second);

        session.getTransaction().commit();
        session.close();
    }

    @Test
    public void removed_second_from_parent_remove_second_from_database() {
        Parent parent = new Parent();
        parent.setId((Integer) parentId);

        Child first = new Child();
        first.setId((Integer) firstId);

        /**
          * It simulates the second one has been removed
          */
        parent.getChildList().add(first);

        Session session = sessionFactory.openSession();
        session.beginTransaction();

        session.update(parent);

        session.getTransaction().commit();
        session.close();

        session = sessionFactory.openSession();
        session.beginTransaction();

        Child second = (Child) session.get(Child.class, secondId);
        Assert.assertNull(second);

        session.getTransaction().commit();
        session.close();
    }
}

К сожалению, тест не проходит. Что вы можете сделать???

  • Включить длительный сеанс

Спящий режим ссылается на

Расширенный (или длинный) сеанс. Сессия Hibernate может быть отключена от базового JDBC-соединения после транзакции базы данных и повторно подключена при возникновении нового запроса клиента. Этот шаблон известен как сеанс для разговора и делает невозможным повторную привязку. Автоматическое управление версиями используется для изоляции одновременных изменений, и сеансу обычно не разрешается автоматически очищаться, но явно.

отказ от ответственности: у меня нет сценария, который использует длительный разговор. Java EE Stateful session beans поддерживает длительный разговор. Но его поддержка для JPA (не Hibernate)

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

Создайте класс с именем AlternativeParent , который расширяет Parent

public class AlternativeParent extends Parent {}

Теперь его отображение (примечание Child как составной элемент вместо plain @Entity)

<class name="AlternativeParent" table="PARENT">
    <id name="id">
        <generator class="native"/>
    </id>
    <bag name="childList" table="CHILD">
        <key column="PARENT_ID" not-null="false"/>
        <composite-element class="Child">
            <property column="CHILD_ID" name="id"/>
        </composite-element>
    </bag>
</class>

Теперь реализуем удобный метод equals в классе Child

public boolean equals(Object o) {
    if (!(o instanceof Child))
        return false;

    Child other = (Child) o;
    // identity equality
    // Used by composite elements
    if(getId() != null) {
        return new EqualsBuilder()
                   .append(getId(), other.getId())
                   .isEquals();
    } else {
        // object equality
     }
}

Если я реорганизую тестовый пример, показанный выше (теперь вместо этого используйте AlternativeParent)

@Test
public void removed_second_from_parent_remove_second_from_database() {
    AlternativeParent parent = new AlternativeParent();
    parent.setId((Integer) parentId);

    Child first = new Child();
    first.setId((Integer) firstId);

    /**
      * It simulates the second one has been removed
      */
    parent.getChildList().add(first);

    Session session = sessionFactory.openSession();
    session.beginTransaction();

    session.update(parent);

    session.getTransaction().commit();
    session.close();

    session = sessionFactory.openSession();
    session.beginTransaction();

    Child second = (Child) session.get(Child.class, secondId);
    Assert.assertNull(second);

    session.getTransaction().commit();
    session.close();

}

Я вижу зеленую полосу

Ответ 2

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