Why is java.util.Map a managed type for Hibernate when entity is audited?

If I list the managed types of Hibernate, when having only registered a single entity, I get that entity. As expected.

If I put a @Audited annotation on it I get two more managed types:

class org.hibernate.envers.DefaultRevisionEntity As expected.
And interface java.util.Map which seems really weird.

Why is Map considered a managed type?

@vlad mentioned on https://stackoverflow.com/q/51518740/66686 that this might be a bug and recommended to ping @Naros

This causes problems in Spring Data JPA as described here: https://jira.spring.io/browse/DATAJPA-1384 The issue also contains an example project.

Adding a test like this:

@Autowired
private EntityManager em;

@Test
public void mapIsNotAManagedType_IsIt(){

	Set<ManagedType<?>> types = em.getMetamodel().getManagedTypes();

	types.forEach(x -> System.out.println(x.getJavaType()));

	assertThat(types).extracting(ManagedType::getJavaType).doesNotContain((Class)Map.class);
}  

Demonstrates the behavior without Spring Data involvement.

might be a bug and recommended to ping Naros

While this may be a bug, it isn’t directly related to any Envers.

The reason this occurs when a user uses Envers is because the audit framework relies heavily on a specific mapping mode offered by Hibernate ORM called dynamic-map. Essentially, all the audit mappings are type-less and therefore Hibernate treats the data as map-of-maps.

Why is Map considered a managed type?

I think the team agrees that when a dynamic-map mapping is in play, there is no java-type. In fact, we’ve specifically made changes in the run-time and meta model for Hibernate 6 that supports this view.

But as for 5.x, I want to look back at what we did specifically when we released 5.0. Has the meta model always dictated that for dynamic-map mappings, the java-type is java.util.Map? If it has, then I’m a bit reluctant to change that behavior in 5.x as that could have an impact on other applications.

I’ll get back to you.

So I think this definitely a bug, but not necessarily from the same perspective you have.

I took a very simple audited entity here (not fancy at all)

@Audited
@Entity
public class SimpleEntity {
  @Id
  @GeneratedValue
  private Integer id;
}

In 5.0, the meta-model generated consisted of 4 managed types

ManagedType EntityName JavaType MapType Origin
Entity SimpleEntity com.package.SimpleEntity POJO ORM
Entity DefaultRevisionEntity o.h.envers.DefaultRevisionEntity POJO Envers
Entity SimpleEntity_AUD null MAP Envers
Embeddable n/a java.util.Map MAP Envers

This makes sense because the first two rows are actual mapped entities and the last two represent the map mode data for the audited entity SimpleEntity_AUD and its associated embeddable identifier.

Due to recent changes to the meta-model generation, 5.3/5.4 now only show 3 managed types

ManagedType EntityName JavaType MapType Origin
Entity SimpleEntity com.package.SimpleEntity POJO ORM
Entity DefaultRevisionEntity o.h.envers.DefaultRevisionEntity POJO Envers
Embeddable n/a java.util.Map MAP Envers

This leads me to a series of questions

  1. Why is the SimpleEntity_AUD entity managed type no longer present?
  2. If (1) is intended, then the associated embeddables for that parent entity-type should not exist either.

While this highlights some recent differences in the meta-model generation step, I still need a clearer understanding from spring-data’s perspective why this is even problematic.

We try to use the list of managed types to decide if we need to pass a target class to the JPA implementation when executing a query coming from a @Query annotation.

If the return type of the method is a managed type we assume the user wrote the query in such a way that JPA returns that entity.
If not we request a Tuple and construct the result ourself.

Map suddenly appearing in the managed types makes Map-returning-methods behave differently (and actually fail) depending on the usage of Envers.

I guess on our side we do have a couple of options to fine tune that check:

  • only consider ManageTypes that have a JavaType and an EntityName
  • or just those with origin ORM. Depending on what is available in the API
  • special handling for Maps

come to my mind.

I guess the real problem is that I so far didn’t find a clear definition what being a managed type even mean exactly. Preferably that would come from the JPA spec of course.

Then wouldn’t the following make more sense here for what you’re trying to accomplish?

List<ManagedType<?>> types = em.getMetamodel()
  .getManagedTypes()
  .stream()
  .filter( x -> { return x.getPersistenceType().equals( PersistenceType.ENTITY ) && x.getJavaType() != null; } )
  .collect( Collectors.toList() );

This would basically work for all 5.x releases.

This effectively eliminates the Map embeddable because you filter based solely on ENTITY and then it also eliminates the dynamic-map entity types which have no java-type.

As a follow-up, in 5.0 / 5.1, we built the metamodel slightly different than we did in 5.2+ when we merged the JPA artifact as a part of Hibernate Core. Prior to 5.2, users could use the following to filter the Envers obtains from the metamodel if they wished:

hibernate.ejb.metamodel.population=ignoreUnsupported

If you set that setting to enabled, then the metamodel was populated with dynamic-map objects. But I believe even when using the ignoreUnsupported setting, this ultimately lead to the 5.2+ outcome where the dynamic-map entity type was filtered but any embeddable-type that had that entity-type as a parent was not.

So for 5.2+ we have 2 options:

  1. Exclude the embeddables owned by a dynamic-map entity-type which is also excluded.
  2. Include dynamic-map entity-types like we did prior to 5.2.

As a workaround, i used setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP), it makes query return list of maps. That method is deprecated though, AFAIK, there is no recommended replacement.

That’s what I basically ended up implementing.

Thanks.

@schauder, after discussing this internally we agree that we need to revert this behavior to the 5.1 and prior way of dealing with dynamic-map based entity types where the configuration settings influence how the metamodel is to be built. So ultimately we’ll be going with (2) and will be handled in HHH-12871.