Can Hibernate delete orphaned collections when updating a detached object?
Asked Answered
L

2

17

I know that deleting orphaned child objects is a common question on SO and a common problem for people new to Hibernate, and that the fairly standard answer is to ensure that you have some variation of cascade=all,delete-orphan or cascade=all-delete-orphan on the child collection.

I'd like to be able to have Hibernate detect that child collection has been emptied/removed from the parent object, and have the rows in the child table deleted from the database when the parent object is updated. For example:

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

My current mapping for the Parent class looks like:

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

This works fine for me when updating an attached object, but I have a use case in which we'd like to be able to take a detached object (which has been sent to our API method by a remote client over HTTP/JSON), and pass it directly to the Hibernate Session - to allow clients to be able to manipulate the parent object in whichever way they like and have the changes persisted.

When calling session.update(parent) on my detached object, the rows in the child table are orphaned (the FK column is set to null) but not deleted. Note that when I'm calling session.update(), this is the first time the Hibernate Session is seeing this object instance - I am not re-attaching or merging the object with the Session in any other way. I'm relying on the client to pass objects whose identifiers correspond to actual objects in the database. For example, the logic in my API service method is something like this:

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

Is it possible for Hibernate to detect orphaned children collections in detached parent objects when passed to session.update(parent)? Or am I mis-using the detached object in some way?

My hope was that I could avoid any sort of complex interactions with Hibernate to persist changes to a detached instance. My API method has no need to further modify the detached object after the call to session.update(parent), this method is merely responsible for persisting changes made by remote client applications.

Letha answered 21/10, 2010 at 13:57 Comment(0)
Z
8

Your mapping (simplified)

<?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>

produces

PARENT
    ID

CHILD
    CHILD_ID
    PARENT_ID

According what you said

I would like to be able to have Hibernate detect that child collection has been removed from the parent object, and have the rows in the child table deleted from the database when the parent object is updated

Something like

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

session.update(parent);

You said it works fine because you have an attached Parent instance

Now let's see the following one (Notice Assert.assertNull(second))

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();
    }
}

Unfortunately, the test do not pass. What you can do ???

  • Enable a long-running conversation

Hibernate reference says

Extended (or Long) Session - The Hibernate Session may be disconnected from the underlying JDBC connection after the database transaction has been committed, and reconnected when a new client request occurs. This pattern is known as session-per-conversation and makes even reattachment unnecessary. Automatic versioning is used to isolate concurrent modifications and the Session is usually not allowed to be flushed automatically, but explicitely.

disclaimer: i do not have any scenario which uses long running conversation. Java EE Stateful session beans support long running conversation. But its support is for JPA (not Hibernate)

Or you can create an alternative mapping which enables your Child as composite elements. Because its lifecycle depends on the parent object, you can rely on composite elements to get what you want

Create a class named AlternativeParent which extends Parent

public class AlternativeParent extends Parent {}

Now its mapping (Notice Child as composite element instead of 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>

Now implement a convenient equals method in the Child class

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
     }
}

If i refactor the test case shown above (Now by using AlternativeParent instead)

@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();

}

I see a green bar

Zsa answered 30/10, 2010 at 19:54 Comment(0)
L
1

i think,when using detached session, you might face problem, with collections. i will suggest you to first load the entity with collection, and then update that entity with the changes, that will help.

Lingua answered 27/10, 2010 at 4:52 Comment(3)
Do you mean, load the existing entity and then merge() it with the detached instance passed to me API?Letha
@ mattb : you can write a logic to merge the collection, or even just replace the previous collection and set the new one, but make sure that you have added <sql-delete> tag in your hbm. This <sql-delete> will contain a simple sql delete query to delete the complete collection before saving a new one. or in other ways, whever you will add anything, this sql delete will be fired everytime and then fresh copy of your collection will be added. This can resolve your problem of everytime updating the collection manually.Lingua
Not sure if I like this solution, as it would require changing my use pattern - adding <sql-delete> for example. Was hoping this could be fixed with mapping alone.Letha

© 2022 - 2024 — McMap. All rights reserved.