Hibernate Envers : polymorphism issues

1.6k views Asked by At

In my domain model, I have an abstract entity Indicator inherited by two concrete Elementary and Composite.
The abstract entity hold an entity Factor in order to be accessible for the two sub-classes.
The relation is bi-directional. So the entity Factor hold an instance of the abstract entity Indicator.
Of course, the real instance is either Elementary or Composite.

@Entity  
@Inheritance(strategy = JOINED)  
public abstract class Indicator implements Serializable {  
    @OneToMany(mappedBy = "indicator")  
    private List<Factor> factors = new ArrayList<Factor>();  
    ...  
}

@Entity
@Audited
public class Factor implements Serializable {
    @ManyToOne(optional = false)
    @JoinColumn(name = "ID_RSK_IND", nullable = false)
    @ForeignKey(name = "FK_FAC__IND")
    private Indicator indicator;
}

@Entity
@Audited
public class Elementary extends Indicator {
    ...
}

@Entity
@Audited
public class Composite extends Indicator {
    ...
}

I use Dozer to map these entities with themselves in order to "break" the hibernate instrumentation and push them on client side (GWT).

Whith "classic" Hibernate, all works fine : Dozer cross the beans model to duplicate it.

But, when I use the Envers AuditReader for querying versioned entities, I get an InstantiationException. It come from the fact that the instance of Factor try to instantiate an instance of Indicator

09:36:04,702 - ERROR - org.dozer.MappingProcessor - Field mapping error -->
  MapId: null
  Type: null
  Source parent class: com.sg.rrf.l2r.shared.entity.market.indicator.elementary.Elementary
  Source field name: factors
  Source field type: class org.hibernate.envers.internal.entities.mapper.relation.lazy.proxy.ListProxy
  Source field value: [1]
  Dest parent class: com.sg.rrf.l2r.shared.entity.market.indicator.elementary.Elementary
  Dest field name: factors
  Dest field type: java.util.List
org.dozer.MappingException: java.lang.InstantiationException
    at org.dozer.util.MappingUtils.throwMappingException(MappingUtils.java:82)
    at org.dozer.factory.ConstructionStrategies$ByConstructor.newInstance(ConstructionStrategies.java:280)
    at org.dozer.factory.ConstructionStrategies$ByConstructor.create(ConstructionStrategies.java:245)
    at org.dozer.factory.DestBeanCreator.create(DestBeanCreator.java:65)
    at org.dozer.MappingProcessor.mapCustomObject(MappingProcessor.java:489)
    at org.dozer.MappingProcessor.mapOrRecurseObject(MappingProcessor.java:446)
    at org.dozer.MappingProcessor.mapFromFieldMap(MappingProcessor.java:342)
    at org.dozer.MappingProcessor.mapField(MappingProcessor.java:288)
    at org.dozer.MappingProcessor.map(MappingProcessor.java:248)
    at org.dozer.MappingProcessor.map(MappingProcessor.java:197)
    at org.dozer.MappingProcessor.mapCustomObject(MappingProcessor.java:495)
    at org.dozer.MappingProcessor.mapOrRecurseObject(MappingProcessor.java:446)
    at org.dozer.MappingProcessor.addOrUpdateToList(MappingProcessor.java:776)
    at org.dozer.MappingProcessor.addOrUpdateToList(MappingProcessor.java:850)
    at org.dozer.MappingProcessor.mapListToList(MappingProcessor.java:686)
    at org.dozer.MappingProcessor.mapCollection(MappingProcessor.java:541)
    at org.dozer.MappingProcessor.mapOrRecurseObject(MappingProcessor.java:434)
    at org.dozer.MappingProcessor.mapFromFieldMap(MappingProcessor.java:342)
    at org.dozer.MappingProcessor.mapField(MappingProcessor.java:288)
    at org.dozer.MappingProcessor.map(MappingProcessor.java:248)
    at org.dozer.MappingProcessor.map(MappingProcessor.java:197)
    at org.dozer.MappingProcessor.map(MappingProcessor.java:187)
    at org.dozer.MappingProcessor.map(MappingProcessor.java:124)
    at org.dozer.MappingProcessor.map(MappingProcessor.java:119)
    at org.dozer.DozerBeanMapper.map(DozerBeanMapper.java:120)
    at com.sg.rrf.l2r.server.audit.AuditTransactionalBean.getEntityForRevision(AuditTransactionalBean.java:30)
    at com.sg.rrf.l2r.server.audit.AuditTransactionalBean$$FastClassByCGLIB$$78958945.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:713)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:52)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:52)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:58)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:646)
    at com.sg.rrf.l2r.server.audit.AuditTransactionalBean$$EnhancerByCGLIB$$36312869.getEntityForRevision(<generated>)
    at com.sg.rrf.l2r.server.audit.AuditServiceImpl.getEntityForRevision(AuditServiceImpl.java:37)
    at com.sg.rrf.l2r.server.market.indicator.audit.IndicatorAuditServiceImplTest.assertElementaryValues(IndicatorAuditServiceImplTest.java:120)
    at com.sg.rrf.l2r.server.market.indicator.audit.IndicatorAuditServiceImplTest.testAuditElementary(IndicatorAuditServiceImplTest.java:105)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:232)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:175)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
