Hibernate lazy loading for reverse one to one workaround - how does this work?

RNJ picture RNJ · Jun 17, 2013 · Viewed 8.9k times · Source

I was having problems today with lazy loading not working when using a mapped by collection. I found this excellent article that seems to fix the problem

http://justonjava.blogspot.co.uk/2010/09/lazy-one-to-one-and-one-to-many.html

One thing I do not understand is how the workaround using FieldHandled works. Can anyone help me understand this? The code in question is below (copied from the example on the link):

@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;
   }
}

What am I missing? Perhaps I dont know enough about hibernate's lifecycle here? Im happy to investigate but can anyone give me some pointers.

Thanks in advance.

EDIT

I pushed through a lot of changes so a lot of my entities implemented FieldHandled but then discovered some of my tests were failing. I pumped out the SQL and got some weird things where the SQLs were happening in different orders if this interface was implemented with just these methods set.

   public FieldHandler getFieldHandler() {
     return fieldHandler;
   }

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

This was causing tests to fail as things were not quite in the correct state when I was asserting. This adds to my mis-understanding of this FieldHandler variable.

Answer

lifus picture lifus · Jun 20, 2013

The following code tells Hibernate to use interception handler instead of proxy.

@LazyToOne(LazyToOneOption.NO_PROXY)

From javadoc:

give back the real object loaded when a reference is requested (Bytecode enhancement is mandatory for this option, fall back to PROXY if the class is not enhanced)

As can be seen, it's required to instrument the bytecode before using it. 'Persisted class is enhanced' after its 'bytecode is instrumented'.

The idea is to fool Hibernate that the entity class which we want to use has been already instrumented

Instrumentation task is called after code is compiled. Instrumented entity extends FieldHandled. FieldHandled is an 'Interface introduced to the enhanced class'

Hibernate verifies entity at a run time and comes to a conclusion that class was enhanced that's why it uses the real object instead of proxy and isn't loading related entity object as it normally did.

Edit:

Lets take a look under the hood:

  1. AnnotationBinder handles NO_PROXY option

    if ( lazy != null ) {
        toOne.setLazy( !( lazy.value() == LazyToOneOption.FALSE ) );
        toOne.setUnwrapProxy( ( lazy.value() == LazyToOneOption.NO_PROXY ) );
    }
    
  2. Both org.hibernate.mapping.ManyToOne and org.hibernate.mapping.OneToOne are subclasses of org.hibernate.mapping.ToOne. ToOne#isUnwrapProxy() only usage is in #getType:

    getMappings().getTypeResolver().getTypeFactory().oneToOne(

  3. Both ManyToOneType and OneToOneType are subclasses of EntityType and only usage of 'EntityType#unwrapProxy' is in EntityType#resolveIdentifier(Serializable, SessionImplementor)

    boolean isProxyUnwrapEnabled = unwrapProxy &&
            session.getFactory()
                    .getEntityPersister( getAssociatedEntityName() )
                    .isInstrumented();
    
  4. Here's Tentative call hierarchy: AbstractEntityPersister#isInstrumented() ->EntityMetamodel#isInstrumented() ->EntityInstrumentationMetadata#isInstrumented() -> etc. and finally BytecodeProviderImpl.EntityInstrumentationMetadataImpl.EntityInstrumentationMetadataImpl()

     this.isInstrumented = FieldHandled.class.isAssignableFrom( entityClass );
    

That's why it's required either instrument the code (e.g with InstrumentTask) or implement FieldHandled.


In order to make long story short you may take a look at EntityType#resolveIdentifier(Serializable, SessionImplementor). That's the reason why second object is not loaded even if it's nullable.