Bug: event onEntitySaving is not called in some cases

I have 3 entities: GrandParent, Parent and Child. with composition to there respective child with CascadeType.PERSIST.

@JmixEntity
@Table(name = "GRAND_PARENT")
@Entity(name = "GrandParent")
class GrandParent {
    @JoinColumn(name = "PROP_ID")
    @Composition
    @OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST])
    var property: Parent? = null
}
----
@JmixEntity
@Table(name = "PARENT")
@Entity(name = "Parent")
class Parent {
    @JoinColumn(name = "PROP_ID")
    @Composition
    @OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST])
    var property: Child? = null
}
----
@JmixEntity
@Table(name = "CHILD")
@Entity(name = "Child")
class Child {
    var data ....
}

When I save the GrandParent then the parent and child are saved automatically. The bug is in the EventListener, the EntitySavingEvent is not called for the Child (it is for the Parent and GrandParent). I absolutly need to check validity of the data in the Child entity before it is saved otherwise the data could be wrong in DB.

Hello @t.vignal,

I’ve tried to reproduce the problem using next entities:

@Entity(name = "test_JpaCascadeParent")
@JmixEntity
@Table(name = "TEST_JPA_CASCADE_PARENT")
public class JpaCascadeParent extends BaseEntity {

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
    @JoinColumn(name = "FOO_ID")
    @Composition
    private JpaCascadeFoo foo;

    @Column(name = "NAME")
    private String name;

    //...
}
// ---------------------------------------------------------------
@Entity(name = "test$JpaCascadeFoo")
@JmixEntity
@Table(name = "TEST_JPA_CASCADE_FOO")
public class JpaCascadeFoo extends BaseEntity {

    @Column(name = "NAME")
    private String name;

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
    @Composition
    @JoinColumn(name = "BAR_ID")
    private JpaCascadeBar bar;
    //...
}
//-----------------------
@Entity(name = "test$JpaCascadeBar")
@JmixEntity
@Table(name = "TEST_JPA_CASCADE_BAR")
public class JpaCascadeBar extends BaseEntity {

    @Column(name = "NAME")
    private String name;

    //...
}

And test:

def "check nested cascade events"(){
        setup:
        TestCascadeFooEventListener.clear()
        TestCascadeBarEventListener.clear()

        when: "cascade persist occurs"
        def foo = dataManager.create(JpaCascadeFoo)
        foo.name = "testFoo"

        def bar = dataManager.create(JpaCascadeBar)
        bar.name = "testBar"

        foo.setBar(bar)

        def parent = dataManager.create(JpaCascadeParent)
        parent.name = "testParent"
        parent.setFoo(foo)

        dataManager.save(parent)


        def barChangedEvents = TestCascadeBarEventListener.allEvents
        def fooChangedEvents = TestCascadeFooEventListener.allEvents

        then: "All events present for cascade-persisted entity"
        barChangedEvents.size() == fooChangedEvents.size()

               barChangedEvents.stream().anyMatch(info -> info.message == "AfterInsertEntityListener")
        barChangedEvents.stream().anyMatch(info -> info.message == "BeforeInsertEntityListener")
        barChangedEvents.stream().anyMatch(info -> info.message == "BeforeDetachEntityListener")
        barChangedEvents.stream().anyMatch(info -> info.message == "EntitySavingEvent: isNew=true")
        barChangedEvents.stream().anyMatch(info -> info.message == "EntityChangedEvent: beforeCommit, CREATED")
        barChangedEvents.stream().anyMatch(info -> info.message == "EntityChangedEvent: afterCommit, CREATED")
        barChangedEvents.stream().anyMatch(info -> info.message == "EntityLoadingEvent")

        fooChangedEvents.stream().anyMatch(info -> info.message == "AfterInsertEntityListener")
        fooChangedEvents.stream().anyMatch(info -> info.message == "BeforeInsertEntityListener")
        fooChangedEvents.stream().anyMatch(info -> info.message == "BeforeDetachEntityListener")
        fooChangedEvents.stream().anyMatch(info -> info.message == "EntitySavingEvent: isNew=true")
        fooChangedEvents.stream().anyMatch(info -> info.message == "EntityChangedEvent: beforeCommit, CREATED")
        fooChangedEvents.stream().anyMatch(info -> info.message == "EntityChangedEvent: afterCommit, CREATED")
        fooChangedEvents.stream().anyMatch(info -> info.message == "EntityLoadingEvent")
    }

