In this recipe, we will see how to write server stored JavaScript similar to stored procedures in relational databases. This is a common use case where other pieces of code require access to these common functions and we have them in one central place. To demonstrate server-side scripts, the function will simply add two numbers.
There are two parts to this recipe. First, we see how to load the scripts from the collections on the client-side JavaScript shell and secondly, we will see how to execute these functions on the server.
The documentation specifically mentions that it is not recommended to use server-side scripts. Security is one concern though if the data is not properly audited and hence we need to be careful with what functions are defined. Since Mongo 2.4, the server-side JavaScript engine is V8, which can execute multiple threads in parallel as opposed to the engine prior to version 2.4 of Mongo, which executes only one thread at a time.
Look at the recipe Installing single node MongoDB in Chapter 1, Installing and Starting the Server and start a single instance of Mongo. That is the only prerequisite for this recipe. Start a mongo shell and connect to the started server.
add
and save it to the collection db.system.js
as follows. The current database should be test:> use test > db.system.js.save({ _id : 'add', value : function(num1, num2) {return num1 + num2}})
> db.loadServerScripts()
add
and see if it works:> add(1, 2)
> use test > db.eval('return add(1, 2)')
> use test1 > db.eval('return add(1, 2)')
The collection system.js
is a special MongoDB collection used to store JavaScript code. We add a new server-side JavaScript using the save
function in this collection. The save
function is just a convenience function that inserts the document if it is not present or updates an existing one. The objective is to add a new document to this collection which you may add even using insert
or upsert
.
The secret lies in the method loadServerScripts
. Let's look at the code of this method: this.system.js.find().forEach(function(u){eval(u._id + " = " + u.value);});
It evaluates a JavaScript using the eval
function and assigns the function defined in the value
attribute of the document to a variable named with the name given in the _id
field of the document for each document present in the collection system.js
.
For example, if the following document is present in the collection system.js
, { _id : 'add', value : function(num1, num2) {return num1 + num2}}
, then the function given in the value
field of the document will be assigned to the variable named as add
in the current shell. The value add
is given in the _id
field of the document.
These scripts do not really execute on the server but their definition is stored on the server in a collection. The JavaScript method loadServerScripts
, just instantiates some variables in the current shell and make those functions available for invocation. It is the JavaScript interpreter of the shell that executes these functions and not the server. The collection system.js
is defined in the scope of the database. Once loaded, these act as JavaScript functions defined in the shell and hence the functions are available throughout the scope of the shell irrespective of the database currently active.
As far as security is concerned, if the shell is connected to the server with security enabled, then the user invoking loadServerScripts
must have privileges to read the collections in the database. For more details on enabling security and various roles a user can have, refer to the recipe Setting up users in Mongo in Chapter 4, Administration. As we saw earlier, the function loadServerScripts
reads data from the collection system.js
and if the user doesn't have privileges to read from the collection, the function invocation will fail. Apart from that, the functions executed from the shell after being loaded should have appropriate privileges. For instance, if a function inserts/updates in any collection, the user should have read and write privileges on that particular collection accessed from the function.
Executing scripts on the server is perhaps what one would expect to be server-side script as opposed to executing in the shell connected. In this case, the functions are evaluated on the server's JavaScript engine and the security checks are more stringent as long running functions can hold locks, having detrimental effects on the performance. The wrapper to invoke the execution of a JavaScript code on the server-side is the db.eval
function accepting the code to evaluate on the server-side along with the parameters if any.
Before evaluating the function, the write operation takes a global lock; this can be skipped if the parameter nolock
is used. For instance, the preceding add
function can be invoked as follows instead of calling db.eval
and achieving the same results. We additionally provided the nolock
field to instruct the server not to acquire the global lock before evaluating the function. If this function were to perform write operations on a collection, then the nolock
field is ignored.
> db.runCommand({eval: function (num1, num2) {return num1 + num2}, args:[1, 2],nolock:true})
If security is enabled on the server, the invoking user needs to have the following four roles: userAdminAnyDatabase
, dbAdminAnyDatabase
, readWriteAnyDatabase
, and clusterAdmin
(on the admin database) to successfully invoke the db.eval
function.
Programming languages do provide a way for invocation of such server-side scripts using the eval
function. For instance, in Java API, the class com.mongodb.DB
has the method eval
to invoke server-side JavaScript code. Such server-side executions are highly useful when we want to avoid unnecessary network traffic for the data and get the result to the clients. However, too much logic on the database server can quickly make things difficult to maintain and affect the performance of the server badly.
As of MongoDB 3.0.3, the db.eval()
method is being deprecated and it is advised that users do not rely on this method but instead use client-side scripts. See https://jira.mongodb.org/browse/SERVER-17453 for more details.