27

I know MongoDB doesn't support transactions as relational databases do, but I still wonder how to achieve atomicity for several operations. Hunting around the web, I see people mentioning Transactions without Transactions. Reading through the slides, I am still not clear how to implement that with Mongoose.js.

Take this code snippet for example:

player.save(callback1);
story.save(callback2);

How do I implement callback1 and callback2 so that they either succeed together or fail together?

Peter Lyons
  • 142,938
  • 30
  • 279
  • 274
Zebra Propulsion Lab
  • 1,736
  • 1
  • 17
  • 28
  • 3
    This question as nothing to do with mongoose or node.js. This link may help you http://docs.mongodb.org/manual/tutorial/perform-two-phase-commits/ – Jean-Philippe Leclerc Jul 03 '13 at 22:40
  • 2
    If you need transactions, choose another DB platform. MongoDB doesn't have transactions, and even the link above from @Jean-PhilippeLeclerc still offers only transaction-like behavior. And, it's certainly not as easy to implement as it would be with a transactional database. – WiredPrairie Jul 04 '13 at 00:41
  • possible duplicate of [MongoDB transactions?](http://stackoverflow.com/questions/2655251/mongodb-transactions) – WiredPrairie Jul 04 '13 at 00:43
  • MongoDB 4.0 now have transactions support. (release date : 2018-07) – Félix Brunet Aug 07 '18 at 13:27
  • Since MongoDB 4.0.0 (and Mongoose 5.2.0) there's now support for Transactions in MongoDB: https://mongoosejs.com/docs/transactions.html – João Otero Aug 16 '18 at 18:08

6 Answers6

9

If you really must have transactions across multiple documents types (in separate collections) the means to achieve this is with a single table that stores actions to take.

db.actions.insert(
{ actions: [{collection: 'players', _id: 'p1', update: {$set : {name : 'bob'} } },
            {collection: 'stories', _id: 's1', update: {$set : {location: 'library'} } }], completed: false }, callback);

This insert is atomic, and all done at once. You then can perform the commands in the 'actions' collection and mark them as complete or delete them as you complete them, calling your original callback when they are all completed. This only works if your actions processing loop is the only thing updating the db. Of course you'll have to stop using mongoose, but the sooner you do that the better you'll be anyway.

Will Shaver
  • 12,471
  • 5
  • 49
  • 64
  • how to use this using mongoose plugin in node.js – Sudhanshu Gaur Apr 23 '16 at 20:38
  • When Will says "you'll have to stop using mongoose" I think he means you'll no longer be able to use it freely on those collections. You can continue to use mongoose on your other collections that are never used for transactions, and your action processing loop can even do its work using mongoose, but you should only [update](https://github.com/niahmiah/mongoose-transact) the protected collections through that queue processing system. – joeytwiddle Jun 21 '16 at 08:00
9

This question is quite old but for anyone who stumbles upon this page, you could use fawn. It's an npm package that solves this exact problem. Disclosure: I wrote it

Say you have two bank accounts, one belongs to John Smith and the other belongs to Broke Individual. You would like to transfer $20 from John Smith to Broke Individual. Assuming all first name and last name pairs are unique, this might look like:

var Fawn = require("fawn");
var task = Fawn.Task()

//assuming "Accounts" is the Accounts collection 
task.update("Accounts", {firstName: "John", lastName: "Smith"}, {$inc: {balance: -20}})
  .update("Accounts", {firstName: "Broke", lastName: "Individual"}, {$inc: {balance: 20}})
  .run()
  .then(function(){
    //update is complete 
  })
  .catch(function(err){
    // Everything has been rolled back. 

    //log the error which caused the failure 
    console.log(err);
  });

Caveat: tasks are currently not isolated(working on that) so, technically, it's possible for two tasks to retrieve and edit the same document just because that's how MongoDB works.

It's really just a generic implementation of the two phase commit example on the tutorial site: https://docs.mongodb.com/manual/tutorial/perform-two-phase-commits/

e-oj
  • 209
  • 2
  • 5
  • 1
    But, If you want to use result from first transaction to be used in to the second one, how exactly can you do it? Example: let currLog = await Log.findOne({"id": logID}); let newUser = await User.save(payload); let updatedLog = await currLog.users.push(newUser._id); Is it possible using the FAWN module? So, if for some reason, LOG is not updated, The new user should be rolledback. – CoderX Mar 08 '18 at 09:01
  • This functionality is detailed in the [docs](https://github.com/e-oj/Fawn#using-the-result-of-previous-steps-in-subsequent-steps) – e-oj Mar 08 '18 at 10:47
  • I'll recommend you take advantage of MongoDB operators for effecting changes instead of rely on returned data from one operation. It depends heavily on your use case though – Jalasem Oct 27 '20 at 21:47
7

You can simulate a transaction by manually rolling-back the changes whenever an error occurs. In your above example, simply remove the saved document if the other one fails.

You can do this easily using Async:

    function rollback (doc, cb) {
      doc.remove(cb);
    }

    async.parallel([
          player.save.bind(player),
          story.save.bind(story),
      ],
      function (err, results) {
        if (err) {
          async.each(results, rollback, function () {
            console.log('Rollback done.');
          });
        } else {
          console.log('Done.');
        }
      });

Obviously, the rollback itself could fail -- if this is a not acceptable, you probably need to restructure your data or choose a different database.

Note: I discussed this in detail on this post.

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
jramoyo
  • 336
  • 4
  • 11
  • probably a good way to do it, but again, reinventing the RDBMs wheel – Alexander Mills Mar 03 '17 at 22:59
  • 1
    More frustrating than the rollback failing, is the threat of the server crashing at some point during this process. For production environments it's important to consider that the server can crash after the 2nd document fails, but before the rollback process is applied based on the failed 2nd document. Dealing with this case is especially tricky! – Gershom Maes Nov 17 '17 at 19:26
  • 1
    just be aware that this doesn't prevent race conditions when other requests perform the same actions in the DB. – Alexander Mills Dec 15 '17 at 20:46
6

Transactions are now supported in Mongoose >= 5.2.0 and MongoDB >= 4.0.0 (with replica sets)

https://mongoosejs.com/docs/transactions.html

João Otero
  • 948
  • 1
  • 15
  • 30
2

Transactions are not supported in MongoDB out of the box. However they are implementable with Two-phase commit protocol. Here you can see official recommendation on how to do 2PCs in MongoDB. This approach is quite universal and can be applied to different scenarios such as

  • documents from different collections (your case)
  • more than 2 documents
  • custom actions on documents
2

There have been a few attempts to implement solutions. None of them are particularly active at the time of writing. But maybe they work just fine!

niahmiah's README is worth looking at. It notes some disadvantages of using transactions, namely:

  • All updates and removals for the relevant collections should go through the transaction processor, even when you weren't intentionally doing a transaction. If you don't do this, then transactions may not do what they are supposed to do.
  • Those updates, as well as the transactions you initially wanted, will be significantly slower than normal writes.

niahmiah also suggests that there may be alternatives to using transactions.


Just a tip: If you don't like the idea of locking up your frequently used collections, you could consider separating the sensitive (transactional) data from the less sensitive data.

For example, if you are currently holding credits in the User collection, then you would be better off separating it into two collections: User and UserWallet, and moving the credits field into the wallets. Then you can freely update User documents without fear of interfering with transactions on the UserWallet collections.

joeytwiddle
  • 29,306
  • 13
  • 121
  • 110