(this test is based on https://github.com/jmix-framework/jmix/blob/master/jmix-data/eclipselink/src/test/groovy/cascade_operations/CascadeEventsTest.groovy)

Unfortunately, it was no success in problem reproduction: test passed.

Maybe I miss or misunderstood something?

Could you please provide a sample project where the problem can be reproduced?

Regards,
Dmitry

Hello,
the problem seams to be more subtle. After some research, I created a simple project to reproduce the bug.
bugEventListener.7z (68.9 KB)

the project contains two tests, the first one works but the second one don’t

@Test
    fun test_events_works() {
        var grandParent = dataManager.create(GrandParent::class.java)

        grandParent.property = mutableListOf(dataManager.create(Parent::class.java))
        grandParent.property.first().grandParent = grandParent
        grandParent.property.first().property = dataManager.create(Child::class.java)
        val saveGrandParent = dataManager.save(grandParent)
        Assertions.assertThat(saveGrandParent.property.first().property?.data).isEqualTo("initialized by event listener")
    }

    @Test
    fun test_events_do_not_works() {
        var grandParent = dataManager.create(GrandParent::class.java)

        grandParent = dataManager.save(grandParent) // <<<-- only difference between the two tests

        grandParent.property = mutableListOf(dataManager.create(Parent::class.java))
        grandParent.property.first().grandParent = grandParent
        grandParent.property.first().property = dataManager.create(Child::class.java)
        val saveGrandParent = dataManager.save(grandParent)
        Assertions.assertThat(saveGrandParent.property.first().property?.data).isEqualTo("initialized by event listener")
    }

As I understand it, both test should work.

Hello,

As I can see, both Parent and GrandParent have cascade = [CascadeType.PERSIST]. But PERSIST occurs for new entities only. Already existed ones cannot be “persisted”, they are MERGEd instead:

 @Test
    fun test_events_works() {
        var grandParent = dataManager.create(GrandParent::class.java)

        grandParent.property = mutableListOf(dataManager.create(Parent::class.java))
        grandParent.property.first().grandParent = grandParent
        grandParent.property.first().property = dataManager.create(Child::class.java)
        val saveGrandParent = dataManager.save(grandParent) // PERSIST occurs
        Assertions.assertThat(saveGrandParent.property.first().property?.data).isEqualTo("initialized by event listener")
    }

    @Test
    fun test_events_do_not_works() {
        var grandParent = dataManager.create(GrandParent::class.java)
        grandParent = dataManager.save(grandParent) // grandParent PERSISTED
        grandParent.property = mutableListOf(dataManager.create(Parent::class.java))
        grandParent.property.first().grandParent = grandParent
        grandParent.property.first().property = dataManager.create(Child::class.java)
        val saveGrandParent = dataManager.save(grandParent) //grandParent MERGED because it already exists in db
        Assertions.assertThat(saveGrandParent.property.first().property?.data).isEqualTo("initialized by event listener")
    }

Thus, setting cascade = [CascadeType.PERSIST, CascadeType.MERGE] needed in order to cascade saving operation not only for new entities but for already existed too, which happens at com.company.bugeventlistener.user.UserTest#test_events_do_not_works:43.
With such value both tests in the attached project are passed.

Regards,
Dmitry

Thank you for your anwser,
I was able to fix the issue in my projet, In the other hand, I think there is still something wrong.
The event listener OnEntitySaving supposes that it is call every time the entity is saved. Otherwise in my second test case, the entity Child is correctly saved but the event listener is not called on that same entity.