wtorek, 28 września 2010

Lazy one-to-one inverse relationships in Hibernate

Many people are surprised when lazy loading of inverse (not owning) optional one-to-one relationships using Hibernate does not work for them out of the box.

Let's start with a short example:

@Entity
public class Person {
 private Animal animal;

 @OneToOne(optional = false)
 public Animal getAnimal() {
  return animal;
 }

 public void setAnimals(Animal animal) {
  this.animal = animal;
 }
}

@Entity
public class Animal {
 private Person owner;

 @OneToOne(fetch = FetchType.LAZY, optional = true, mappedBy = "animal")
 public Person getOwner() {
  return owner;
 }

 public void setOwner(Person owner) {
  this.owner = owner;
 }
}

Many developers expect that owner property of the Animal entity will not be loaded until it is accessed for the first time. This is true but only for relationships where the proxy object for the other end of the relationship can be used. In our example it won't work.

There are at least three well known solutions for this problem:
  1. The simplest one is to fake one-to-many relationship. This will work because lazy loading of collection is much easier then lazy loading of single nullable property but generally this solution is very inconvenient if you use complex JPQL/HQL queries.
  2. The other one is to use build time bytecode instrumentation. For more details please read Hibernate documentation: 19.1.7. Using lazy property fetching. Remember that in this case you have to add @LazyToOne(LazyToOneOption.NO_PROXY) annotation to one-to-one relationship to make it lazy. Setting fetch to LAZY is not enough.
  3. The last solution is to use runtime bytecode instrumentation but it will work only for those who use Hibernate as JPA provider in full-blown JEE environment (in such case setting "hibernate.ejb.use_class_enhancer" to true should do the trick: Entity Manager Configuration) or use Hibernate with Spring configured to do runtime weaving (this might be hard to achieve on some older application servers). In this case @LazyToOne(LazyToOneOption.NO_PROXY) annotation is also required.

But what if you don't want to modify the structure of your entities and don't want or cannot use bytecode instumentalization (there are some issues related to this).

There is one more undocumented solution. It requires some modifications in the entities code but thanks to this building and deployment process can remain untouched.

The idea is to fool Hibernate that the entity class which we want to use has been already instrumented. To do this your entity class has to implement FieldHandled or InterceptFieldEnabled interface.

@Entity
public class Animal implements FieldHandled {
 private Person owner;
 private FieldHandler fieldHandler;

 @OneToOne(fetch = FetchType.LAZY, optional = true, mappedBy = "animal")
 @LazyToOne(LazyToOneOption.NO_PROXY)
 public Person getOwner() {
  if (fieldHandler != null) {
   return (Person) fieldHandler.readObject(this, "owner", owner);
  }
  return owner;
 }

 public void setOwner(Person owner) {
  if (fieldHandler != null) {
   this.owner = fieldHandler.writeObject(this, "owner", this.owner, owner);
   return;
  }
  this.owner = owner;
 }

 public FieldHandler getFieldHandler() {
  return fieldHandler;
 }

 public void setFieldHandler(FieldHandler fieldHandler) {
  this.fieldHandler = fieldHandler;
 }
}

If you are using javassist as bytecode provider (default from Hibernate version 3.3.0.CR2) implement org.hibernate.bytecode.javassist.FieldHandled and if you are using CGLib implement net.sf.cglib.transform.impl.InterceptFieldEnabled.

Getters and setters for non-lazy properties requires no changes.

The last thing worth mentioning here is that Hibernate does not support one by one lazy properties loading. This means that if your entity class has more then one lazy properties all of them are going to be loaded during the first access to any of them (I know that this is stupid but this is how it is currently implemented).

