Reputation: 893
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.
(https://github.com/bwagner5/grailsCollectionsDebugApp/tree/master)
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
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
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
Tested in Grails 2.4.2 & can share the sample app if there is still a problem.
Upvotes: 1