Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Wednesday, March 09, 2011

Hibernate w/ transient objects

Am I missing something with Hibernate, or is it pretty darn hard to mix the following:

  • Session-per-request processing (the approach provided by the tapestry-hibernate module)
  • Transient objects (a wizard where a complex object is "built" across multiple request/response cycles)
  • Persistent objects (the transient keeps references to some persistent objects)

Hibernate seems to make it a bit tricky for me here. I get a lot of odd exceptions, because the new object has references and collections that ultimately point to persistent objects that are detached (their session is long gone).

I'm having to write a lot of code to reattach dependencies, just before I render the page (which traverses the transient object, eventually hitting persistent objects) and before persisting the transient object.

I'm having to iterate over various collections and a few fields and lock the object, to convert it back to a persistent object from a transient one:

    public static void reattach(Session session, Object transientObject) {
        if (transientObject != null) {
            session.buildLockRequest(LockOptions.NONE).lock(transientObject);
        }
    }
In other cases, where the transient object has a reference to an object that may already be present in the Session, I must use code like:
  category = (Session) session.get(Category.class, session.getId());

If Tapestry supported it, I suppose some of this would go away if we used a long-running Session that persisted between requests. However, that has its own set of problems, such as coordinating the lifecycle of such a session (when is it started? When is it discarded? What about in a cluster?)

My current solution feels kludgey, and not like Idiomatic Java, more like appeasing the API Gods. I'd really like to see this happen more automatically or transparently ... for instance, when persisting a transient instance, there should be a way for Hibernate to "gloss over" these detached objects and just do what I want. Perhaps its there and I'm missing it?

13 comments:

lance said...

Have you tried session.merge()?:
Session session = getHibernateSession();
Object trans = getTransientObject();
List<Object> unattachedList = getPersistantObjectsFromTransient(trans);
List<Object> mergedList = new ArrayList<Object>();
for (Object unattached : unattachedList) {
mergedList.add(session.merge(unattached));
}
doStuff(mergedList);

I'm guessing you have tried this but I've never had a trouble with merge.

Thiago H. de Paula Figueiredo said...

Hi, Howard!

The trick is to use Session.merge() before using a transient object and use the returned object from now on instead of the original one. Using the cascading for merge option in the mapping of properties which are entities or collections also helps a lot.

Hugo said...

This problem is a major hassle for many people using hibernate in a 3 tier application.

Why not implement long-running-session in Tapestry and answering your questions:
1. At app startup.
2. At app shutdown.
3. It shouldn't be a problem with long-running-session i think. Only if you support long-running-transactions should this be a problem.

Massimo said...

I don't think you're missing anything.

It's what they call "impedance mismatch" and it's a price to pay for ORM.

I've always fought against it in _any_ of my projects.

Cheers

Howard said...

@Lance

You code is pretty much what I'm doing, but I'm tending to use lock() rather than merge() when I know that there isn't already a conflicting persistent object in the current session.

@Hugo

I think it is more complex than that as the long-running Sessions need to be restricted to particular clients (and request threads).

Rob said...

Hmmm, probably not helpful here but Ebean ORM (www.avaje.org) doesn't have this type of issue.

It gets around it by having 'automatic management of the persistence context'. This means it makes use of weak references and other support mechanisms (for lazy loading etc).

This removes the whole session management drama from the developer (no attach/detach/merge etc). This also means there is no flush() but instead you explicitly save() the objects/object graphs.

Eclipselink does have the option of using a weak reference based persistence context - so might be close to doing automatic management of the pc. I don't see that feature in Hibernate though. There is also the architectural decision of storing the 'old values' for optimistic locking (ie. not storing them in the persistence context) - so IMO this is not just a matter of using weak references for the persistence context.

Certainly this is one of the main reasons for Ebean ORM to exist (to make life easier for the developer).

Hmmm, not sure if a 'sessionless ORM' perspective is useful but you never know.

Cheers, Rob.

Fiona said...

@Massimo
I don't think this is an ORM Impedance mismatch if some ORM's don't have the same issue. Granted the most well known Java ORM's are all 'session' based (Hibernate, JPA, JDO) but not all (Ebean ORM) and there is at least one .Net ORM that similarly won't have this issue.