17 komentarzy:

  1. Hi,
    I think Hibernate (3.3) uses runtime class enhancement for @ManyToOne by default, see http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html section 19.1.3.

    OdpowiedzUsuń
  2. Fantastic, it works like a charm for reverse @OneToOne relationships. And it works for lazy loading of plain values, like

    @Basic(fetch = FetchType.LAZY)
    @Column(length = 4000)
    private String note;

    And I consider this solution better than any build time instrumentation. I wonder, why something like this is not an "official" Hibernate solution.

    OdpowiedzUsuń
  3. I had a little problems with updating of such detached values. Prior to Entitymanager.merge(...), I have to load the existing lazy values, so as the Hibernate may recognize a possible change.

    OdpowiedzUsuń
  4. yes,it works,but the Animal cannot be updated

    OdpowiedzUsuń
  5. This is amazing. I have been looking for a solution for ages for this problem. Hates off for a great solution. In the past to get around this issue I have changed the @OneToOne relationship to a @OneToMany, I know very ugly but it works!!! Also enhancing the byte-code could not work for me; the ant script took for ever so I ended up killing it. And even if it worked somebody may forget to do the enhancement if it is not well documented once the code is in production!!!

    I am wondering if you or anybody else reading this blog have experienced any side affect to this solution?

    OdpowiedzUsuń
    Odpowiedzi
    1. Hi,
      Regarding the side affects, I am facing a problem to load those entities (the one's that I marked as lazy).

      When I query the Animal entity and setting the fetch mode of the owner to be JOIN, the owner member (on the animal entity) remains to be null (although the join clause appears on the SQL and I can see on the log that the owner entity was materialized).

      I still didn't solve this issue, please reply if you have a solution.

      Usuń
  6. Wielkie dzięki, mała rzecz a cieszy :)

    OdpowiedzUsuń
  7. Approach with implementing FieldHandled works like a charm, event with lazy loading properties, not only associations @OneToOne or else. I had a problem on merge too, but I solved it using custom UserType - I extended Hibernate PrimitiveByteArrayBlobType and overide the isEqual() method, this is how I avoided this problem. But ofcourse it is acceptable solution only for not updatable fields. You can insert the new value, but you cannot update it, or you will need to load the value from database before updating, so this can be done too.

    OdpowiedzUsuń
  8. This is not working for me. Could you guide me if i am missing anything?

    public class PackageEntityInfo extends UpdateTracker implements Serializable, FieldHandled {
    ....
    @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY, optional=true)
    @LazyToOne(LazyToOneOption.NO_PROXY)
    @JoinTable(name = "PACKAGE_ENTITY_RATING_REL", joinColumns = @JoinColumn(name = "PACKAGE_ENTITY_ID"), inverseJoinColumns = @JoinColumn(name = "RATING_ID"))
    private RatingInformation ratingInformation;
    ...

    }

    OdpowiedzUsuń
  9. Does not work if the entity has more than one one-to-one relationship.
    How is that to be handled?

    Thanks, Bala R

    OdpowiedzUsuń
    Odpowiedzi
    1. For more than one relationship it works exactly the same. When calling readObject() and writeObject() methods you just need to pass different property names for different relationships.

      Usuń
  10. Hi,
    This pattern is excellent for not loading the XToOne relations, but when I query the Animal entity and setting the fetch mode (of the owner member) to be: 'join' (in order to load the owner), it is not assigned on the animal entity:
    Criteria query = Session.createCriteria(Animal.class);
    query.setFetchMode("owner",FetchMode.JOIN);
    List lst = query.list();

    I do see that the query is OK (means, I see the join on the SQL) and I see the the owner is materialized but it is not set on the animal.

    Any inputs will be appreciated (please do send some inputs).

    OdpowiedzUsuń
    Odpowiedzi
    1. I want to rephrase,
      1) Is there a way to initialize the owner without invoking the getOwner() method?
      2) In case that there is an animal without owner, when the getOwner() is invoked, another SQL is executed (although, I the previous query used a join to the parent table. Is there a way to avoid this extra join?

      Usuń
  11. Hi,

    I have used above sample and worked very well in most of the cases thanks a lot. But I am facing inconsistent exception as well:

    com.myapp.shape.ui.exception.DataIngestionException
    at com.myapp.shape.ui.data.DataIngestionJob.importMif(DataIngestionJob.java:242)
    at com.myapp.shape.ui.service.impl.DataIngestionServiceImpl$DataImportProcess.processJob(DataIngestionServiceImpl.java:488)
    at com.myapp.shape.ui.service.impl.DataIngestionServiceImpl$DataImportProcess.run(DataIngestionServiceImpl.java:462)
    at java.lang.Thread.run(Thread.java:722)
    Exception in thread "Thread-7" javax.persistence.RollbackException: Error while committing the transaction
    at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:93)
    at com.myapp.shape.ui.service.impl.DataIngestionServiceImpl.readJob(DataIngestionServiceImpl.java:539)
    at com.myapp.shape.ui.service.impl.DataIngestionServiceImpl$DataImportProcess.processJob(DataIngestionServiceImpl.java:511)
    at com.myapp.shape.ui.service.impl.DataIngestionServiceImpl$DataImportProcess.run(DataIngestionServiceImpl.java:462)
    at java.lang.Thread.run(Thread.java:722)
    Caused by: javax.persistence.PersistenceException: org.hibernate.PropertyAccessException: Exception occurred inside getter of com.myapp.shape.data.dao.Place.placeShape
    at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1214)
    at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1147)
    at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:81)
    ... 4 more
    Caused by: org.hibernate.PropertyAccessException: Exception occurred inside getter of com.myapp.shape.data.dao.Place.placeShape
    at org.hibernate.property.BasicPropertyAccessor$BasicGetter.get(BasicPropertyAccessor.java:175)
    at org.hibernate.envers.entities.mapper.MultiPropertyMapper.mapToMapFromEntity(MultiPropertyMapper.java:107)
    at org.hibernate.envers.synchronization.work.CollectionChangeWorkUnit.generateData(CollectionChangeWorkUnit.java:56)
    at org.hibernate.envers.synchronization.work.AbstractAuditWorkUnit.perform(AbstractAuditWorkUnit.java:72)
    at org.hibernate.envers.synchronization.AuditProcess.executeInSession(AuditProcess.java:114)
    at org.hibernate.envers.synchronization.AuditProcess.doBeforeTransactionCompletion(AuditProcess.java:152)
    at org.hibernate.engine.ActionQueue$BeforeTransactionCompletionProcessQueue.beforeTransactionCompletion(ActionQueue.java:543)
    at org.hibernate.engine.ActionQueue.beforeTransactionCompletion(ActionQueue.java:216)
    at org.hibernate.impl.SessionImpl.beforeTransactionCompletion(SessionImpl.java:571)
    at org.hibernate.jdbc.JDBCContext.beforeTransactionCompletion(JDBCContext.java:250)
    at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:138)
    at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:76)
    ... 4 more
    Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.GeneratedMethodAccessor285.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.hibernate.property.BasicPropertyAccessor$BasicGetter.get(BasicPropertyAccessor.java:172)
    ... 15 more
    Caused by: org.hibernate.HibernateException: entity is not associated with the session: null
    at org.hibernate.persister.entity.AbstractEntityPersister.initializeLazyProperty(AbstractEntityPersister.java:806)
    at org.hibernate.intercept.AbstractFieldInterceptor.intercept(AbstractFieldInterceptor.java:97)
    at org.hibernate.intercept.javassist.FieldInterceptorImpl.readObject(FieldInterceptorImpl.java:105)
    at com.myapp.shape.data.dao.Place.getPlaceShape(Place.java:153)

    Any suggestions...

    Regards,
    Ambrish Bhargava

    OdpowiedzUsuń
  12. Hi Paweł
    Great article. Your workaround seems to work for me. My only question is what is it doing? How does the FileHandler fit into the hibernate life cycle? Can you explain how we are tricking hibernate here?

    Thanks

    OdpowiedzUsuń
  13. The FieldHandled works to solve the problem of lazy, but does not work when I need to update the property that implements FieldHandled. Any solution?

    Hibernate 4.2.6.Final

    Thanks

    OdpowiedzUsuń
  14. I'm facing the same problem. I can´t update(merge) the property that implements FieldHandled.

    Hibernate 4.2.2

    OdpowiedzUsuń