-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 
Author Message
 Post subject: Reparenting child object without cascading delete-orphan
PostPosted: Tue Jun 26, 2007 11:50 am 
Newbie

Joined: Wed Feb 21, 2007 6:29 am
Posts: 5
I have a persistent class that has a tree-like structure, with each instance being parent to several children of the same type. I've used cascase="all,delete-orphan" so that I can control the association from the children collection itself (which is ordered - the whole thing is basically a folder structure) and have the parent property set automatically. However, if I want to change the parent of a group by .remove()ing it from one collection and .add()ing it to another, it gets deleted.

Is there anyway to make Hibernate skip the delete-orphan cascade here since the object now has a new parent? I've tried the "obvious" code below, and even various hacks like Session.evict()ing the child after the remove() and Session.replicate()ing it back in before the add, but this just confuses things since none of the actions have been flushed yet.

Hibernate version: 3.2

Mapping documents:

Code:
      <many-to-one name="parent" column="G_PARENT" foreign-key="FK_G_PARENT"
         class="com.foo.Group" insert="false" update="false"/>
      <list name="children" inverse="false" cascade="all,delete-orphan">
         <key column="G_PARENT" />
         <list-index column="G_INDEX" />
         <one-to-many class="com.foo.Group" />
      </list>


Code between sessionFactory.openSession() and session.close():

Code:
      session.refresh(group);
      session.refresh(newParent);
      Group oldParent = group.getParent();
      oldParent.getChildren().remove(group);
      newParent.getChildren().add(group);


This leads to

Code:
16:10:16,409 INFO  [STDOUT] Hibernate:
    update
        GROUPS
    set
        G_PARENT=?,
        G_INDEX=?
    where
        G_ID=?
16:10:16,424 INFO  [STDOUT] Hibernate:
    update
        GROUPS
    set
        G_PARENT=null,
        G_INDEX=null
    where
        G_PARENT=?
        and G_ID=?
16:10:16,424 INFO  [STDOUT] Hibernate:
    delete
    from
        GROUPS
    where
        G_ID=?


Interestingly, the following works like a charm to change the order of an element within the same parent:

Code:
      session.refresh(group);
      session.refresh(parent);
      parent.getChildren().remove(group);
      parent.getChildren().add(group);


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jun 26, 2007 4:21 pm 
Senior
Senior

Joined: Tue Jun 12, 2007 4:49 pm
Posts: 127
Location: India
Well hibernate is doing what its being told. Its being asked to delete the object by calling remove on the parent, with inverse='false', and then adding it to another one, hence asking it to add a new child based on inverse='false' again.

In your case, you might be better of setting inverse='true' on the collection and then instead of adding and removing the group, just change parent in the group by group.setParent(newParent).

*Please do rate if it helps*

Regards,
Jitendra


Top
 Profile  
 
 Post subject:
PostPosted: Wed Jun 27, 2007 6:30 am 
Newbie

Joined: Wed Feb 21, 2007 6:29 am
Posts: 5
jits_1998 wrote:
Well hibernate is doing what its being told. Its being asked to delete the object by calling remove on the parent, with inverse='false', and then adding it to another one, hence asking it to add a new child based on inverse='false' again.


Well, yes and no. I see why it's doing what it's doing, but I'm not convinced it's doing "what it's told" or "what it should", because:

1. The newParent.getChildren().add(group) call fails silently. I would have thought that at the very least Hibernate would complain about the object having been deleted. In fact, looking at the SQL it seems Hibernate is adding the child to the new parent first, then deleting it...

2. I told Hibernate to "delete orphans" but within the context of the transaction the child is not an orphan since it has a new parent before the transaction is closed.

3. If point 2 above is irrelevant and "delete-orphan" semantics are to delete the orphan immediately when I remove it from the old parent, then how come it doesn't delete if I add the child back to the same parent later in the transaction? (as in when I want to change the order of the children - see my last example in the orginal post)

4. Regardless, reparenting a child, even within the context of a "delete-orphan" cascading relationship, is a valid user operation and I think it should be allowed. This is something I could do in SQL (including the cascade delete) but can't do in Hibernate, and I was under the impression that we should be able to do everything in Hibernate :)

To be honest this is looking more and more like a bug. As far as I can tell it would be proper for Hibernate to either:

a. Delete the object immediately, and throw an exception if I try to add it to a parent again, even if it's the same parent

