Monday, May 23, 2011

Detaching Hibernate Objects to pass to GWT

One thing we encountered pretty quickly when we started our GWT work was the fact that you can't serialize objects over the wire from server to GWT client if those objects were obtained via a Hibernate/JPA entity manager.

If you've ever worked with Hibernate/JPA, you'll know that when you get back entity POJOs whose fields are not loaded (i.e. marked for lazy loading and you didn't ask for the data to be loaded), your entity POJO instance will have Hibernate proxies where you would expect a "null" object to be (this is to allow you to load the data later, if your object is still attached to the entity manager session).

Having these proxies even after leaving a JPA entity manager session is a problem in the GWT world because the GWT client sitting in your browser doesn't have Hibernate classes available to it! Trying to send these entity POJO instances that have references to Hibernate proxies causes serialization errors and your GWT client will fail to operate properly.

This is a known issue and is discussed here.

We pretty quickly decided against using DTOs. As that page above mentioned, "if you have many Hibernate objects that need to be translated, the DTO / copy method creation process can be quite a hassle". We have a lot of domain objects that are used server side in RHQ. There was no reason why we shouldn't be able to reuse our domain objects both server side and client side - introducing DTOs just so we could workaround this serialization issue seemed ill-advised. It would have just added bloat and unnecessary complexity.

I can't remember how mature the Gilead project was at the time we started our GWT work, or maybe we just didn't realize it existed. Gilead does require you to have your domain objects and server side impl classes extend certain Java classes (LightEntity for example), so it has a slight downside that it requires you to modify all your domain objects. In any event, we do not use Gilead to do this detaching of hibernate proxies.

RHQ's solution was to write our own "Hibernate Detach Utility". This is a single static utility that you use to process your objects just prior to sending them over the wire to your GWT client. Essentially it scrubs your object of all Hibernate proxies, cleaning it such that it can be serialized over the wire successfully.

We also used this when we originally developed a web services interface to the RHQ remote API.

Here is the HibernateDetachUtility source code in case you are interested in seeing how we do it - maybe you could use this in your own GWT/Hibernate application. I think it is reuseable - not much custom RHQ stuff is going on in here.

11 comments:

  1. Can You please write a simple How To.. so we can use the functionality?

    ReplyDelete
  2. Sure teneke. Just to summarize - the purpose here is to support the use-case where you have server-side code that is about to send an object (or collection of objects) to a remote client that doesn't have Hibernate in its classpath (such as a GWT client running in a browser). In such a case, you wouldn't even be able to deserialize the objects on the remote client, because it couldn't load any Hibernate proxy classes that are in the objects.

    To solve this, your server-side code can call this to "scrub" your object(s) of all Hibernate proxy objects and effectively null out all fields that weren't loaded by Hibernate:

    HibernateDetachUtility.nullOutUninitializedFields(object, HibernateDetachUtility.SerializationType.SERIALIZATION);

    At this point, "object" has been scrubbed and you can send it on its way to the client - any fields not loaded by Hibernate will appear null (you won't get LazyInitializeExceptions because the Hibernate proxy is gone). It's really that simple.

    ReplyDelete
  3. First of all, thank you very much for your extremely useful blog and class.

    I have spent a lot of time, and this blog definitely have gave me some light.

    I'm trying to reuse your HibernateDetachUtility class and I'm getting the following error:

    java.lang.NoSuchFieldException: id
    at java.lang.Class.getDeclaredField(Class.java:1882)
    at org.rhq.enterprise.server.safeinvoker.HibernateDetachUtility.nullOutFieldsByFieldAccess(HibernateDetachUtility.java:538)
    at org.rhq.enterprise.server.safeinvoker.HibernateDetachUtility.nullOutFieldsByFieldAccess(HibernateDetachUtility.java:460)
    at org.rhq.enterprise.server.safeinvoker.HibernateDetachUtility.nullOutUninitializedFields(HibernateDetachUtility.java:432)
    at org.rhq.enterprise.server.safeinvoker.HibernateDetachUtility.nullOutUninitializedFields(HibernateDetachUtility.java:160)
    at com.ejc.sadi.server.remote.AuditTemplateServiceImpl.sendreceive(AuditTemplateServiceImpl.java:52)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at com.google.gwt.user.server.rpc.RPC.invokeAndEncodeResponse(RPC.java:569)
    at com.google.gwt.user.server.rpc.RemoteServiceServlet.processCall(RemoteServiceServlet.java:208)
    at com.google.gwt.user.server.rpc.RemoteServiceServlet.processPost(RemoteServiceServlet.java:248)
    at com.google.gwt.user.server.rpc.AbstractRemoteServiceServlet.doPost(AbstractRemoteServiceServlet.java:62)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:637)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
    at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:487)
    at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:362)
    at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
    at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
    at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:729)
    at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
    at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
    at org.mortbay.jetty.handler.RequestLogHandler.handle(RequestLogHandler.java:49)
    at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
    at org.mortbay.jetty.Server.handle(Server.java:324)
    at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:505)
    at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:843)
    at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:647)
    at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:211)
    at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:380)
    at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:395)
    at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:488)
    ERROR (org.rhq.enterprise.server.safeinvoker.HibernateDetachUtility:551) - No id constructor and unable to set field id for base bean com.ejc.sadi.domain.NaveCat
    java.lang.NoSuchFieldException: id
    at java.lang.Class.getDeclaredField(Class.java:1882)
    at org.rhq.enterprise.server.safeinvoker.HibernateDetachUtility.nullOutFieldsByFieldAccess(HibernateDetachUtility.java:538)

    ReplyDelete
  4. I adjust your class and it seems like is working now, tomorrow I will do more tests with more complicated nested classes.

    ReplyDelete
  5. I'm curious what your solution is for passing data back to the server. Since you have altered your entity POJOs you must be doing some sort of merge or are creating a brand new object to persist.

    ReplyDelete
    Replies
    1. That's right. On the server, you'll need to use the Entity Manager to find/attach or merge those detached POJOs that the client sent in, but that's what you normally have to do anyway when a remote client sends any detached entity POJO into your server layer. That's the typical Hibernate/EJB3 model.

      Delete
    2. I previously used Gilead to serialize and de serialize objects between the client and the server using persistentBeanManager. No that I'm using this class I'm not able to merge the objects on the server. I have to treat them as all new objects. My Ui showed the change but my database did not show the change until I wrapped the update or save around a transaction. Can you provide an example of how to find and attach the object after its passed back to the server?

      Delete
  6. This is a really nice piece of code, you definitely saved me a big hassle. I was using Gilead for this purpose but there is a major problem with Gilead... object references are NOT maintained. So let's say you have a List of objects which all point back to a single object (named ObjectA), Gilead will re-create the list of objects with each item pointing to a separate instance of ObjectA. This of course can get really terrible if ObjectA has a List of more objects which point to an Object. Essentially Gilead will exponentially create objects instead of maintaining the reference to a single copy of the original.

    One thing to note for the other devs who use this class. Right around line 481 (in the version I'm looking at) there is a clause like this:

    if (fieldValue != null
    && (fieldValue.getClass().getName().contains("org.rhq") || fieldValue instanceof Collection
    || fieldValue instanceof Object[] || fieldValue instanceof Map))
    nullOutUninitializedFields((fieldValue), checkedObjects, checkedObjectCollisionMap, depth + 1,
    serializationType);

    Developers will need to change the 'org.rhq' into whatever package contains the objects that should be serialized. If you do not do that then some of the classes in the hierarchy won't be cleansed.

    ReplyDelete
  7. Also one more mod I needed to make after testing... fields only present in the super class were not being set/cleansed. The reason is due to how setField is working (two methods at the bottom of the class).

    object.getClass().getDeclaredField(fieldName) will only find the fields in the subclass.. I switched over to a Spring utility instead via:
    ReflectionUtils.findField(value.getClass(), fieldName);

    ReplyDelete
  8. I have one question for anyone that might come across this. This certainly seems like a viable solution, however how do you trigger a get of the lazily loaded member of the parent entity when you do need it (server side)? Do you simply do a .size() (or whatever) to trigger the load? I always felt like that was such a hack. I have been experimenting with RequestFactory and the requestFactory.blahRequest.findAll.with(lazilyLoadedItems) syntax in the client. Unfortunately this doesn't influence what happens with the SQL server-side but only what is serialized to the client. I found a solution using a filter to extend the life of the EntityManager throughout the entirety of the RequestFactory request, thus enabling the .with(lazilyLoadedItems) to affect what is actually queried server side. It's similar to the ubiquitous "OpenEntityManagerInView" solution which in and of itself feels like a bit of a hack. I'm not sure which approach is superior.

    ReplyDelete
  9. Following up on the comment by "Java", I had the same problem because a Hibernate object did not use "id" as the name of the identifier property. I have modified the code to use Hibernate's built-in mapping to figure out the id. It falls back to the old mechanism, if necessary.


    if (replacement == null) {

    String className = ((HibernateProxy) fieldValue).getHibernateLazyInitializer().getEntityName();

    //see if there is a context classLoader we should use instead of the current one.
    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

    try {
    Class clazz = contextClassLoader == null ? Class.forName(className) : Class.forName(className,
    true, contextClassLoader);

    final ClassMetadata classMetadata = HibernateRunner.getSessionFactory().getClassMetadata(clazz);
    String idPropertyName = classMetadata.getIdentifierPropertyName();
    if (idPropertyName == null) {
    idPropertyName = FALLBACK_DEFAULT_ID_PROPERTY_NAME;
    }
    final Field idField = clazz.getDeclaredField(idPropertyName);
    if (idField == null) {
    // See also: BaseIdObject
    Class[] constArgs = {Integer.class};
    Constructor integerConstructor = clazz.getConstructor(constArgs);
    if (integerConstructor == null) {
    throw new RuntimeException("Could not find Hibernate mapped Id property nor " +
    "an \"" + FALLBACK_DEFAULT_ID_PROPERTY_NAME + "\" property, " +
    "and could not find a constructor that takes " + Arrays.toString(constArgs));
    }
    replacement = integerConstructor.newInstance((Integer) ((HibernateProxy) fieldValue)
    .getHibernateLazyInitializer().getIdentifier());
    } else {
    Constructor ct = clazz.getDeclaredConstructor();
    if (ct == null || !ct.isAccessible()) {
    throw new RuntimeException("Could not find public no-args constructor on " +
    className);
    }
    replacement = ct.newInstance();
    if (!idField.isAccessible()) {
    idField.setAccessible(true);
    }
    idField.set(replacement, ((HibernateProxy) fieldValue)
    .getHibernateLazyInitializer().getIdentifier());
    }
    } catch (Exception e) {
    LOG.log(Level.SEVERE, "Failed to create an instance of pojo " + className + " for an " +
    object.getClass() + "#" + field + " so the value will be null.", e);
    }
    field.set(object, replacement);
    }

    ReplyDelete