MongoTemplate upsert - easy way to make Update from pojo (which user has editted)?
Asked Answered
C

13

23

Here is a simple pojo:

public class Description {
    private String code;
    private String name;
    private String norwegian;
    private String english;
}

And please see the following code to apply an upsert to MongoDb via spring MongoTemplate:

Query query = new Query(Criteria.where("code").is(description.getCode()));
Update update = new Update().set("name", description.getName()).set("norwegian", description.getNorwegian()).set("english", description.getEnglish());
mongoTemplate.upsert(query, update, "descriptions");

The line to generate the Update object specifies every field of the Item class manually.

But if my Item object changes then my Dao layer breaks.

So is there a way to avoid doing this, so that all fields from my Item class are applied automatically to the update?

E.g.

Update update = new Update().fromObject(item);

Note that my pojo does not extend DBObject.

Cusack answered 15/11, 2013 at 12:55 Comment(0)
L
0

I ran into the same problem. In het current Spring Data MongoDB version no such thing is available. You have to update the seperate fields by hand.

However it is possible with another framework: Morphia.

This framework has a wrapper for DAO functionality: https://github.com/mongodb/morphia/wiki/DAOSupport

You can use the DAO API to do things like this:

SomePojo pojo = daoInstance.findOne("some-field", "some-value");
pojo.setAProperty("changing this property");
daoInstance.save(pojo);
Linehan answered 5/3, 2014 at 23:51 Comment(0)
O
24

I found a pretty good solution for this question

//make a new description here
Description d = new Description();
d.setCode("no");
d.setName("norwegian");
d.setNorwegian("norwegian");
d.setEnglish("english");

//build query
Query query = new Query(Criteria.where("code").is(description.getCode()));

//build update
DBObject dbDoc = new BasicDBObject();
mongoTemplate.getConverter().write(d, dbDoc); //it is the one spring use for convertions.
Update update = Update.fromDBObject(dbDoc);

//run it!
mongoTemplate.upsert(query, update, "descriptions");

Plz note that Update.fromDBObject return an update object with all fields in dbDoc. If you just want to update non-null fields, you should code a new method to exclude null fields.

For example, the front-end post a doc like below:

//make a new description here
Description d = new Description();
d.setCode("no");
d.setEnglish("norwegian");

We only need to update the field 'language':

//return Update object
public static Update fromDBObjectExcludeNullFields(DBObject object) {
    Update update = new Update();       
    for (String key : object.keySet()) {
        Object value = object.get(key);
        if(value!=null){
            update.set(key, value);
        }
    }
    return update;
}

//build udpate
Update update = fromDBObjectExcludeNullFields(dbDoc);
Osmious answered 22/1, 2016 at 9:27 Comment(2)
@starkk92 plz note that Update.fromDBObject will replace all fields in dbDoc. If u just want to create Update with non-null fields, u have to code it yourself.Osmious
OK for the answer, but this also sets null objects. If you don't want null objects to be updated, you have to include an ObjectMapper with the option to ignore inlcudes. This is @gogstad comment on Vivek Sethi's answer. Thanks by the way.Atmosphere
P
15

The solution for a new spring-data-mongodb version 2.X.X.

The API has evolved, since 2.X.X version there is:

Update.fromDocument(org.bson.Document object, String... exclude)

instead of (1.X.X):

Update.fromDBObject(com.mongodb.DBObject object, String... exclude)

The full solution:

//make a new description here
Description d = new Description();
d.setCode("no");
d.setName("norwegian");
d.setNorwegian("norwegian");
d.setEnglish("english");
Query query = new Query(Criteria.where("code").is(description.getCode()));

Document doc = new Document(); // org.bson.Document
mongoTemplate.getConverter().write(item, doc);
Update update = Update.fromDocument(doc);

mongoTemplate.upsert(query, update, "descriptions");

It works!

