grails-data-mapping
grails-data-mapping copied to clipboard
Table per subclass doesn't work with abstract base class in hierarchy
I wanted to create a hierarchy of classes with the table per subclass strategy (see example below). Since we have common functionality in our domain classes, we have an abstract base class that is extended by all other domain classes.
Note: if I remove the abstract base class from the hierarchy, the application starts and works as expected.
Task List
- [x] Steps to reproduce provided
- [x] Stacktrace (if present) provided
- [x] Example that reproduces the problem uploaded to Github
- [x] Full description of the issue provided (see below)
Steps to Reproduce
Put the following classes in the grails domain folder.
abstract class BaseEntity {
// common behavior for all entities in the application
}
class X extends BaseEntity {
static mapping = {
tablePerHierarchy false
}
}
class A extends X {
static hasMany = [bs: B]
}
class B extends X {
A a
}
Expected Behaviour
I think the given class hierarchy is not that complex and pretty common and should just work.
Actual Behaviour
An exception is thrown during application boot.
Caused by: org.hibernate.MappingException: Entities [example.A] and [example.B] are mapped with the same discriminator value 'null'.
at org.hibernate.persister.entity.SingleTableEntityPersister.addSubclassByDiscriminatorValue(SingleTableEntityPersister.java:450)
at org.hibernate.persister.entity.SingleTableEntityPersister.<init>(SingleTableEntityPersister.java:424)
... 57 common frames omitted
Environment Information
- Operating System: Windows 10
- GORM Version: 7.0.1
- Grails Version (if using Grails): Grails 3.3.10
- JDK Version: 8
Example Application
The 4 given classes under "Steps to Reproduce" are sufficient to reproduce the exception in a newly created grails application.
Is the abstract in the domain
folder? If so then GORM will attempt to map that class to a table. If this is desired then you should move:
static mapping = {
tablePerHierarchy false
}
.. to the Abstract class instead. I am currently using this very pattern inside an app I am currently developing and it is working as expected. The problem that you are hitting at the moment is probably because you are mixing 2 types of inheritance within 1 hierarchy. Table per hierarchy
from BaseEntity
to X
and then table per class
from X
to A
/B
If what you want is the abstract class to not be persisted to its own table, you could try moving the abstract to src/main/groovy
and annotating it with @MappedSuperclass.
In my case I already did put BaseEntity
in src/main/groovy
and I don't want BaseEntity
to be mapped.
I currently can't reproduce the above mentioned exception and unfortunately I don't have access to the source where the problem occured at the moment. But I created a new example application with the above configuration and the newest version of Grails 4.0.0 and GORM 7.0.2.
The application does start without the MappingException
. But I get a BadSqlGrammarException
during runtime when I execute the following code:
A.withNewTransaction {
B b = new B()
A a = new A()
a.addToBs(b)
a.save(flush: true)
}
A.withNewSession {
println A.list()
println B.list()
println X.list()
println A.list().first().bs // this doesn't work
}
Full stacktrace:
2019-07-22 21:20:28.763 ERROR --- [ restartedMain] o.h.engine.jdbc.spi.SqlExceptionHelper : Column "BS0_.CLASS" not found; SQL statement:
select bs0_.a_id as a_id2_1_0_, bs0_.id as id1_1_0_, bs0_.id as id1_2_1_, bs0_1_.version as version2_2_1_, bs0_.a_id as a_id2_1_1_ from b bs0_ inner join x bs0_1_ on bs0_.id=bs0_1_.id where ( bs0_.class in ('example.B')) and bs0_.a_id=? [42122-199]
2019-07-22 21:20:28.780 ERROR --- [ restartedMain] o.s.boot.SpringApplication : Application run failed
org.springframework.jdbc.BadSqlGrammarException: Hibernate operation: could not prepare statement; bad SQL grammar [select bs0_.a_id as a_id2_1_0_, bs0_.id as id1_1_0_, bs0_.id as id1_2_1_, bs0_1_.version as version2_2_1_, bs0_.a_id as a_id2_1_1_ from b bs0_ inner join x bs0_1_ on bs0_.id=bs0_1_.id where ( bs0_.class in ('example.B')) and bs0_.a_id=?]; nested exception is org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "BS0_.CLASS" not found; SQL statement:
select bs0_.a_id as a_id2_1_0_, bs0_.id as id1_1_0_, bs0_.id as id1_2_1_, bs0_1_.version as version2_2_1_, bs0_.a_id as a_id2_1_1_ from b bs0_ inner join x bs0_1_ on bs0_.id=bs0_1_.id where ( bs0_.class in ('example.B')) and bs0_.a_id=? [42122-199]
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:234)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
at org.grails.orm.hibernate.GrailsHibernateTemplate.convertJdbcAccessException(GrailsHibernateTemplate.java:725)
at org.grails.orm.hibernate.GrailsHibernateTemplate.convertHibernateAccessException(GrailsHibernateTemplate.java:712)
at org.grails.orm.hibernate.GrailsHibernateTemplate.doExecute(GrailsHibernateTemplate.java:301)
at org.grails.orm.hibernate.GrailsHibernateTemplate.execute(GrailsHibernateTemplate.java:241)
at org.grails.orm.hibernate.GrailsHibernateTemplate.executeWithNewSession(GrailsHibernateTemplate.java:153)
at org.grails.orm.hibernate.AbstractHibernateDatastore.withNewSession(AbstractHibernateDatastore.java:360)
at org.grails.orm.hibernate.AbstractHibernateGormStaticApi.withNewSession(AbstractHibernateGormStaticApi.groovy:82)
at org.grails.datastore.gorm.GormEntity$Trait$Helper.withNewSession(GormEntity.groovy:1027)
at org.grails.datastore.gorm.GormEntity$Trait$Helper$withNewSession$3.call(Unknown Source)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:115)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:135)
at example.A.withNewSession(A.groovy)
at example.A$withNewSession$2.call(Unknown Source)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:115)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:127)
at grails4.test.BootStrap$_closure1.doCall(BootStrap.groovy:20)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:101)
at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:323)
at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:263)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1099)
at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041)
at groovy.lang.Closure.call(Closure.java:405)
at groovy.lang.Closure.call(Closure.java:399)
at grails.util.Environment.evaluateEnvironmentSpecificBlock(Environment.java:541)
at grails.util.Environment.executeForEnvironment(Environment.java:534)
at grails.util.Environment.executeForCurrentEnvironment(Environment.java:510)
at org.grails.web.servlet.boostrap.DefaultGrailsBootstrapClass.callInit(DefaultGrailsBootstrapClass.java:74)
at org.grails.web.servlet.context.GrailsConfigUtils.executeGrailsBootstraps(GrailsConfigUtils.java:83)
at org.grails.plugins.web.servlet.context.BootStrapClassRunner.onStartup(BootStrapClassRunner.groovy:56)
at grails.boot.config.GrailsApplicationPostProcessor.onApplicationEvent(GrailsApplicationPostProcessor.groovy:269)
at grails.boot.config.GrailsApplicationPostProcessor.onApplicationEvent(GrailsApplicationPostProcessor.groovy)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:402)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:359)
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:896)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:161)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:552)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:742)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:389)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:311)
at grails.boot.GrailsApp.run(GrailsApp.groovy:97)
at grails.boot.GrailsApp.run(GrailsApp.groovy:458)
at grails.boot.GrailsApp.run(GrailsApp.groovy:445)
at grails4.test.Application.main(Application.groovy:11)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
Do you know what could be the problem?
Edit: I also tested this configuration with your suggestion to annotate @MappedSupperclass
. I tried putting it on either BaseEntity
and X
but it doesn't seem to have an effect.
It would be best if you could create a project so I can look at the full picture.
That said however, the fact that it's looking for class
would suggest that it is treating BaseEntity
as an entity, which you have said you do not want. In which case I think you should add the @MappedSupperclass
to your base entity and remove completely the tablePerHierarchy
from your model. As your parent is not to be mapped anyway. You should also drop X from your model as it adds nothing except implementing BaseEntity
. I would have thought that would produce 2 tables 1 named a
and one named b
, with both table containing the columns represented by BaseEntity
and any extras declared in A
or B
respectively.
You are right, the problem is best described with an example application, so I created one here: https://github.com/davidkron/grails-data-mapping-issue-1254
I also created a minimal Spring Boot Data JPA version on a separate branch. I implemented the same model with standard JPA annotations and it seems to be working in a Spring Boot application. Maybe this could also help locating the problem.