Brandon Wagner
Brandon Wagner

Reputation: 893

hasMany save not working on Grails 2.4.2 upgrade

I recently upgraded from Grails 2.2.5 to 2.4.2 . After the upgrade a lot of my hasMany relationships are not saving.

For Example:

Domains:
    class Node {
        String name
        String description

        static belongsTo = CustomGlobe
        static hasMany = [containers: Container]
    }

    class Container {
        String name
        CustomGlobe customGlobe

        static belongsTo = Node
        static hasMany = [nodes: Node]
    }

    class CustomGlobe {
        String name

        static belongsTo = CustomLocation
        static hasMany = [customLocations: CustomLocation, nodes: Node]
    }

    class CustomLocation {
        String name
        String description
    }

On the service that performs the transaction I did add a @Transactional above the class def. I also attempted adding a @Transactional(propagation=Propagation.REQUIRES_NEW) per Grails 2.4.2: Strange cascading save behaviour in nested transactions . If I rollback the Grails upgrade (same controller, service, and view code) the nodes set is persisted correctly, however, with Grails 2.4.2 it does not. I also checked right before and right after the save by printing the object's nodes and it displays on the console, but when my application redirects to the list view it does not show and is not persisted anywhere.

--UPDATE-- This still occurs in Grails 2.4.3 I also believe it may have something to do with the join table, but I can't figure out why. The Container has nodes attached to it after params binding, but after the .save() it is not persisted to the join table.

--UPDATE-- Sorry, there was an error in the post of the Domain class code, it has been updated and is correct now. Hopefully someone can shed some light on what I'm missing now.

The issue occurs with the Nodes collection not being persisted to the Container instance in the NODE_CONTAINERS table.

--UPDATE-- Issue is ongoing. When debugging I created a new groovy Sql instance with the Grails datasource and manually inserted the Nodes into the NODE_CONTAINERS table. Everything saved correctly and was recalled correctly when viewing the Container show gsp. So it appears that GORM is treating the join table correctly when reading the instance, still not sure why it isn't saving the Nodes to the join table correctly.

SAMPLE APPLICATIONS FOR DEMONSTRATION OF ERROR:

  1. Working Application using Grails 2.2.5 (2.2.5 branch)
  2. Application exhibiting error described above using Grails 2.4.3 (MASTER branch)

(https://github.com/bwagner5/grailsCollectionsDebugApp/tree/master)

Grails Data Binder:

The issue seems to be the Grails Data Binder. The Spring Data Binder works fine (default in 2.2.x and you are able to override the Grails binder in 2.3.x but not 2.4.x) I have put in a JIRA but would still like to see if there is a workaround for now: https://jira.grails.org/browse/GRAILS-11638

Upvotes: 1

Views: 2515

Answers (2)

th3morg
th3morg

Reputation: 4809

I would recommend actually adding separate join classes, which is also a recommendation made by Burt Beckwith and you'll find this practice used in the Grails Spring Security Core project. For example with just your Node and Container classes you'd end up with:

class Node {
    String name
    String description
    Set<NodeContainer> nodeContainers = []
    static hasMany = [nodeContainers: NodeContainer]
}

class Container {
    String name
    CustomGlobe customGlobe

    //potentially add a helper method for fetching the nodes, but no GORM specification should be made on this class
}


class NodeContainer {
    Container container
    Node node

    static NodeContainer addNodeContainer(Node node, Container container){
        def n = findByNodeAndContainer(node, container)
        if(!n){
            n = new NodeContainer(container: container)
            node.addToContainers(n)
            n.save()
        }  
    }

    // Other helper methods such as remove and table mappings if necessary
}

In this way, you are creating and removing the joins one at a time instead of requiring hibernate to load the whole Set and then add/remove items from it. You are also removing the need to have hasMany on both classes, which can have performance and save issues on large sets. I've managed to resolve all of my funky saving issues by implementing this pattern. Good luck and I hope this helps!

Upvotes: 3

dmahapatro
dmahapatro

Reputation: 50275

Many to Many should have only one owning side. Based on that you would need one modification to Node domain class and with the below sample, the join table gets populated accordingly:

// Modification in Node
// Remove this belongsTo
// belongsTo should be only in the child side of m-m relationship
// static belongsTo = CustomGlobe

Then follow this sample (tried in BootStrap.groovy) to see the join table NODE_CONTAINERS table populated like below:

def node = new Node(name: "Node1", description: "Desc1")
def cg = new CustomGlobe(name: "CG1")

def cl1 = new CustomLocation(name: "CL1", description: "Cust Location 1")
def cl2 = new CustomLocation(name: "CL2", description: "Cust Location 2")

[cl1, cl2].each { cg.addToCustomLocations it }

cg.save()

def cont1 = new Container(name: "Cont1", customGlobe: cg)
def cont2 = new Container(name: "Cont2", customGlobe: cg)

// After removing one of the belongsTo the cascading behavior will not be
// achieved. So the other side of many-many has to be saved explicitly.
def node2 = new Node(name: "Node2", description: "Desc2").save()
def node3 = new Node(name: "Node3", description: "Desc3").save()
cont2.addToNodes( node2 ).addToNodes( node3 ).save()

[cont1, cont2].each { node.addToContainers it}

node.save flush: true

enter image description here

Tested in Grails 2.4.2 & can share the sample app if there is still a problem.

Upvotes: 1

Related Questions