Unfortunately many people paint all ORM's with the same problems of Hibernate and JPA.

Cheers, Rob.

Josh Long said...

Hiya Howard,

The concept you're looking for is called an extended persistence context. This is possible in Hibernate. You can see Spring Web Flow or JBoss Seam for a good implementation of the concept. As to when to start and stop - well, both SWF and Seam have explicit notions of conversations that start and stop, and the EPC is managed accordingly. It would be cool if Tapestry had it. Naturally, there are tradeoffs as you mentioned - it's an extra resource laying around even during user-wait periods. So, it definitely shouldn't be the default case.

Howard said...

@Josh,

I know that Kalle has done some work on conversational state for Tapestry; it seems like you end up with two approaches: for traditional objects, and for persistent entities. Having tapestry-hibernate manage a long-lived transaction is a strong possibility, with either method annotations and/or an API for controlling when such a long-lived Session is to be created.

Josh Long said...

btw, the concept of a stateful, versioned, persistent conversational transaction context isn't the only way to skin the 'conversation'/'wizards' cat. SWF flows optionally support flow-managed persistence, it's not required, and I think Tapestry's a MILLION times more usable / useful / scalable than JBoss Seam, anyday. :-) If you do implement it, please also ensure that it can be used to model conversational ajax interactions - so that only a section of the page need repaint to step through all the states.

Numito said...

Hi Howard,

I am surprised you encountered this problem only now. Since I am using tapestry and hibernate together 6 years ago I stumbled into this problem.
It took me a long time to figure it out. And your problems will get even worst when dealing with concurrent user:
1/ if you link a transient object A to a persistent one B, and at the same time you load an object C that is linked to the same persistent object B, you will get an exception when you try to save A.
2/ if another user loads an object A and changes it, you have in the meantime attached a transient object B to object A, when you save it will fail. This can be seen as a feature or also pain in the a...

The solution is to add a "smart lock" in your DAO, with the following logic : try to lock the object, is the object is already loaded due to an alternate dependency/connection, load it by it's id

private HibernateObject lockOrLoad(HibernateObject obj) {
try {
Session session = daoFactory.getSession();
synchronized (session) {
// locking the object, if the obj is already in the session,
// does nothing else lock it
session.lock(obj, LockMode.NONE);
}
} catch (Exception ex) {
// object is not in the session any more, try to load it and get a correct reference
try {
obj = (HibernateObject) daoFactory.getSession().load(
Hibernate.getClass(obj), getId(obj));
} catch (Exception e) {
LOG
.error(
"tried to load an object that does not exist any more ",
ex);
}
}
return obj;
}

A problem with this approach is that you need to keep track of all references, which is a pain if like me your objects have a lot of connections!!!!!

If you don't re-lock object on each page view, you might not be able to navigate your graph and get all kind of crazy exception!?


Another option is to load your object graph at the start of your wizard, keep the full graph in the session, never try to reconnect it to the session and at the end of the wizard, when you when to save your object call saveOrUpdate.


But this is one of the pitfall's of hibernate !? You cannot really use the full power of the object graph, or you have to make sure you load everything and you avoid lazy loading.

Cheers,

Numa

Kalle Korhonen said...

Web conversations doesn't mean or require the use of long running transactions. The latter may be useful in some cases, but typically, it just complicates things and make it even harder to scale. The issue is that it's difficult to know when the (persistence) session or transaction is abandoned. IMHO, web conversations are most useful in the lifespan of a few seconds and mainly for better managing the (servlet) session for you - and that's how http://tynamo.org/tapestry-conversations+guide approaches the issue. Wizards with known stages, where the expected lifespan is in the order of a few minutes, are best implemented by storing the state of the wizard database at each commit point. Howard is on the right track with using locking; that's the only known way to attach a previous detached object in Hibernate. Understand the semantics of merge() - in most cases you really don't want a new object but you want to use the same object, and often you really really want to "save over" even if somebody else has modified the same object in between. Like Howard, I would really love all of this to just work. Especially the Hibernate API makes it all too error-prone. Good tip on Ebean ORM, must check that out.

Ernie said...

Were you able to find a less hacky solution to this?