Unidirectional one to many, and mapped by in Grail

2019-08-21 07:10发布

问题:

This has now been raised as a new query after all my explorations today which I believe is a defect - please read the newer post first.

I have an RootEntity domain class and an EntityRelationship entity (I have separate explicit entity here as I want the relationship to have attributes of its own.

I want two collections of unidirectional links from RootEntity to EntityRelationship like this

A --references --> link (+attribs) --referenced by --> B

Code:

abstract class RootEntity {
    Long id
    String name
    LocalDateTime dateCreated
    LocalDateTime lastUpdated

    Collection<? extends EntityRelationship> entityReferences = []
    Collection<? extends EntityRelationship> entityReferencedBy = []

    static hasMany = [entityReferences: EntityRelationship, entityReferencedBy: EntityRelationship]

    /* doesn't appear to be required - mucks up the test
    static mappedBy = [
        entityReferences : "references",
        entityReferencedBy : "referencedBy"
    ]*/

    static mapping = {
        tablePerHierarchy false  //multiple tables+joins
    }

    static constraints = {
        entityReferences nullable:true
        entityReferencedBy nullable:true
    }
}

And my EntityRelationship object has two discrete ForKeys - one for referenced (entity that owns the 'link' , and other referencedBy for entities at remote end, like this:

class EntityRelationship<M extends RootEntity, N extends RootEntity> {

    String relationshipType
    String name
    String owningRole
    String referencedRole

    M references  //fk to owning entity
    N referencedBy  //fk to other end

    static mappedBy = [references: "none", referencedBy: "none"]  //seems to need this here - but not in RootEntity

    static constraints = {
        relationshipType unique:true, nullable:true
        name nullable:true
        owningRole nullable:true
        references nullable:true
        referencedRole nullable:true
        referencedBy nullable:true
    }
}

In order to get my test to work correctly - integration test snippet looks like this

     ...
    EntityRelationship <Device, Device> rel = new EntityRelationship()
    rel.name = "i need a PE"
    rel.owningRole = "i need this PE "
    rel.referencedRole = "i am supporting "

    ce.addToEntityReferences(rel)
    pe.addToEntityReferencedBy(rel)
   rel.save(failOnError:true)

    assert ce.entityReferences.size() == 1
    assert ce.entityReferencedBy.size() == 0

    assert pe.entityReferences.size() == 0
    assert pe.entityReferencedBy.size() == 1

So reading the the examples I though I had to have the mappedBy closure on the one to many side (rootEntity) - to tell hibernate which column in EntityLink was to be updated.

However if I tried that in the test code - it would tell me that the pe.referencedBy.size() was 2 not 1 as expected and fail. I looked at the ce and pe and rel in the debugger and they looked right in memory - but the assert for the pe.referencedBy fails.

In order to fix this what seems to work is to comment out the mappedBy in the RootEntity that defines the one-to-many collections, and instead put a mappedBy clause on the many (EntityLink) table withe mapped field:none.

Now when I run the test this will work.

This looks 'counter' to what the gorm example suggests (flights and airports example) where it says the mappedBy should be on the flights entity.

So clearly I've not understood the fine points of mappedBy. Why does my working code has to have the mappedBy on the many side?

Correction and addenda

Actually this is weirder than I thought

PS: stepped it through slowly in debugger and I do need the mappedBy on the RootEntity. This ensures that the call to addToReferences() and addToReferencedBy() correctly update the correct attribute in EntityRelationship - revised form now looks like

RootEntity.groovy

abstract class RootEntity {
    //id provided default by grails implicit in infrastructure - shown explicitly here
    Long id
    String name
    LocalDateTime dateCreated
    LocalDateTime lastUpdated

    Collection<? extends EntityRelationship> entityReferences = []
    Collection<? extends EntityRelationship> entityReferencedBy = []

    static hasMany = [entityReferences: EntityRelationship, entityReferencedBy: EntityRelationship]

    static mappedBy = [
        entityReferences : "references",        //map entityReferences to EntityRelationship.references
        entityReferencedBy : "referencedBy"     //map entityReferencedBy to EntityRelationship.referencedBy
    ]

    static mapping = {
        tablePerHierarchy false  //multiple tables+joins
    }

    static constraints = {
        entityReferences nullable:true
        entityReferencedBy nullable:true
    }

}

EntityRelationship.groovy is looking like this

class EntityRelationship<M extends RootEntity, N extends RootEntity> {

    String relationshipType
    String name
    String owningRole
    String referencedRole

    M references  //fk to owning entity
    N referencedBy  //fk to other end

    static mappedBy = [references: "none", referencedBy: "none"]  //seems to need this here - but not in RootEntity

    static constraints = {
        relationshipType unique:true, nullable:true
        name nullable:true
        owningRole nullable:true
        references nullable:true
        referencedRole nullable:true
        referencedBy nullable:true
    }

    static EntityRelationship createRelationship(String name, M from, N to) {
        if (from == null || to == null)
            return null

        EntityRelationship rel = new EntityRelationship()
        rel.name = name
        rel.owningRole = from.name
        rel.referencedRole = to.name
        from.addToEntityReferences (from)
        to.addToEntityReferencedBy (to)
        rel.save(failOnError: true)
        rel

    }
}

However this doesn't work as expected. This is the weird bit.

I set two break points one just before rel.save() and one just after it.

Section of my integration test:

    ...EntityRelationship <Device, Device> rel = new EntityRelationship()
    rel.name = "i need a PE"
    rel.owningRole = "i need this PE "
    rel.referencedRole = "i am supporting "

    ce.addToEntityReferences(rel)
    pe.addToEntityReferencedBy(rel)
   rel.save(failOnError:true)         //set Breakpoint 1

    assert ce.entityReferences.size() == 1  //set breakpoint 2
    assert ce.entityReferencedBy.size() == 0

    assert pe.entityReferences.size() == 0
    assert pe.entityReferencedBy.size() == 1

If I run the test with no debug - test fails with pe.entityReferencedBy.size() returned as 2.

If I run the debugger and step immediately past the first break point and onto the second, then look at the pe.entityReferencedBy collection it has two items in it assert then fails.

However if I stop at the first breakpoint, and inspect pe.entityReferencedBy collection before the rel.save() then the referencedBy collection has one entry. you step though the save and check again - still ok. When run to completion the test works!

So running without debug/or checking after rel.save() then the answer is wrong.

If I stop and check pe.entityReferencedBy before I call rel.save() the answer is correct 1 entry only.

Why does this happen, and more importantly why does it fail at all with no debugger?

Addenda 2

My last go for tonight - replaced the addToMethod calls and set up by hand like this

    //ce.addToEntityReferences(rel)
    //pe.addToEntityReferencedBy(rel)
    ce.entityReferences << rel
    rel.references = ce
    //pe.entityReferencedBy << rel
    rel.referencedBy = pe
    rel.save(failOnError:true)

So if I uncover pe.entityReferencedBy << rel and run the save - I seem to get two entries in referencedBy collection on the PE. If I add the rel to the ce instance, set the rel.rel.referencedBy = pe, but exclude adding to the pe's entityReferencedBy collection - the test says its working.

Now I am confused when I trigger the rel.save() action that appears to be doing an auto insert back into pe.entityReferenced collection (but doesn't do an extra update back on the ce.