b. Only delete the orphan object after the transaction is closed and only if it hasn't been added to another parent.

With b. of course being the preferable one.

jits_1998 wrote:
In your case, you might be better of setting inverse='true' on the collection and then instead of adding and removing the group, just change parent in the group by group.setParent(newParent).


Would love to but this being an ordered collection (list) you can't use the parent relationship as the main one because you need to set the list index. Unless I find a solution I'll probably remove the "delete-orphan" cascade and delete orphans manually.


Top
 Profile  
 
 Post subject:
PostPosted: Wed Jun 27, 2007 11:56 am 
Senior
Senior

Joined: Tue Jun 12, 2007 4:49 pm
Posts: 127
Location: India
I think there is some contradiction in what is happening in the test case below and your explanation of the problem.

Code:
<class name="Self">
      <id name="id" column="id" type="long" unsaved-value="0">
          <generator class="increment"/>
      </id>
      
      <property name="name"/>
      
      <set name="children"  cascade="all-delete-orphan" inverse="false">
         <key column="parent_id"/>
         <one-to-many class="Self" />
      </set>
      
      <many-to-one name="parent" column="parent_id" class="Self"/>
      
   </class>


Test method:
Code:

      Self a = new Self();
      a.setName("parent");
      Session s = factory.openSession();
      s.save(a);
      
      s.flush();
      
      a = (Self)s.load(Self.class, a.getId());
      
      Self b = new Self();
      b.setName("child1");
      
      a.getChildren().add(b);
      
      b = new Self();
      b.setName("child2");
      
      a.getChildren().add(b);
      
      
      
      s.update(a);
      
      s.flush();
      
      a = (Self)s.load(Self.class, a.getId());
      
      a.getChildren().remove(b);
      
      s.update(a);
      
      //creating a new parent
      a = new Self();
      
      a.setName("parent2");
      
      //adding the child2 which was removed from parent 1
      a.getChildren().add(b);
      
      s.save(a);
      
      s.flush();
      
      s.close();
      


