Monday 7 April 2014

Hibernate: Beware of OneToOne Mappings

Problem Background

So recently I had to deal with a performance issue at my job.  Its a simple JSP page, and it returns maybe a 1000 or so record.

Simple.  Or so I thought.  I can write a select query in SQL and return all the data it needs that will take almost no time to run.

So why is it that my application (backed by Hibernate) takes 10sec to load this page?

After a bit of digging and profiling we found the culprit.  It was a query written in HQL that seems to do more or less exactly what my SQL query was doing.  However, what it was also doing was about 4 or 5 extra follow up queries for EVERY SINGLE result that was returned in the first query.
This is a well known problem in the hibernate world, its known as the "N+1 Select" problem.

So what causes this problem and how to work around it?

Cause : OneToOne Mapping

OneToOne mapping are dangerous if you don't know what hibernate is doing in the background.
Lazy fetching does NOT work with entities which are mapped as OneToOne when the relationship is optional.

I.e. the following will NOT be using lazy fetch:
@OneToOne(fetch = FetchType.LAZY, optional = true)
@OneToOne(fetch = FetchType.LAZY)  //default optional is true.

So if you have an HQL, that brings back an Entity that has a optional OneToOne mapping to another entity, after executing the original HQL it will issue a separate query for every single result returned in your query for every single optional OneToOne entity that your result references.

Example:
class A {
      @OneToOne(fetch = FetchType.LAZY)
      B b;

      @OneToOne(fetch = FetchType.LAZY)
       C c;

      @OneToOne(fetch = FetchType.LAZY)
      D d;
}

If I have class A above and I write a query that brings back 1000 A entity, it will issue 1000 separate queries to fetch B, 1000 queries to fetch C, 1000 queries to fetch D.   That's 3001 queries all together, instead of just the 1!

Why?
When your OneToOne mapping is optional i.e. (the relationship is really 1 : 0/1), how does hibernate know whether to return null or a proxy object when you try to access this entity?  Hibernate must issue select query to the database to see if the object exists, and if it has to issue a query, it might as well just return the object, so the object is always fetched.

Tip No.1: If you OneToOne mapping is NOT optional set optional = false.

This will hint to hibernate that this object will always exist, and if I am lazily fetching this, you can just create a proxy for it.

Optional OneToOne

As mentioned above, hibernate needs to issue a query to figure out whether an child entity is null when you have a OneToOne mapping which is optional effectively preventing lazy loading.  So what if my OneToOne mapping REALLY is optional, am I stuck with this N+1 problem?

Option 1 : Fake One To Many

One way to work around it is to use a "fake OneToMany" mapping instead.
This is a pretty simple trick, instead of using OneToOne, you use OneToMany, and you change the field type to a Set

Hibernate has no problem lazy loading collection because a collection is never null, the collection can be empty or it can have some data in it, but the collection is never null.

To prevent breaking your existing code, you must now also change the IMPLEMENTATION of you GETTERS and SETTERS.

For example:

public YourType getA(){
      return this.a;
}

public void setA(YourType a){
    this.a = a;
}

change it to:

public YourType getA(){
      if (a.isEmpty()){
            return null;   //This will ensure the code behave the same as before.
      }
      return this.a.iterator().next();  //Remember "a" is now a Set
}

public void setA(YourType a){
    this.a.clear();
    this.a.add(a);
}

The above changes should ensure that your exist code does not break (unless you are doing direct field access, or reflection stuff.. then all bets are off!)

Option 2 : Use JOIN FETCH in your query

Another option (if you problem is specifically related to specific HQL query which you are trying to tune), is to perform a JOIN FETCH on those OneToOne entities.

Your HQL query might look something like this:
" FROM com.example.A AS A "

change it to:

" FROM com.example.A AS A "
+ "LEFT JOIN FETCH A.b AS B "
+ "LEFT JOIN FETCH A.c AS C "
+ "LEFT JOIN FETCH A.d AS D "

The above changes should prevent it from issuing further queries to fetch the Bs, Cs, and Ds, it will fetch all the information it needs in one query.

Problem with Subclass and Instanceof

So we were implementing the changes recommended above, and one of the problem that we hit is when you specify a non-optional OneToOne mapping to an abstract super class.

Prior to telling hibernate that the OneToOne mapping is non-optional, hibernate issues query to bring back the actual object and in doing so it will always return the concrete sub class (hibernate figures out the correct sub class by doing a left outer join to all sub class tables).  However, once I added the "optional = false" argument to the mapping annotation, hibernate started creating proxy for the super class (which is correct because its now correctly lazy loading your OneToOne mapping!), but its a problem if you are using the instanceof operator on the proxy field!

Example:

abstract class Drink {}

class Coke extends Drink{}

class Milk extends Drink{}

class Fridge{
      @JoinColumn(name = "drinkId", nullable = false)
      @OneToOne(fetch = FetchType.LAZY, optional = false)
      Drink drink;

      Drink getDrink(){
     }
}

When I have code like the following:

Fridge fridge = .... //some code that gives me my fridge.
if (fridge.getDrink() instanceof Milk){
     // you got milk!
}  else {
    // you got coke!
}

This code would fail, where it wouldn't before!  Before the change fridge.getDrink() would return either a Coke or Milk, because its actually returning the actual object in the database.  After the change, fridge.getDrink() actually returns an javaassist proxy to Drink!

If you step through the code in the debugger, you will see that fridge.getDrink() is actually returning something like com.example.Drink_$$_javassist_1, its neither a Milk or Coke! Its hibernate generated proxy class, which means your instanceof check will fail!  If you try to cast it to a Milk or Coke, you will get a ClassCastException!

So after a bit of googling the way we found to overcome this problem is to write a util method which will convert a hibernate proxy object to the actual implementation.

 public static T initializeAndUnproxy(final T entity) {
      if (entity == null) {
          return null;
      }

      Hibernate.initialize(entity);
      if (entity instanceof HibernateProxy) {
           return (T) ((HibernateProxy) entity).getHibernateLazyInitializer().getImplementation();
     } else {
          return entity;
     }
 }


With this method you can do the following:

Fridge fridge = .... //some code that gives me my fridge.
Drink drink = initializeAndUnproxy(fridge.getDrink());

if (drink instanceof Milk){
     // you got milk!
}  else {
    // you got coke!
}


No comments:

Post a Comment