Server-side Code Execution

Mongo supports the execution of code inside the database process.

Map/Reduce

MongoDB supports Javascript-based map/reduce operations on the server. See the map/reduce documentation for more information.

Using db.eval()

Use map/reduce instead of db.eval() for long running jobs. db.eval blocks other operations!

db.eval() is used to evaluate a function (written in JavaScript) at the database server.
This is useful if you need to touch a lot of data lightly. In that scenario, network transfer of the data could be a bottleneck.

db.eval() returns the return value of the function that was invoked at the server. If invocation fails an exception is thrown.

For a trivial example, we can get the server to add 3 to 3:

> db.eval( function() { return 3+3; } );
6
>

Let's consider an example where we wish to erase a given field, foo, in every single document in a collection. A naive client-side approach would be something like

function my_erase() {
  db.things.find().forEach( function(obj) {
                       delete obj.foo;
                       db.things.save(obj);
                     } );
}

my_erase();

Calling my_erase() on the client will require the entire contents of the collection to be transmitted from server to client and back again.

Instead, we can pass the function to eval(), and it will be called in the runtime environment of the server. On the server, the db variable is set to the current database:

db.eval(my_erase);

Examples

> myfunc = function(x){ return x; };

> db.eval( myfunc, {k:"asdf"} );
{ k : "asdf"  }

> db.eval( myfunc, "asdf" );
"asdf"

> db.eval( function(x){ return x; }, 2 );
2.0

If an error occurs on the evaluation (say, a null pointer exception at the server), an exception will be thrown of the form:

{ dbEvalException: { errno : -3.0 , errmsg : "invoke failed" , ok : 0.0  } }

Example of using eval() to do equivalent of the Mongo count() function:

function mycount(collection) {
  return db.eval( function(){return db[collection].find({},{_id:ObjId()}).length();} );
}

Example of using db.eval() for doing an atomic increment, plus some calculations:

function inc( name , howMuch ){
    return db.eval(
        function(){
            var t = db.things.findOne( { name : name } );
            t = t || { name : name , num : 0 , total : 0 , avg : 0 };
            t.num++;
            t.total += howMuch;
            t.avg = t.total / t.num;
            db.things.save( t );
            return t;
        }
    );
}

db.things.remove( {} );
print( tojson( inc( "eliot" , 2 )) );
print( tojson( inc( "eliot" , 3 )) );

Limitations of eval

Write locks

It's important to be aware that by default eval takes a write lock. This means that you can't use eval to run other commands that themselves take a write lock. To take an example, suppose you're running a replica set and want to add a new member. You may be tempted to do something like this from a driver:

db.eval("rs.add('ip-address:27017')");

As we just mentioned, eval will take a write lock on the current node. Therefore, this won't work because you can't add a new replica set member if any of the existing nodes is write-locked.

The proper approach is to run the commands to add a node manually. rs.add simply queries the local.system.replSet collection, updates the config object, and run the replSetReconfig command. You can do this from the driver, which, in addition to not taking out the eval write lock, manages to more directly perform the operation.

In 1.7.2, a nolock option was added to eval. To use nolock you have to use the command interface directly:

db.runCommand({$eval: function() {return 42;}, nolock: true})

or with args

db.runCommand({$eval: function(x,y) {return x*y;}, args: [6,7], nolock: true})
Sharding

Note also that eval doesn't work with sharding. If you expect your system to later be sharded, it's probably best to avoid eval altogether.

Storing functions server-side

Note: we recommend not using server-side stored functions when possible. As these are code it is likely best to store them with the rest of your code in a version control system.

There is a special system collection called system.js that can store JavaScript functions to be reused. To store a function, you would do:

db.system.js.save( { _id : "foo" , value : function( x , y ){ return x + y; } } );

_id is the name of the function, and is unique per database.

Once you do that, you can use foo from any JavaScript context (db.eval, $where, map/reduce)

Here is an example from the shell:

>db.system.js.save({ "_id" : "echo", "value" : function(x){return x;} })
>db.eval("echo('test')")
test

See http://github.com/mongodb/mongo/tree/master/jstests/storefunc.js for a full example.

In MongoDB 2.1 you will also be able to load all the scripts saved in db.system.js into the shell using db.loadServerScripts()

>db.loadServerScripts()
>echo(3)
3

$where Clauses and Functions in Queries

In addition to the regular document-style query specification for find() operations, you can also express the query either as a string containing a SQL-style WHERE predicate clause, or a full JavaScript function. Note: if a normal data-driven BSON query expression is possible, use that construction. Use $where only when you must it is significantly slower.

When using this mode of query, the database will call your function, or evaluate your predicate clause, for each object in the collection.

In the case of the string, you must represent the object as "this" (see example below). In the case of a full JavaScript function, you use the normal JavaScript function syntax.

The following four statements in mongo - The Interactive Shell are equivalent:

db.myCollection.find( { a : { $gt: 3 } } );
db.myCollection.find( { $where: "this.a > 3" });
db.myCollection.find( "this.a > 3" );
db.myCollection.find( { $where: function() { return this.a > 3;}});

The first statement is the preferred form. It will be faster to execute because the query optimizer can easily interpret that query and choose an index to use.

You may mix data-style find conditions and a function. This can be advantageous for performance because the data-style expression will be evaluated first, and if not matched, no further evaluation is required. Additionally, the database can then consider using an index for that condition's field. To mix forms, pass your evaluation function as the $where field of the query object. For example, both of the following would work:

db.myCollection.find( { active: true, $where: function() { return obj.credits - obj.debits < 0; } } );
db.myCollection.find( { active: true, $where: "this.credits - this.debits < 0" } );

Do not write to the database from a $where expression.

Notes on Concurrency

If you don't use the "nolock" flag, db.eval() blocks the entire mongod process while running. Thus, its operations are atomic but prevent other operations from processing.

When more concurrency is needed consider using map/reduce instead of eval().

Running .js files via a mongo shell instance on the server

This is a good technique for performing batch administrative work. Run mongo on the server, connecting via the localhost interface. The connection is then very fast and low latency. This is friendlier than db.eval() as db.eval() blocks other operations.


Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.

PLEASE POST QUESTIONS IN THE USER GROUPS FORUM. Post non-question comments and helpful hints here.

blog comments powered by Disqus