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.nametoChild.namewhileBoy.namestays 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.