Categories
database-versioning mongodb

Ways to implement data versioning in MongoDB

328

Can you share your thoughts how would you implement data versioning in MongoDB. (I’ve asked similar question regarding Cassandra. If you have any thoughts which db is better for that please share)

Suppose that I need to version records in an simple address book. (Address book records are stored as flat json objects). I expect that the history:

  • will be used infrequently
  • will be used all at once to present it in a “time machine” fashion
  • there won’t be more versions than few hundred to a single record.
    history won’t expire.

I’m considering the following approaches:

  • Create a new object collection to store history of records or changes to the records. It would store one object per version with a reference to the address book entry. Such records would looks as follows:

    {
     '_id': 'new id',
     'user': user_id,
     'timestamp': timestamp,
     'address_book_id': 'id of the address book record' 
     'old_record': {'first_name': 'Jon', 'last_name':'Doe' ...}
    }
    

    This approach can be modified to store an array of versions per document. But this seems to be slower approach without any advantages.

  • Store versions as serialized (JSON) object attached to address book entries. I’m not sure how to attach such objects to MongoDB documents. Perhaps as an array of strings.
    (Modelled after Simple Document Versioning with CouchDB)

3

  • 1

    I want to know if this has changed since the question was answered? I don’t know much about oplog but was this around at the time, would it make a difference?

    – Randy L

    Jul 31, 2014 at 23:40

  • My approach is to think of all data as a time series.

    – user636044

    Dec 3, 2015 at 18:40

  • MongoDB blog has described a simple approach: Building with Patterns: The Document Versioning Pattern

    Nov 7 at 21:59

167

The first big question when diving in to this is “how do you want to store changesets”?

  1. Diffs?
  2. Whole record copies?

My personal approach would be to store diffs. Because the display of these diffs is really a special action, I would put the diffs in a different “history” collection.

I would use the different collection to save memory space. You generally don’t want a full history for a simple query. So by keeping the history out of the object you can also keep it out of the commonly accessed memory when that data is queried.

To make my life easy, I would make a history document contain a dictionary of time-stamped diffs. Something like this:

{
    _id : "id of address book record",
    changes : { 
                1234567 : { "city" : "Omaha", "state" : "Nebraska" },
                1234568 : { "city" : "Kansas City", "state" : "Missouri" }
               }
}

To make my life really easy, I would make this part of my DataObjects (EntityWrapper, whatever) that I use to access my data. Generally these objects have some form of history, so that you can easily override the save() method to make this change at the same time.

UPDATE: 2015-10

It looks like there is now a spec for handling JSON diffs. This seems like a more robust way to store the diffs / changes.

16

  • 2

    Wouldn’t you worry that such History document (the changes object) will grow in time and updates become inefficient? Or does MongoDB handles document grow easily?

    Nov 16, 2010 at 7:33

  • 7

    Take a look at the edit. Adding to changes is really easy: db.hist.update({_id: ID}, {$set { changes.12345 : CHANGES } }, true) This will perform an upsert that will only change the required data. Mongo creates documents with “buffer space” to handle this type of change. It also watches how documents in a collection change and modifies the buffer size for each collection. So MongoDB is designed for exactly this type of change (add new property / push to array).

    – Gates VP

    Nov 17, 2010 at 5:59

  • 2

    I’ve done some testing and indeed the space reservation works pretty well. I wasn’t able to catch the performance loss when the records were reallocated to the end of the data file.

    Nov 27, 2010 at 10:03


  • 5

    You can use github.com/mirek/node-rus-diff to generate (MongoDB compatible) diffs for your history.

    Mar 11, 2014 at 18:05

  • 1

    The JSON Patch RFC provides a way to express difffs. It has implementations in several languages.

    – Jérôme

    Oct 26, 2015 at 16:25

33

There is a versioning scheme called “Vermongo” which addresses some aspects which haven’t been dealt with in the other replies.

One of these issues is concurrent updates, another one is deleting documents.

Vermongo stores complete document copies in a shadow collection. For some use cases this might cause too much overhead, but I think it also simplifies many things.

https://github.com/thiloplanz/v7files/wiki/Vermongo

3

  • 5

    How do you actually use it?

    – hadees

    Dec 18, 2012 at 18:49

  • 6

    There is no documentation on how this project is actually used. Is it something that lives withing Mongo somehow? It is a Java library? Is it merely a way of thinking about the problem? No idea and no hints are given.

    – ftrotter

    Feb 6, 2013 at 6:41

  • 1

    This is actually a java app and the relavant code lives here: github.com/thiloplanz/v7files/blob/master/src/main/java/v7db/…

    – ftrotter

    Feb 6, 2013 at 7:48


29

Here’s another solution using a single document for the current version and all old versions:

{
    _id: ObjectId("..."),
    data: [
        { vid: 1, content: "foo" },
        { vid: 2, content: "bar" }
    ]
}

data contains all versions. The data array is ordered, new versions will only get $pushed to the end of the array. data.vid is the version id, which is an incrementing number.

Get the most recent version:

find(
    { "_id":ObjectId("...") },
    { "data":{ $slice:-1 } }
)

Get a specific version by vid:

find(
    { "_id":ObjectId("...") },
    { "data":{ $elemMatch:{ "vid":1 } } }
)

Return only specified fields:

find(
    { "_id":ObjectId("...") },
    { "data":{ $elemMatch:{ "vid":1 } }, "data.content":1 }
)

Insert new version: (and prevent concurrent insert/update)

update(
    {
        "_id":ObjectId("..."),
        $and:[
            { "data.vid":{ $not:{ $gt:2 } } },
            { "data.vid":2 }
        ]
    },
    { $push:{ "data":{ "vid":3, "content":"baz" } } }
)

2 is the vid of the current most recent version and 3 is the new version getting inserted. Because you need the most recent version’s vid, it’s easy to do get the next version’s vid: nextVID = oldVID + 1.

The $and condition will ensure, that 2 is the latest vid.

This way there’s no need for a unique index, but the application logic has to take care of incrementing the vid on insert.

Remove a specific version:

update(
    { "_id":ObjectId("...") },
    { $pull:{ "data":{ "vid":2 } } }
)

That’s it!

(remember the 16MB per document limit)

2

  • With mmapv1 storage, everytime a new version is added to data, there is a possibility that document will be moved.

    – raok1997

    Jan 7, 2016 at 15:30

  • Yes, that’s right. But if you just add new versions every once in while, this should be neglectable.

    Jan 7, 2016 at 15:44