Thursday, September 3, 2009

How I Lost My Annotations: JPA, Hibernate and FetchType.LAZY

Many JPA applications use Java annotations on their domain model classes in order to specify validation rules. Runtime frameworks use reflection to discover these validation rules and enforce them at runtime. This all works nicely until you realize that you need FetchType.LAZY on some of your @ManyToOne associations. After specifying lazy fetching you may find that some of your annotations disappear – only not all the time. So what's going on here? This article discusses the implications of FetchType.LAZY and how to overcome this intermittent problem.

In order to implement lazy fetching under the covers Hibernate enhances your classes. What does this mean? Basically, Hibernate extends your classes and overrides its methods at runtime. It does this by using one of two bytecode manipulation libraries: Javassist or CGLib. If you ask one of these enhanced classes for its name, you'll end up with something like this:


// JPA entity class name
greensopinion.Person

// CGLib enhanced class name
greensopinion.Person$$EnhancerByCGLIB$$4ad0592d

// Javassist enhanced class name
greensopinion.Person_$$_javassist_0

Hibernate only does this when necessary. So when you get an instance of your entity it might be enhanced, and it might not – depending on how it was loaded. This can be confusing, since instances of a Person may not always be the same class. For example, code like this can be problematic:


Person person1 = entityManager.find(Person.class,id1);
Person person2 = entityManager.find(Person.class,id2);

// bad: contrary to what we'd expect, this comparison may
// be true sometimes, but not always
if (person1.getClass() == person2.getClass()) {
}
// bad: this may not work as expected either
if (person1.getClass().isAssignableFrom(person2.getClass())) {
}
// bad: again, sometimes true sometimes false
if (person1.getClass().isAssignableFrom(Person.class)) {
}
// this is ok: comparison will always be true (assuming person1 is not null)
if (person1 instanceof Person) {
}

In the above example comparisons that look straight-forward may behave differently depending on how and if these entities were already loaded by the same entity manager. This can be tricky when implementing hashCode and equals.

Furthermore, we're in for more trouble if we declare Person as follows:


@Entity
public class Person {

...

@Required
public String getFirstName() { ... }

}

Here we're using an annotation to indicate to our validation framework that the firstName property is a required field (see JSR-303, Hibernate Validation, Spring Modules, OVal). Our validation framework may do something like this:


Class clazz = entity.getClass();

// iterate on class accessors
...

if (method.getAnnotation(Required.class) != null) {
...
}

This may work well most of the time, and may work in all of our unit tests – however in the running application it may appear as if our class has lost its annotations! Why is this happening? Don't worry, your JVM isn't losing its head, or your annotations. This code may not work as expected if our entity is an enhanced version since Hibernate's ProxyFactory enhancers don't consider annotations when overriding methods on your entity.

So why use FetchType.LAZY at all? Most non-trivial JPA applications will have to make use of FetchType.LAZY in order to avoid @ManyToOne associations from causing cascading loads. Using FetchType.LAZY can also reduce the number of joins when fetching collections, which in some cases greatly improves performance.

So how to we solve this problem? Our reflection code must be aware of HibernateProxy. Here's an example of how this can be fixed:



Class clazz = PersistenceUtil.getEntityClass(entity);
... now do reflection ...

// in PersistenceUtil.java
public static Class getEntityClass(Object entity) {
if (entity instanceof HibernateProxy) {
return ((HibernateProxy)entity).getHibernateLazyInitializer().getPersistentClass();
}
return entity.getClass();
}

The intermittent and in some cases occasional nature of the symptoms involved make this problem hard to reproduce. Lazy fetching may seem like a panacea, however it's another case where leaky abstractions make our lives more challenging. As with most things in software development, it's important to understand the implementation details in order to work effectively with JPA.

4 comments:

Eric Rizzo said...

I know I'm in the minority, but this is a demonstration of why I'm not a big fan of annotations. Code is code and configuration is configuration and while I don't like editing multiple files for the same entity any more than the next guy, I like even less the idea of encountering nasty surprises like you describe. And I REALLY don't like the idea of complicating other simple code just because of this kind of nasty surprise. At that point, it seems to me that whatever benefit was gained from the use of annotations has been offset.

Olaf Bergner said...

Though in this case Hibernate's use of CGLIB/Javassist is to blame rather than the author's use of annotations. After all, it's the class hierarchy's runtime manipulation which violates the principle of least surprise, not the perfectly obvious presence of some annotations.

Oziel José said...

From what i read in the bug report, this happenas only for annotations in the method and not in the attribute itself, and thats how i use it...so its not for all of the FethType.LAZY uses, as the post implies.

Anonymous said...

Thanks.