blaze-persistence icon indicating copy to clipboard operation
blaze-persistence copied to clipboard

Updating a shared attribute in entity hierarchy doesn't work

Open david-kubecka opened this issue 2 years ago • 6 comments

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.

david-kubecka avatar Jan 26 '23 17:01 david-kubecka

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.

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.

beikov avatar Jan 27 '23 15:01 beikov

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?

david-kubecka avatar Apr 08 '23 08:04 david-kubecka

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.

beikov avatar Apr 08 '23 08:04 beikov

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?

david-kubecka avatar Apr 08 '23 08:04 david-kubecka

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.

david-kubecka avatar Apr 08 '23 08:04 david-kubecka

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.

david-kubecka avatar Apr 08 '23 09:04 david-kubecka