blaze-persistence
blaze-persistence copied to clipboard
Updating a shared attribute in entity hierarchy doesn't work
Setup
Entity model:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
class Child {
@Id
@GeneratedValue
var id: Long? = null
var name: String? // NOTE: it's nullable
}
class Boy : Child() {
override var name: String // NOTE: overridden to non-nullable
}
Corresponding entity view model:
@CreatableEntityView
@EntityView(Child::class)
@EntityViewInheritance
interface ChildView {
@get:IdMapping
val id: Long?
var name: String,
}
@EntityView(Boy::class)
@EntityViewInheritance
interface BoyView : ChildView
Actual behaviour
Saving a new BoyView
instance with non-null name
via evm.save
leads to name
being not saved to DB.
Expected behaviour
The BoyView
instance is saved with name
as defined.
Analysis
Debugging shows that BP is confused by the overridden name
attribute in the entities. When converting the EV instance into the entity it assigns BoyView.name
to Child.name
while Boy.name
stays null.
When I drop the name
override from Boy
everything works as expected.
Debugging shows that BP is confused by the overridden name attribute in the entities. When converting the EV instance into the entity it assigns
BoyView.name
toChild.name
whileBoy.name
stays null.
This is by design. Since ChildView
is an entity view for Child
, ChildView#name
will map to Child#name
. The problem is that there are two distinct fields in the class files Child
and Boy
with the same name name
and that you are using field access in JPA. BP will respect that, and write to the respective field.
Using property access would fix the problem, because by calling getters/setters, only the Boy#name
field would be used. This is really a Kotlin issue, because in regular Java, nobody would declare a field/property in a subclass again with the same name.
So is there any way how I can make BP use the BoyView.name
setter? I tried overriding the field in BoyView
but that didn't help.
Also, you say that it's a Kotlin issue but how come then that Hibernate (whose philosophy BP apparently follows) doesn't have issues with that?
I believe Hibernate has a similar issue. The solution is to use property access i.e. use @get:Id
etc.
Maybe we can add an option to override the access method in BP, but it is kind of strange that Kotlin maps properties to getters/setters, yet puts annotations on fields.
The solution is to use property access
AFAIK this requires to use property access simply everywhere in my entity model which is huge (and probably risky) work which I would rather like to avoid. Especially when I have no issues with plain Hibernate with this setup, i.e. all data are always saved to DB as expected.
Honestly, I still don't understand the underlying BP behaviour. If BP maps an instance of BoyView to the underlying entity and BP knows that the Boy entity has field name
, why on earth it maps to Child#name
?
Also, I try to remedy the issue by simply not doing the entity overrides. I lose nullability info but that's not so important with entities. This worked.
But then I thought I might reinstate the nullability on the EV side, which makes more sense anyway because EVs are app facing. So I did this (using more realistic model):
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE", length = 40)
class ApplicantLiability : BaseEntity() {
@Embedded
@AttributeOverride(name = "amount", column = Column(name = "balance"))
@AttributeOverride(name = "currency", column = Column(name = "balance_currency", length = 3))
var balance: Money? = null
}
@Entity
class ApplicantConsumerLoanLiability(
// no balance override here
@Embedded
@AttributeOverride(name = "amount", column = Column(name = "monthly_installment"))
@AttributeOverride(name = "currency", column = Column(name = "monthly_installment_currency", length = 3))
var monthlyInstallment: Money,
) : ApplicantLiability()
@CreatableEntityView(excludedEntityAttributes = ["creationTimestamp", "updateTimestamp"])
@EntityView(ApplicantLiability::class)
@EntityViewInheritance
interface ApplicantLiabilityView : BaseView {
val balance: MoneyView?
}
@EntityView(ApplicantConsumerLoanLiability::class)
@GraphQLName("ApplicantConsumerLoanLiability")
interface ApplicantConsumerLoanLiabilityView : ApplicantLiabilityView {
// it works without this override
override var balance: MoneyView
var monthlyInstallment: MoneyView
}
I'm able to correctly save the ApplicantConsumerLoanLiabilityView
data to DB. But when I query it back (via CriteriaBuilder#resultList
then I get an error
java.lang.RuntimeException: Could not invoke the proxy constructor 'public com.vacuumlabs.robo.backend.api.v1.dtos.MoneyView_$$_javassist_entityview_(com.vacuumlabs.robo.backend.api.v1.dtos.MoneyView_$$_javassist_entityview_,int,java.lang.Object[])' with the given tuple: [MoneyView(amount = 3080, currency = EUR)] with the types: [com.vacuumlabs.robo.backend.api.v1.dtos.MoneyView_$$_javassist_entityview_]",
It seems that the EV-side override confuses BP to instantiate MoneyView
with a MoneyView
instance instead of its components. I wonder whether this might be related.
To be more clear, I suspect that the common denominator for both issues might be @EntityViewInheritance
. There are several issues opened for that so it wouldn't be entirely surprising.