Caused by: java.lang.InstantiationException
    at sun.reflect.InstantiationExceptionConstructorAccessorImpl.newInstance(InstantiationExceptionConstructorAccessorImpl.java:30)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
    at org.dozer.factory.ConstructionStrategies$ByConstructor.newInstance(ConstructionStrategies.java:276)
    ... 74 more

Does it come from the fact that Envers use Lazy loading even when Eager is specified ?

PS : Of course, I need the bi-directional navigation from Indicator to Factor.

2

There are 2 answers

2
Angular University On BEST ANSWER

The field factors was attempted to be mapped to a field factors of a a new object of type Elementary, but the property here is a List interface for which no concrete type is known.

The mapping of this field works for actual domain model types, but not for hibernate proxies.

Do you initialize factors with an ArrayList? It seems so otherwise the non Envers mapping would not work.

This could have to do with some limitations that dozer has in mapping generic types such as lists, due to the fact that generic information is not available at runtime, so dozer does not known the type of the objects that a list contains, so it tries to infer it from the contents of the source collection.

According to the Dozer documentation:

If a Hint is not specified for the destination field, then the destination Collection will be populated with objects that are the same type as the elements in the src Collection.

So to solve this there are several ways:

1 - Put a dozer hint on the mapping of this property to specify the target type, that way it will not try to infer it:

<field>
  <a>factors</a> 
  <b>factors</b> 
  <b-hint>your.target.class.Here</b-hint> 
</field>

2 - write and apply to this property a Dozer custom converter, where you map this list manually, this would always work (use the custom converter API that is generics based).

3 - Avoid the need for mapping and Dozer altogether, by solving the LazyInitialization at serialization time in another way: ensure the hibernate session is kept open all the way up to the serializing of the request, by using Open Session In View if in a Spring application, or similar if otherwise.

One of these ways should solve it, if still in doubts you can always:

  • post the dozer mapping and the code for the type of factors?

  • With the debugger can you put a breakpoint in ConstructionStrategies line 280 to see what is the abstract class or interface that it's trying to instantiate.

0
user3154016 On

Here is my Dozer mapping:

protected final static DozerBeanMapper MAPPER = new DozerBeanMapper();
static {
    BeanMappingBuilder builder = new BeanMappingBuilder() {
        @Override
        protected void configure() {
            mapping(Elementary.class, Elementary.class);
            mapping(Composite.class, Composite.class);
        }
    };

    MAPPER.addMapping(builder);
}

Thanks to this mapping, Dozer can map the Hibernate proxified beans.
But With this mapping, Dozer can't map the Envers proxified beans.

I don't think that the issue is due to the Collection mapping. When I put a breakPoint on the Instanciation, I'm in the org.dozer.factory.ConstructionStrategies.newInstance(Class clazz) method. The clazz parameter is Indicator.class.
Moreover, suppose I don't use Dozer and I call this code in my DAO :

Indicator indicator = elementary.getFactors().get(0).getIndicator();
System.out.println(indicator.getClass().getSimpleName());

It prints "Indicator_$$_javassist_6".

The test

indicator instanceof Elementary 

or

indicator instanceof Composite 

return false.

The same tests with classic Hibernate (without Envers) return true for one of them and print the correct class name (even with a FetchType.LAZY on the ManyToOne indicator attribute !)