Monday, March 12, 2012

JPA 2 Metamodels and Hibernates @Any

The Java Persistence API 2 integrates many features and annotations that where vendor specific extensions in JPA 1. One interesting feature that isn't yet integrated into JPA 2 is Hibernates @Any annotation for non-polymorphic associations. Currently there exist some problems with any-typed associations in combination with JPA Metamodels and the new Criteria API. This article describes the problem and a solution.

Hibernates @Any

Associations of type org.hibernate.type.AnyType exist since Hibernate 3.0. They can be used to model associations to multiple entity types without a common ancestor entity (non-polymorphic).

You can configure Hibernates O/R mapping through classic hbm.xml mapping files or through Hibernate Annotations which consist out of JPA annotations and Hibernate-specific annotation extensions. The AnyType has no counterpart in JPA 1/2 and up to Hibernate 3.3.1.GA you could configure this feature only through XML mappings. Since then the Hibernate-specific annotation @Any exists (another explanation).

As example we use a persistent business log that can contain references to entities as additional information:
@Entity
@Getter
@Setter
public class Blog extends BaseEntity {

 private static final long serialVersionUID = -2669494633145498742L;

 @ManyToOne
 private User user;

 @NotNull
 private Date date;

 @NotNull
 @Column(length = Number.SIZE31)
 @Enumerated(EnumType.STRING)
 private BlogCategorie category;

 @Size(max = Number.SIZE255)
 private String message;

 // no JPA metamodel for @Any!
 @Any(metaColumn = @Column(name = "ref_disc"))
 @AnyMetaDef(idType = "long", metaType = "string", metaValues = {
   @MetaValue(targetEntity = User.class, value = "U"),
   @MetaValue(targetEntity = Event.class, value = "E") })
 @JoinColumn(name = "ref_id")
 private BaseEntity ref;

 @PostLoad
 @SuppressWarnings("unused")
 private void postLoad() {
  // default mode fetch = EAGER doesn't work for @Any in Hibernate <= 4.1.1
  this.ref.getId();
 }


 // or as example for many to any associations
 @ManyToAny(metaColumn = @Column(name = "ref_disc"))
 @AnyMetaDef(idType = "long", metaType = "string", metaValues = {
   @MetaValue(targetEntity = User.class, value = "U"),
   @MetaValue(targetEntity = Event.class, value = "E") })
 @JoinTable(inverseJoinColumns = @JoinColumn(name = "blog_id"), joinColumns = @JoinColumn(name = "ref_id"))
 private List<BaseEntity> refs;

}
In this case you often see this exception at application deployment:
ERROR [org.jboss.msc.service.fail] (MSC service thread 1-2) MSC00001: Failed to start service jboss.persistenceunit."app.war#database": org.jboss.msc.service.StartException in service jboss.persistenceunit."app.war#database": Failed to start service
 at org.jboss.msc.service.ServiceControllerImpl$StartTask.run(ServiceControllerImpl.java:1767) [jboss-msc-1.0.2.GA.jar:1.0.2.GA]
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110) [rt.jar:1.7.0_03]
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603) [rt.jar:1.7.0_03]
 at java.lang.Thread.run(Thread.java:722) [rt.jar:1.7.0_03]

Caused by: java.lang.UnsupportedOperationException: any not supported yet
 at org.hibernate.ejb.metamodel.AttributeFactory.determineAttributeMetadata(AttributeFactory.java:431)
 at org.hibernate.ejb.metamodel.AttributeFactory.buildAttribute(AttributeFactory.java:91)
 at org.hibernate.ejb.metamodel.MetadataContext.wrapUp(MetadataContext.java:185)
 at org.hibernate.ejb.metamodel.MetamodelImpl.buildMetamodel(MetamodelImpl.java:64)
 at org.hibernate.ejb.EntityManagerFactoryImpl.<init>(EntityManagerFactoryImpl.java:91)
 at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:904)
 at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:889)
 at org.hibernate.ejb.HibernatePersistence.createContainerEntityManagerFactory(HibernatePersistence.java:73)
 at org.jboss.as.jpa.service.PersistenceUnitServiceImpl.createContainerEntityManagerFactory(PersistenceUnitServiceImpl.java:162)
 at org.jboss.as.jpa.service.PersistenceUnitServiceImpl.start(PersistenceUnitServiceImpl.java:85)
 at org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1811) [jboss-msc-1.0.2.GA.jar:1.0.2.GA]
 at org.jboss.msc.service.ServiceControllerImpl$StartTask.run(ServiceControllerImpl.java:1746) [jboss-msc-1.0.2.GA.jar:1.0.2.GA]
 ... 3 more

JPA 2 Metamodel and the Criteria API

Since version 3.5 the Hibernate EntityManager tries to automatically generate a JPA 2 Metamodel and this fails for @Any attributes. This generation is enabled by default but can be disabled in the META-INF/persistence.xml:
<property name="hibernate.ejb.metamodel.generation" value="disabled" />
The Metamodel is necessary for the new Criteria API in JSF 2 (see next section) or you get such exceptions:
ERROR [app.controller.util.exception.ExceptionHandler] (http--127.0.0.1-8082-1) null: java.lang.NullPointerException
 at org.hibernate.ejb.criteria.QueryStructure.from(QueryStructure.java:136) [hibernate-entitymanager-4.1.1.Final.jar:4.1.1.Final]
 at org.hibernate.ejb.criteria.CriteriaQueryImpl.from(CriteriaQueryImpl.java:177) [hibernate-entitymanager-4.1.1.Final.jar:4.1.1.Final]
Currently it isn't possible to represent a proper Metamodel for the Hibernate-specific @Any attribute. So you had to decide if you like to use the Criteria API or the @Any annotation.

Hibernate 4.1 to the rescue!

Hibernate 4.1 has a new default mode - enabled Metamodel generation which ignores unsupported attributes
(see EntityManagerFactoryImpl.JpaMetaModelPopulationSetting.IGNORE_UNSUPPORTED): 
"ignore unsupported"
So now you can use both features together by default: Criteria API and @Any annotations.

Beware: You cannot use such @Any attributes in Criteria API queries because the method Path.get("attributeName") internally uses the generated Metamodel to resolve the Entity attributes. Hence this isn't possible:
  final CriteriaBuilder cb = this.em.getCriteriaBuilder();
  final CriteriaQuery<Blog> cq = cb.createQuery(Blog.class);
  final Root<Blog> blog = cq.from(Blog.class);
  cq.select(blog).where(cb.equal(blog.<User> get("ref"), user));
  final Query q = this.em.createQuery(cq);
  ... q.getResultList();
But it's possible to use the Criteria API for JPA attribute types and JPA QL for @Any references like this:
final Query q = this.em.createQuery("SELECT b FROM Blog b WHERE b.ref = :ref");
q.setParameter("ref", user);
... q.getResultList();
In this case you should use a named query. You can also translate Criteria Queries into Hibernate or native SQL queries, append the ref-restriction and execute this. Example how to get native SQL from Criteria Query in Hibernate:
q.unwrap(org.hibernate.ejb.QueryImpl.class).getHibernateQuery().getQueryString()

You might also be interested in