Using Transient Entity in Hibernate to Update/Merge an existing Persistent Object
Asked Answered
K

2

8

I am dealing with a fairly complex object graph in my database. I am using XStream to serialize and deserialize this object graph which works fine. When I import an object graph of an object that exists in the database, it is initially transient, since there are no IDs and hibernate knows nothing of it. I then have business logic which sets IDs on parts of my object graph by figuring out which objects in the newly transient imported object map to existing persistent objects. I then use Hibernate's merge() and saveOrUpdate().

Some pseudocode to give you a better idea of what I'm doing:

ComplexObject transObj = xstream.import("object.xml");
ComplexObject persistObj = someService.getObjByName(transObj.getName());
for (OtherObject o : c.getObjects()) {
    if (persistObj.getObjects().contains(o.getName())) {
        o.setId(persistObj.getObjectByName(o.getName()).getId())
    }
    ... set a bunch of other IDs deeper in the object graph ...
}

transObj = session.merge(transObj);
session.saveOrUpdate(transObj);

Now this doesn't work as I get errors such as:

   org.springframework.dao.InvalidDataAccessApiUsageException: deleted object would be re-saved by cascade (remove deleted object from associations): [com.......SomeObject#353296]; nested exception is org.hibernate.ObjectDeletedException: deleted object would be re-saved by cascade (remove deleted object from associations): [com.......SomeObject#353296]

and it seems like hibernate merge was not meant for associating transient objects to persistent ones.

Is there any way to achieve what I want to do without having to get the persistent object in session, and modifying that, instead of modifying the transient one, and trying to save that and override the existing persistent one?

Kirima answered 15/3, 2013 at 14:54 Comment(6)
That's how I would have solved it: #4779739 -- for performance reasons you should either cache the reflection/introspection data or create a class generator from your entities.Upcountry
Having a setId is always a bad idea... ?Haphazardly
user1050755, the only thing about that solution is that it does not seem to be able to handle complicated cascades, one-to-many's, etc. I imagine this would only work for a non-graph type structure, but correct me if I'm wrong.Kirima
Can you not do this the other way round? I mean first load the object(s) from Hibernate, and then write a custom marshaller from XStream?Installment
That's exactly what I'm trying to avoid. That might be possible, but then it becomes a maintenance nightmare any time the data model changes and it would be difficult to do due to the graph-like nature of the data.Kirima
If one of A's was good for you, could you accept it? Q is still open.Nekton
N
8

it seems like hibernate merge was not meant for associating transient objects to persistent

Merge is JPA standard - merges "new & detached entity instances" to "(persistence context-) managed entity instances". Will cascade across FK relationships marked Cascade.MERGE or Cascade.ALL.

From a JPA perspective - no, merge was not meant for associating your Xstream streamed transient objects to persistent. JPA intends for merge to work with the normal JPA lifecycle - create new objects, persistent them, get/find them, detach them, modify them (including adding new objects), merge them, optionally modifiy them some more, then persist/save them. This is deliberate design, so that JPA is stream-lined & performant - each individual object persist to the database doesn't need to be preceded with a retrieval of object state in order to determine whether/what to insert/update. The JPA persistence context object state already contains enough details to determine this.

Your problem is that you have new entity instances that you want to act as if they have been detached - and that's not the JPA way.

SaveOrUpdate is a hibernate proprietary operation - if an entity has an identity it is updated, but if it has not entity then it is inserted.

From a hibernate perspective - yes, merge followed by saveOrUpdate can theoretically work for associating (Xstream streamed) transient objects to persistent, but it may have restrictions when used in conjuction with JPA operations. saveOrUpdate DOES precede each object persist to the database with a retrieve to determine whether/what to insert/update - it's smart, but it's not JPA and it's not the most performant. i.e. you should be able to get this to work with some care and the right configuration - and using hibernate operations rather than JPA operations when conflict occurs.

I get errors such as:

org.hibernate.ObjectDeletedException: deleted object would be re-saved by cascade (remove deleted object from associations): [com.......SomeObject#353296]

I believe this might be caused by two factors:

  • somewhere in your (Xstream-created) object graph, a child entity instance is missing that would be present if you did a (cascaded) retrieve using JPA: this gives an object deletion
  • somewhere else in you (Xstream-created) object graph, this same child entity is present: this gives the object being re-saved

    How to debug this: (a) create the object graph from Xstream and print it out IN FULL - every entity, every field; (b) load the same object graph via JPA, cascading the retrieve from the top entity & print it out IN FULL - every entity, every field (c) compare the two - is something missing from (a) that is present in (b)??

Is there any way to achieve what I want to do without having to get the persistent object in session, and modifying that, instead of modifying the transient one, and trying to save that and override the existing persistent one?

Via the debug/fix just suggested - hopefully.

OR my brute-force suggestion to (dumbly) side-step this bug (slows performance a little, though): after you populate IDs in the object graph, call EntityManager.clear(), THEN proceed with merge() and saveOrUpdate(). But UNIT TEST the DB results - to ensure you haven't overlooked something important in populating your Xstream graph.

For testing / as a last resort, try the saveOrUpdate() without the merge(), but you might then be forced to clear() the Entity manager to avoid Hibernate exceptions, such as NonUniqueObjectException.

Let me know if you learn more / have more info B^)

Nekton answered 18/3, 2013 at 5:9 Comment(2)
I've already tried to do the clear(), and the saveOrUpdate() without the merge. They yield other hibernate errors... I think such as entity copy already exists and transient object instantiation. I will give your other suggestions a try. Thank you for the thorough answer.Kirima
How would you "print" the entire object? That is essentially the original problem I was working on, which is where XStream came in.Kirima
F
1
    Hibernate merge was not meant for associating transient objects 
to persistent ones.

The merge() should ideally work for you because you are actually dealing with detached object. Since you are setting ids in the 'transObj' before calling merge, hibernate would consider them as detached (not transient).

I think the problem with your code is that it is tripping hibernate.

In your code, you are loading the 'persistObj' from the database. Now, hibernate holds this 'persistObj' in session. You are then setting some of the ids from 'persistObj' in 'transObj' and then calling merge. Some of the child objects in 'persistObj' and 'transObj' have the same ids, hibernate gets confused.

ComplexObject transObj = xstream.import("object.xml");
ComplexObject persistObj = someService.getObjByName(transObj.getName());
for (OtherObject o : c.getObjects()) {
    if (persistObj.getObjects().contains(o.getName())) {
        o.setId(persistObj.getObjectByName(o.getName()).getId())
     }
    ... set a bunch of other IDs deeper in the object graph ...
}
transObj = session.merge(transObj);
session.saveOrUpdate(transObj);

Try calling the the session.clear() after loading 'persistObj', so that hibernate removes the persistObj and considers only your detached object.

ComplexObject transObj = xstream.import("object.xml");
ComplexObject persistObj = someService.getObjByName(transObj.getName());
// Clear the session, so that hibernate removes 'persistObj' from it's cache
session.clear();
for (OtherObject o : c.getObjects()) {
    if (persistObj.getObjects().contains(o.getName())) {
        o.setId(persistObj.getObjectByName(o.getName()).getId())
     }
    ... set a bunch of other IDs deeper in the object graph ...
}
transObj = session.merge(transObj);
session.saveOrUpdate(transObj);
Facsimile answered 18/3, 2013 at 18:6 Comment(10)
If you check the comment I put on Glen's answer, I did try using clear initially before merge() and saveOrUpdate() but it tended to bring up other problems. I will try it again though and come up with a clearer response as to why it did not work or at least some error messages. Thanks.Kirima
Sorry, did not see the other comment. Yes, pls post the errors you see with clear(). I would definitely not have the 'persistObt' and 'tranObj' attached to the same session because you are trying to represent the same database row using two different instances of the object. Hibernate normally does not allow this.Facsimile
I just tried this again. I ran clearSession(), merge(), then saveOrUpdate(). Like in my original question, I'm getting the deleted object would be resaved error.Kirima
May I ask where you have placed 'session.clear()'. Is it immediately after loading 'persistObj'?Facsimile
I called session.clear() immediately before calling session.merge().Kirima
Is the merge() causing the exception or is it saveOrUpdate() ?Facsimile
I think on the saveOrUpdate() but I'll check when I'm back at work tomorrow. It just says proxy and doesn't give a line number so I'll have to debug line by line. Everything is transactionally wrapped so it happens at the end of my method I think.Kirima
On my merge(), I am getting java.lang.IllegalStateException: An entity copy was already assigned to a different entity. This is despite calling clearSession() right before the merge.Kirima
Looks like hibernate still has some kind of reference to the old object despite calling session.clear(). I would suggest that you refactor your code such that you load 'persistObj' in one hibernate session. Use 'persistObj' to populate your ids outside a transaction scope. And then call 'saveOrUpdate' (no merge) in a new hibernate session.Facsimile
Yeah, that seems to be the case. I will give that a shot.Kirima

© 2022 - 2024 — McMap. All rights reserved.