Ploughman answered 9/7, 2018 at 8:31 Comment(3)
why does this fail to work for BulkOperations any idea here is a snippet of my code pastebin.com/t0mZDZd8 java.lang.IllegalArgumentException: Invalid BSON field name codeBrigid
@Brigid did you find any solution to this? I have the same problem.Fanfaronade
@Fanfaronade check if this helps pastebin.com/LxEPym3SBrigid
B
7

Just like previous answers said, use mongoTemplate.getConverter().write() and Update.fromDocument() functions. But i found Update.fromDocument() won't add "$set" key and won't work directly, the solution is to add "$set" yourself, like below (PS: I'm using 2.2.1.RELEASE version):

public static Update updateFromObject(Object object, MongoTemplate mongoTemplate) {
    Document doc = new Document();
    mongoTemplate.getConverter().write(object, doc);
    return Update.fromDocument(new Document("$set", doc));
}
Burmeister answered 27/11, 2019 at 9:27 Comment(3)
As surprising as it looks, this solutions helps us out ! Why would we have to manually add the $set key to the update definition ?Sander
@Sander Because not all updates are using "$set", so it makes sense to let the user to add it if needed.Burmeister
Thanks. Helped me to get difference between UPDATED and ALREADY_UPDATEDRaised
P
6

you can use save : (if non exist = insert else = upsert)

save(Object objectToSave, String collectionName)

read : javadoc

Pepperandsalt answered 7/10, 2014 at 8:34 Comment(0)
R
3

If you want to upsert Pojos incl. property String id; you have to exclude the _id field in the fromDBObject method Update.fromDBObject(dbDoc,"_id").

Otherwise you get the Exception:

org.springframework.dao.DuplicateKeyException: { "serverUsed" : "127.0.0.1:27017" , "ok" : 1 , "n" : 0 , "updatedExisting" : false , "err" : "E11000 duplicate key error collection: db.description index: _id_ dup key: { : null }" , "code" : 11000}; nested exception is com.mongodb.MongoException$DuplicateKey: { "serverUsed" : "127.0.0.1:27017" , "ok" : 1 , "n" : 0 , "updatedExisting" : false , "err" : "E11000 duplicate key error collection: db.description index: _id_ dup key: { : null }" , "code" : 11000}

because the _id field of the first is null

{
    "_id" : null,
...

}

Fullcode based on @PaniniGelato answer would be

public class Description(){
    public String id;
...
}

Description d = new Description();
d.setCode("no");
d.setName("norwegian");
d.setNorwegian("norwegian");
d.setEnglish("english");

//build query
Query query = new Query(Criteria.where("code").is(description.getCode()));

//build update
DBObject dbDoc = new BasicDBObject();
mongoTemplate.getConverter().write(d, dbDoc); //it is the one spring use for convertions.
Update update = Update.fromDBObject(dbDoc, "_id");

//run it!
mongoTemplate.upsert(query, update, "descriptions");

Then the upsert is working in the cases of insert and update. Corrections & thoughts are welcome ;)

Residential answered 2/9, 2016 at 10:41 Comment(1)
caveat: If you are using a @Version - tagged field in your entity, this approach will interfere with spring's version incrementation, producing an exception "Invalid BSON field name $inc". In this case, one must iterate over the dbDoc and use update.set like in @PaniniGelatos answer.Provisory
S
1

This is what I am doing for the time being. Not so much elegant way to do it, but it does save a precious DB call:

import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.util.JSON;

/**
 * Perform an upsert operation to update ALL FIELDS in an object using native mongo driver's methods
 * since mongoTemplate's upsert method doesn't allow it
 * @param upsertQuery
 * @param object
 * @param collectionName
 */
private void performUpsert(Query upsertQuery, Object object, String collectionName){

    ObjectMapper mapper = new ObjectMapper();

    try {
        String jsonStr = mapper.writeValueAsString(object);
        DB db = mongoTemplate.getDb();
        DBCollection collection = db.getCollection(collectionName);
        DBObject query = upsertQuery.getQueryObject();
        DBObject update = new BasicDBObject("$set", JSON.parse(jsonStr));
        collection.update(query, update, true, false);
    } catch (IOException e) {
        LOGGER.error("Unable to persist the metrics in DB. Error while parsing object: {}", e);
    }
}
Shareeshareholder answered 6/8, 2015 at 16:23 Comment(1)
You need to make sure the ObjectMapper doesn't serialize nulls: String jsonStr = objectMapper.copy().setSerializationInclusion(JsonInclude.Include.NON_NULL).writeValueAsString(object);Actual
T
1

