Static metamodel in JPA

on waitingforcode.com

Static metamodel in JPA

Entity modeling looks like the most important and the single meta data associated with a Java class in JPA. But it's not alone. Static metamodels are a meta data describing entity too. Unless, they are used in different purposes.

Current article will introduce us to the world of static metamodels. In the first part, we'll approach this concept theoretically by explaining its two main features and components. Two next parts will detail each of mentioned purposes. They will be illustrated by sample codes.

What is static metamodel in JPA ?

At the begin, we need to understand what is and why static metamodel was invented. We can define it as a simple class which fields describe entity. Practically, what does it mean in the code side ? Imagine we have following entity:

@Entity
@Table(name="products")
public class Product {

  private Integer id;
  private String name;

  public void setId(Integer id) {
          this.id = id;
  }

  @Id
  @GeneratedValue(strategy = IDENTITY)
  @Column(name="id")
  public Integer getId() {
    return this.id;
  }
      
  public void setName(String name) {
    this.name = name;
  }
  
  @Column(name="name")
  @NotEmpty
  public String getName() {
    return this.name;
  }

  @Override
  public String toString() {
    return "Product {"+this.name+"}";
  }
        
}

This entity has two fields: id and name. Its static metamodel will also have two fields, but they won't be defined as String or Integer, but as types interpreted by JPA. Take a look on it:

@StaticMetamodel(Product.class)
public class Product_ {
  public static volatile SingularAttribute<Product, Integer> id;
  public static volatile SingularAttribute<Product, String> name;	
}

Note that this file is called Product_.java. The trailing underscore in this name is a standard defined in official Java's JPA tutorial.

Static metamodel's components are placed in javax.persistence.metamodel package. First, it contains the attributes class used to represent entity fields. We can use the classes for single fields, as Long, String, Integer (for example: SingularAttribute) and so on, or the classes to translate collections from entity to its static metamodel (for example: SetAttribute, MapAttribute, ListAttribute or CollectionAttribute). All listed elements ends with Attribute suffix and they are superinterfaces of javax.persistence.metamodel.Attribute. They all have two generics <X, Y>, where X means the entity class and Y the class of mapped field. You can observe this dependency at our sample, where Product represents entity class and Integer with String its fields (respectively, id and name).

The static metamodels are loaded automatically by persistence provider on entity manager factory creation. All static metamodels must be accessible on the classpath.

We can also implement static metamodel components as a kind of "JPA reflection API". The basic interface that allows us to navigate through metamodels of all persistent entities, is javax.persistence.metamodel.Metamodel. Although, let's take a more detailed look on this feature of static metamodels.

Static metamodel purposes: JPA's "reflection API"

@Test
public void testReflection() {
  EntityManagerFactory emFactory = Persistence.createEntityManagerFactory("sampleUnit");
  EntityManager entityManager = emFactory.createEntityManager();
  Metamodel metamodel = entityManager.getMetamodel();
  
  Map<String, String> attrTypeMap = new HashMap<String, String>();
  attrTypeMap.put("id", "public java.lang.Integer com.mysite.data.Product.getId()");
  attrTypeMap.put("name", "public java.lang.String com.mysite.data.Product.getName()");
  
  EntityType<Product> productEntity =  metamodel.entity(Product.class);
  System.out.println("Persistent entity is: "+productEntity.getName());
  for (Attribute<?, ?> attribute : productEntity.getAttributes()) {
    assertTrue(attribute.getName() + " shouldn't be read by metamodel because it's not defined in Product entity", 
      attrTypeMap.containsKey(attribute.getName()));
    assertTrue("Expected attribute type '"+attrTypeMap.get(attribute.getName())+"' was not found. '"+attribute.getJavaMember() + "' was found instead", 
      attribute.getJavaMember().toString().equals(attrTypeMap.get(attribute.getName())));
    attrTypeMap.remove(attribute.getName());
  }
  assertTrue("No attributes to analyze should left but "+attrTypeMap.size() +" remains to analyze", 
    attrTypeMap.size() == 0);
  
  List<String> managedTypes = new ArrayList<String>();
  managedTypes.add("class com.mysite.data.Product");
  ManagedType productType = metamodel.managedType(Product.class);
  int index = managedTypes.indexOf(productType.getJavaType().toString());
  assertTrue("Managed type '"+productType.getJavaType() + " was not found in test List", index > -1);
  
  boolean notManagedDetected = false;
  try {
    ManagedType<NotManagedProduct> notManaged = metamodel.managedType(NotManagedProduct.class);
  } catch (Exception e) {
    notManagedDetected = true;
  }
  assertTrue("NotManagedProduct should be detected as not managed by entity manager, but it wasn't", notManagedDetected);
}

private class NotManagedProduct {
}

