Search code examples
xodusxodus-dnq

How to cascade deletion of last linked item with Xodus DNQ


I have two entities; municipality and city (actually more but simplifying here). A municipality must always contain at least one city.

Is there a way to set up a constraint that makes the deletion of the final city cascade to delete its parent municipality as well?


Entities

class XdCity(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<XdCity>()

    var name by xdRequiredStringProp()
    var municipality: XdMunicipality by xdLink1(
        XdMunicipality::cities,
        onDelete = OnDeletePolicy.CLEAR,
        onTargetDelete = OnDeletePolicy.CASCADE
    )
}
class XdMunicipality(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<XdMunicipality>()

    var name by xdRequiredStringProp(unique = true)
    val cities by xdLink1_N(
        XdCity::municipality,
        onDelete = OnDeletePolicy.CASCADE,
        onTargetDelete = OnDeletePolicy.CLEAR
    )
}

Test case

@Test
fun testCityDeletionCascade() {
    Database.store.transactional {
        val municipality = XdMunicipality.findOrNew("Mun 1")
        XdCity.findOrNew("City A").apply {
            this.municipality = municipality
        }
        XdCity.findOrNew("City B").apply {
            this.municipality = municipality
        }
    }

    Database.store.transactional { 
        XdCity.all().first().delete()
        assertTrue { XdMunicipality.all().isNotEmpty }

        XdCity.all().first().delete()            
        assertTrue { XdMunicipality.all().isEmpty }
    }
}

Solution

  • Xodus-dnq has two mechanism for that: XdEntityListener or XdEntity#beforeFlush. Both of them can be applied here. For beforeFlush:

    class XdMunicipality(entity: Entity) : XdEntity(entity) {
            companion object : XdNaturalEntityType<XdMunicipality>()
    
            var name by xdRequiredStringProp(unique = true)
            val cities by xdLink1_N(
                 XdCity::municipality,
                 onDelete = OnDeletePolicy.CASCADE,
                 onTargetDelete = OnDeletePolicy.CLEAR
            )
    
            override fun beforeFlush() {
                if (cities.isEmpty) {
                    delete()
                }
            }
        }
    

    Both of this mechanisms called only on transaction flush. So test should be modified like this:

    @Test
        fun testCityDeletionCascade() {
            store.transactional {
                val municipality = XdMunicipality.findOrNew { name = "Mun 1" }
                XdCity.findOrNew { name = "City A" }.apply {
                    this.municipality = municipality
                }
                XdCity.findOrNew { name = "City B" }.apply {
                    this.municipality = municipality
                }
            }
    
            store.transactional {
                XdCity.all().first().delete()
                assertTrue { XdMunicipality.all().isNotEmpty }
            }
            store.transactional {
                XdCity.all().first().delete()
            }
            store.transactional {
                assertTrue { XdMunicipality.all().isEmpty }
            }
        }