There are two cases here that need to be distinguished:

  1. Update an item that was previously fetched from the DB.
  2. Update or insert (upsert) an item you created by code.

In Case 1) You can simply use mongoTemplate.save(pojo, "collection"), because your POJO will already have a filled ObjectID in its id field.

In case 2) You have to explain to mongo what "already exists" means in case of your domain model: By default the mongoTemplate.save() method updates an existing item, if there is one with that same ObjectId. But with a newly instantiated POJO you do not have that id. Therefore the mongoTemplate.upsert() method has a query parameter that you can create like this:

MyDomainClass pojo = new MyDomainClass(...);

Query query = Query.query(Criteria.where("email").is("[email protected]"));

DBObject dbDoc = new BasicDBObject();
mongoTemplate.getConverter().write(pojo, dbDoc);   //it is the one spring use for convertions.
dbDoc.removeField("_id");    // just to be sure to not create any duplicates
Update update = Update.fromDBObject(dbDoc);

WriteResult writeResult = mongoTemplate.upsert(query, update, UserModel.class);
Tenpin answered 26/12, 2016 at 20:42 Comment(0)
L
0

I ran into the same problem. In het current Spring Data MongoDB version no such thing is available. You have to update the seperate fields by hand.

However it is possible with another framework: Morphia.

This framework has a wrapper for DAO functionality: https://github.com/mongodb/morphia/wiki/DAOSupport

You can use the DAO API to do things like this:

SomePojo pojo = daoInstance.findOne("some-field", "some-value");
pojo.setAProperty("changing this property");
daoInstance.save(pojo);
Linehan answered 5/3, 2014 at 23:51 Comment(0)
M
0

I think that: Description add a property

@Id
private String id;

then get a document by the query condition,set Description's id by document's id. and save

Mother answered 14/5, 2014 at 9:44 Comment(0)
I
0

Just use ReflectionDBObject - if you make Description extend it, you should just get your object's fields transferred to Update reflectively, automagically. The note from above about null fields included in the update still holds true.

Itemize answered 7/6, 2016 at 1:10 Comment(0)
L
0
public void saveOrUpdate(String json) {
    try {
        JSONObject jsonObject = new JSONObject(json);
        DBObject update1 = new BasicDBObject("$set", JSON.parse(json));
        mongoTemplate.getCollection("collectionName").update(new Query(Criteria.where("name").is(jsonObject.getString("name"))).getQueryObject(), update1, true, false);
    } catch (Exception e) {
        throw new GenericServiceException("Error while save/udpate. Error msg: " + e.getMessage(), e);
    }

}

this is very simple way to save json string into collection using mongodb and spring. This method can be override to use as JSONObject.

Longobard answered 22/3, 2018 at 17:50 Comment(0)
C
-1
@Override
public void updateInfo(UpdateObject algorithm) {
    Document document = new Document();
    mongoTemplate.getConverter().write(algorithm, document);
    Update update = Update.fromDocument(document);
    mongoTemplate.updateFirst(query(where("_id").is(algorithm.get_id())), update, UpdateObject.class);
}
Committeeman answered 30/10, 2018 at 12:0 Comment(1)
Code only answers are really discouraged. To help future readers, please explain what you are doing too!Shriek
Q
-1

After upsert, I was Tring to fetch same record but it was given me the old one.

But in dB I am having new records.

Quiescent answered 31/10, 2022 at 6:44 Comment(2)
Can someone help me with this.Quiescent
If you have a new question, please ask it by clicking the Ask Question button. Include a link to this question if it helps provide context. - From ReviewMaronite

© 2022 - 2024 — McMap. All rights reserved.