Reputation: 1479
I'm trying to implement a simple "insert or update" (so-called 'upsert') method in Grails / GORM / mongodb plug-in / MongoDB.
The approach I used with Hibernate (using merge) fails with a duplicate key error. I presume perhaps merge() isn't a supported operation in mongodb GORM, and tried to get to the native upsert method through GMongo.
I finally have a version that works (as posted below), but it is probably not the best way, as adding any fields to the object being saved will break the code silently.
public void upsertPrefix(p) {
def o = new BasicDBObject()
o.put("_id", p.id)
o.put("someValue", p.someValue)
o.put("otherValue", p.otherValue)
// DBObject o = p as DBObject // No signature of method: mypackage.Prefix.keySet() is applicable for argument types: () values: []
db.prefix.update([_id : p.id], o, true, false)
// I actually would want to pass p instead of o here, but that fails with:
// No signature of method: com.gmongo.internal.DBCollectionPatcher$__clinit__closure2.doCall() is applicable for argument types: (java.util.ArrayList) values: [[[_id:keyvalue], mypackage.Prefix : keyvalue, ...]]
/* All of these other more "Hibernatesque" approaches fail:
def existing = Prefix.get(p.id)
if (existing != null) {
p.merge(flush:true) // E11000 duplicate key error
// existing.merge(p) // Invocation failed: Message: null
// Prefix.merge(p) // Invocation failed: Message: null
} else {
p.save(flush:true)
}
*/
}
I guess I could introduce another POJO-DbObject mapping framework to the mix, but that would complicate things even more, duplicate what GORM is already doing and may introduce additional meta-data.
Any ideas how to solve this in the simplest fashion?
Edit #1: I now tried something else:
def existing = Prefix.get(p.id)
if (existing != null) {
// existing.properties = p.properties // E11000 duplicate key error...
existing.someValue = p.someValue
existing.otherValue = p.otherValue
existing.save(flush:true)
} else {
p.save(flush:true)
}
Once again the non-commented version works, but is not well maintainable. The commented version which I'd like to make work fails.
Edit #2:
Version which works:
public void upsertPrefix(p) {
def o = new BasicDBObject()
p.properties.each {
if (! (it.key in ['dbo'])) {
o[it.key] = p.properties[it.key]
}
}
o['_id'] = p.id
db.prefix.update([_id : p.id], o, true, false)
}
Version which never seems to insert anything:
def upsertPrefix(Prefix updatedPrefix) {
Prefix existingPrefix = Prefix.findOrCreateById(updatedPrefix.id)
updatedPrefix.properties.each { prop ->
if (! prop.key in ['dbo', 'id']) { // You don't want to re-set the id, and dbo is r/o
existingPrefix.properties[prop.key] = prop.value
}
}
existingPrefix.save() // Never seems to insert anything
}
Version which still fails with duplicate key error:
def upsertPrefix(p) {
def existing = Prefix.get(p.id)
if (existing != null) {
p.properties.each { prop ->
print prop.key
if (! prop.key in ['dbo', 'id']) {
existingPrefix.properties[prop.key] = prop.value
}
}
existing.save(flush:true) // Still fails with duplicate key error
} else {
p.save(flush:true)
}
}
Upvotes: 2
Views: 1604
Reputation: 10222
MongoDB has native support for upsert. See the findAndModify Command with upsert parameter true.
Upvotes: 0
Reputation: 31280
Assuming you have either an updated version of the object, or a map of the properties you need to update with their new values, you could loop over those and apply the updates for each property.
Something like this:
def upsert(Prefix updatedPrefix) {
Prefix existingPrefix = Prefix .findOrCreateById(updatedPrefix.id)
updatedPrefix.properties.each { prop ->
if (prop.key != 'id') { // You don't want to re-set the id
existingPrefix.properties[prop.key] = prop.value
}
}
existingPrefix.save()
}
How to exclude updating the ID may not be quite correct, so you might have to play with it a bit. You also might consider only updating a property if it's corresponding new value is different from the existing one, but that's essentially just an optimization.
If you have a map, you might also consider doing the update the way the default controller scaffolding does:
prefixInstance.properties = params
Upvotes: 1