I get an exception when I try to flush:
Exception in thread "main" org.hibernate.ObjectDeletedException: deleted object would be re-saved by cascade (remove deleted object from associations): [demo.Self#3]

Let me know if you are trying to do something different from the test case above. This is relating to adding the child to a different parent.

Regards,
Jitendra


Top
 Profile  
 
 Post subject:
PostPosted: Wed Jun 27, 2007 12:02 pm 
Senior
Senior

Joined: Tue Jun 12, 2007 4:49 pm
Posts: 127
Location: India
Now relating to changing order, by removing and adding to same parent.
Here is my test class with the same hbm as above:

Code:


      Self a = new Self();
      a.setName("parent");
      Session s = factory.openSession();
      s.save(a);
      
      s.flush();
      
      a = (Self)s.load(Self.class, a.getId());
      
      Self b = new Self();
      b.setName("child1");
      
      a.getChildren().add(b);
      
      Self c = new Self();
      c.setName("child2");
      
      a.getChildren().add(c);
      
      
      
      s.update(a);
      
      s.flush();
      
      a = (Self)s.load(Self.class, a.getId());
      
      a.getChildren().remove(b);
      
      s.update(a);
      
      
      //adding the child2 which was removed from parent
      a.getChildren().add(b);
      
      s.update(a);
      
      s.flush();
      
      s.close();
      
   


I can see the changes in the db and log is as follows:
Code:
INFO: schema export complete
Hibernate: select max(id) from Self
Hibernate: insert into Self (name, parent_id, id) values (?, ?, ?)
Hibernate: insert into Self (name, parent_id, id) values (?, ?, ?)
Hibernate: insert into Self (name, parent_id, id) values (?, ?, ?)
Hibernate: update Self set parent_id=?, idx=? where id=?
Hibernate: update Self set parent_id=?, idx=? where id=?
Hibernate: update Self set parent_id=null, idx=null where parent_id=? and id=?
Hibernate: update Self set parent_id=?, idx=? where id=?
Jun 27, 2007 9:29:58 PM org.hibernate.impl.SessionFactoryImpl close


Let me know if its the same as your scenario.

Regards,
Jitendra
*Please do give rating if it helps*


Top
 Profile  
 
 Post subject:
PostPosted: Wed Jun 27, 2007 2:06 pm 
Newbie

Joined: Wed Feb 21, 2007 6:29 am
Posts: 5
Curiouser and curiouser...

First of all, I compiled your code and it does indeed throw an exception. Why this doesn't happen in my production code is something I will investigate further (my production code is running in a J2EE environment, with JTA transactions and getCurrentSession() etc. so it may have something to do with the environment). This is probably some kind of bug. I'll do some more testing and come back to you on that.

However I still find this behaviour VERY strange. How come

Code:
parent1.getChildren().remove(a);
parent2.getChildren().add(b);


throws an exception but

Code:
parent1.getChildren().remove(a);
parent1.getChildren().add(b);


does not? This looks very inconsistent to me. Since Hibernate can check whether the object has been added to the same parent before declaring it an orphan, shouldn't it check whether it's been added to another parent as well?

Even crazier, when I just do

Code:
parent2.getChildren().add(b);


WITHOUT first removing it from parent1, I get something very inconsistent: during the same session, child1 belongs to both parents. Loading it in another session, however, it only belongs to parent2. This inconsistency between saved state and in-memory persistent object (within a transaction, no less) also smells of buggishness and could be very dangerous.

This of course all stems from trying to define a one-to-many association in the database through a many-to-many (List/Collection property) data structure in memory. If this was an unordered collection (Set) I'd follow your initial suggestion and maintain the association via the "parent" property instead of the "children" property, however with indexed/keyed collections this means manually maintaining the indices/keys. To summarise:

1. Is this inconsistent behaviour worthy of a bug report/request for enhancement in JIRA so that Hibernate can keep track of the collections a child belongs to? In an "ideal world" I'd expect adding a child to a different parent to automagically remove it from another?

2. Without any changes to hibernate, I have three options:

a. Leave things as they are, but be unable to reparent objects (not acceptable)

b. Don't use cascade="delete-orphan" and manually delete orphans (what I've done in my code right now... it works, but I've lost some very useful functionality)

c. Set the list to inverse="true", expose the index as a general property of the object and manage the association through setParent() and setParentIndex(). If I do this, can I still set cascade="all-delete-orphan"? Will that delete any objects where I do setParent(null)?


Top
 Profile  
 
 Post subject:
PostPosted: Thu Jun 28, 2007 2:18 pm 
Senior
Senior

Joined: Tue Jun 12, 2007 4:49 pm
Posts: 127
Location: India
With:
Code:
parent1.getChildren().remove(a);
parent2.getChildren().add(b);


I guess you meant:
Code:
parent1.getChildren().remove(a);
parent2.getChildren().add(a);


This throws exception because hibernate will first delete the object and then try to save an object which has been deleted(or marked for deletion), so it believes something is wrong and hence throws up. Kind of logical, if you look at a general picture instead of this specific case.

Again, with:
Code:
parent1.getChildren().remove(a);
parent1.getChildren().add(b);


I guess you meant:
Code:
parent1.getChildren().remove(a);
parent1.getChildren().add(a);


Here the relationship between objects is not changed only the value in index column has chanaged, so if you look at the log on an update query is fired. *Hibernate is intelligent* isn't it.


Code:
parent2.getChildren().add(b);


This means that the child has been associated with two parents, as it was not removed from the parent1, so the objects in session reflect the same thing. Its only when you flush that the data is moved to the db and refreshed back will you see that the db and object model are in sync.

Seems like you are very very interested in raising a bug :-).. chill its a beautiful app that you have at hand.. learn to think in hibernate and life will be very comfortable. Hibernate is designed to closely resemble java object model, so mostly behaves in similar manner. And, since there is no listner on the objects in hibernate, you cannot expect it to come into picture as soon as you modify the object. Hibernate comes into picture when you have flushed the session. So the best thing in your case would be to use the session for short durations and keep flushing ASAP. That would ensure that there is not a lot of difference between db and objects in memory at any point of time.

There is one more thing you can try.When moving the child from one parent to another i.e. "re-parenting", set the id of the object to '0'. This will tell hibernate that its a new object and it won't complain that you are trying to re-save. This is logical since its a 1-to-many relationship and the children have no existence without the parent, so anyone refering to the child should know that it has been removed.

Its been long long discussion, I guess I should get full 3 credits. Buzz me js_iitk in yahoo for more discussion if you would like.

Regards,
Jitendra


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.