lexicore
lexicore

Reputation: 43709

Returning both old and new entities from Spring/MongoDB findAndModify

We're using MongoDB via Spring Data and rely on the findAndModify operation to update existing entities or create new ones.

In the findAndModify we can configure to return old state of the entity or the new one using returnNew(...).

Is there some way to return both old and new entities from findAndModify?

We need to compare entity states before and after update, this is why we need both instances.

At the moment we're resorting to requireNew(false) and then manually update a copy of the old instance, something like this:

public Pair<Data> saveItems(String id, List<Item> items) {
    final Query findById = ...;

    final Update update = new Update();
    // This is what we actually update
    update.set(ENTITY_FIELD_ITEMS, newItems);
    update.inc(ENTITY_FIELD_VERSION, 1);
    // Try updating and return the old data 
    final Data oldData = operations.findAndModify(findById, update,
        FindAndModifyOptions.options().upsert(true).returnNew(false), Data.class);

    // Copy or create new instance
    final Data newData;
    if (oldData == null) {
        newData = new Data(id);
    }
    else {
        newData = new Data(oldData);
    }
    // Apply the same update
    newData.setItems(newItems);
    newData.incVersion();
    return new Pair<Data>(oldData, newData);
}

Works but isn't pretty as we have to redo the same things we already do in the Update on the copy of the old instance.

What we've also considered was first loading an old instance and the running the update but it's not safe as the entity may have been modified between the load and the update. This may be addressed with versions and optimistic locking, but that makes things even more complicated.

Upvotes: 8

Views: 4755

Answers (3)

Akshansh Jain
Akshansh Jain

Reputation: 407

This probably might not be a very good approach, but I was thinking of having aggregation pipeline update in my findAndModify update. This will let me access the current document as $$ROOT, which I then can store in a field in the same document, and then update other fields in the document.

Something like this

db.test.findAndModify({
  query: {
    _id: ObjectId('677bd298948f20de2040b43f')
  },
  update: [
    {
      $set: {
        "previousState": "$$ROOT",
        "key1": "updatedValue1",
        "key2": "updatedValue2"
      }
    }
  ],
  new: true
});

Before

{
  "_id": {
    "$oid": "677bd298948f20de2040b43f"
  },
  "key1": "value1",
  "key2": "value2"
}

After

{
  "_id": {
    "$oid": "677bd298948f20de2040b43f"
  },
  "key1": "updatedValue1",
  "key2": "updatedValue2",
  "previousState": {
    "_id": {
      "$oid": "677bd298948f20de2040b43f"
    },
    "key1": "value1",
    "key2": "value2"
  }
}

Keeping new: true will return me the modified document, and the previousState field will have the old state of the document that was updated.

This will definitely have negative impacts:

  1. Storage impact: If document is large, then this could quickly lead to storage issues as you're essentially storing the document 2 times.

  2. Performance impact: Extra step to store the document in a field will also affect performance negatively.

So, need to use with caution. I think it will be fine with small document sizes.

Upvotes: 0

rajadilipkolli
rajadilipkolli

Reputation: 3601

No, there's no way to return both old and new value with findAndModify.

But if you want to compare entitiy state before and after updating do the below steps

  1. Just before updating retrieve the data using primary key and store in oldData variable
  2. in your findAndModify query change returnNew to true to save it under newData

Below sample sudocode will return you both values

    public Pair<Data> saveItems(String id, List<Item> items) {
    final Query findById = ...;

    final Update update = new Update();
    // This is what we actually update
    update.set(ENTITY_FIELD_ITEMS, newItems);
    update.inc(ENTITY_FIELD_VERSION, 1);
    // Try updating and return the old data 
    final Data oldData = findById(); //query to retrieve existing data
    final Data newData = operations.findAndModify(findById, update,
        FindAndModifyOptions.options().upsert(true).returnNew(true), Data.class);

      return new Pair<Data>(oldData, newData);
    }

Upvotes: 0

Sede
Sede

Reputation: 61273

Is there some way to return both old and new entities from findAndModify?

No, there's no way to return both old and new value with findAndModify.

Upvotes: 5

Related Questions