Our test case consists on checking if the properties of Product class are all correctly interpreted by JPA. Firstly, we check if given entity, got with metamodel.entity method, has all expected attributes. We see that Attribute's getJavaMember method returns all field definition. In this case, it's a definition of getter annotated with @Column annotation. Because some of attributes can be more complicated, Attribute's interface define two methods that returns a boolean to indicate if, given attribute is a collection (isCollection method) and if it's used as a association field (annotated with XToMany or XToOne annotation, isAssociation method).

In the second part of this test, we check if Product is a class managed by entity manager. The last check is similar to previous one, but it verifies if NotManagedProduct class is correctly interpreted as a class not managed by entity manager.

Static metamodel purposes: JPA criteria

Thanks to previous sample we can observe that static metamodel allows us to iterate through all managed entities nearly as API reflection allows to iterate through Java classes properties. But this possibility of analyze it's not a single feature of JPA static metamodel. Static datamodel can also be used with JPA's Criteria.

We don't focus on explaining Criteria here. You just need to know that is a way of defining JPA queries in programmatic way. One of next articles will cover this subject more in details. Instead, we'll focus on showing static metamodel use in Criteria queries through below code:

@Test
public void test() {
  EntityManagerFactory emFactory = Persistence.createEntityManagerFactory("sampleUnit");
  EntityManager entityManager = emFactory.createEntityManager();

  // first remove all peans and add a new pea to could execute query constructed with CriteriaBuilder
  entityManager.getTransaction().begin();
  Product pea = new Product();
  pea.setName("pea");
  entityManager.persist(pea);
  entityManager.getTransaction().commit();

  // construct query with CriteriaBuilder
  CriteriaBuilder builder = emFactory.getCriteriaBuilder();
  CriteriaQuery<Product> query = builder.createQuery(Product.class);
  Root<Product> product = query.from(Product.class);
  query.where(builder.equal(product.get(Product_.name), "pea"));
  TypedQuery<Product> typedQuery = entityManager.createQuery(query);
  Product peaDb = typedQuery.getSingleResult();
  assertTrue("Pea should be found in the database but it wasn't", 
    peaDb != null);
  assertTrue("Retreived product should be a 'pea' but was '"+peaDb.getName()+"'", 
    peaDb.getName().equals("pea"));
}

Compared with the previous sample, the begin of this code doesn't change. We start by creating entity manager. After we prepare data in test database. We need only one product with the name "pea", so we insert it. Now, this is the most important part. We create query to get just inserted product with JPA Criteria API. First, we tell that we want to prepare a query against Product's entity table. After we finally use static metamodel.

You can see the use of it in query.where() method where builder uses attributes from Product's metamodel to get specific row. Thanks to it the query is considered as type safe. It means that the query won't be executed with values that don't match to types specified in static metamodel. For example, our case expects an instance of String to be able to execute the query. But if we replace "pea" String by "new Product()", the query will throw an IllegalArgumentException:

java.lang.IllegalArgumentException: Unaware how to convert value 
[Product {null} : com.waitingforcode.data.Product] to requested type [java.lang.String]
  at org.hibernate.ejb.criteria.ValueHandlerFactory.unknownConversion(ValueHandlerFactory.java:258)
  at org.hibernate.ejb.criteria.ValueHandlerFactory.access$000(ValueHandlerFactory.java:34)
  at org.hibernate.ejb.criteria.ValueHandlerFactory$IntegerValueHandler.convert(ValueHandlerFactory.java:135)
  at org.hibernate.ejb.criteria.ValueHandlerFactory$IntegerValueHandler.convert(ValueHandlerFactory.java:122)
  at org.hibernate.ejb.criteria.ValueHandlerFactory.convert(ValueHandlerFactory.java:288)
  at org.hibernate.ejb.criteria.predicate.ComparisonPredicate.(ComparisonPredicate.java:70)
  at org.hibernate.ejb.criteria.CriteriaBuilderImpl.equal(CriteriaBuilderImpl.java:392)
  at com.waitingforcode.jpa.CriteriaTest.test(CriteriaTest.java:88)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
  at java.lang.reflect.Method.invoke(Unknown Source)
  at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
  at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
  at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
  at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
  at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
  at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
  at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
  at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
  at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
  at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
  at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
  at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
  at org.junit.runners.ParentRunner.run(ParentRunner.java:300)

This article explains an interesting feature of JPA specification, a static metamodels. This kind of "mirror" classes reflect the properties of entities. Thanks to it, we can easily analyze the entities managed by entity manager and theirs constructions. We can use static metamodels in queries based on JPA's Criteria API too. The metamodels ensure the type safety of generated queries by throwing an IllegalStateException when an inappropriate object is used.

Share on: