I’ve recently switched my project at work to use MongoDB for the user database and a few other datasets.
Currently I don’t use many JavaScript functions, but when I do I like to store them on the server so that they’re accessible when I’m poking around in a console.
I use something similar to the following function to load all of my JS functions onto the server when my app starts:
import os import pymongo import pkg_resources # Relative to distribution's root SCRIPT_DIR = os.path.join('model', 'js') def init_js(db): '''Initializes server-side javascript functions''' scripts = filter( lambda f: f.endswith('.js'), pkg_resources.resource_listdir(__name__, SCRIPT_DIR) ) for script in scripts: # Name the function after the script name func_name, _ = script.split('.', 1) script_path = os.path.join(SCRIPT_DIR, script) # Create a pymongo Code object # otherwise it will be stored as a string code = pymongo.code.Code( pkg_resources.resource_string(__name__, script_path)) # Upsert the function db.system.js.save({ '_id': func_name, 'value': code, })
However, using server-side functions from Python is awkward at best. Say I have the JavaScript function:
add.js
function(x, y) { return x + y; }
To run that function via PyMongo requires wrapping the function call with placeholder parameters in a Code object and passing in values as a dict:
var1 = 1 var2 = 2 result = db.eval(pymongo.code.Code('add(a, b)', {'a': var1, 'b': var2,})) assert result == 3
Update: See MongoDB dev Mike Dirolf comment to see a much more concise way of executing server-side functions.
Bearable for simple functions, but having to manually map parameters to values is tiresome and error prone with longer function signatures.
What I wanted was something more natural like:
var1 = 1 var2 = 2 result = db.add(var1, var2) assert result == 3
I use a simple PyMongo Database object wrapper to make my life easier:
import string from pymongo.code import Code class ServerSideFunctions(object): def __init__(self, db): self.db = db def func_wrapper(self, func): '''Returns a closure for calling a server-side function.''' params = [] # To keep params ordered kwargs = {} def server_side_func(*args): '''Calls server side function with positional arguments.''' # Could be removed with better param generating logic if len(args) > len(string.letters): raise TypeError('%s() takes at most %d arguments (%d given)' % (func, len(string.letters), len(args))) # Prepare arguments for k, v in zip(string.letters, args): kwargs[k] = v params.append(k) # Prepare code object code = Code('%s(%s)' % (func, ', '.join(params)), kwargs) # Return result of server-side function return self.db.eval(code) return server_side_func def __getattr__(self, func): '''Return a closure for calling server-side function named `func`''' return self.func_wrapper(func) dbjs = ServerSideFunctions('foo') var1 = 1 var2 = 2 result = dbjs.add(var1, var2) assert result == 3
I’m tempted to monkey-patch PyMongo’s Database class to add a ServerSideFunctions instance directly as a js attribute, so then I could drop the confusing dbjs variable and just use:
assert db.js.add(1,2) == 3
If someone knows of a better way to access server-side MongoDB functions from Python, please let me know!
I modified this code to remove code specific to my project, so please let me know if there are errors.
Tags: javascript, mongodb, pymongo
One thing that might be a bit nicer would be to define a new anonymous function to call your server-side one. So instead of:
db.eval(pymongo.code.Code(‘add(a, b)’, {‘a’: var1, ‘b’: var2,}))
You could do:
db.eval(“function(a,b) {return add(a,b);}”, var1, var2)
You won’t need to wrap as code since we don’t need the scope anymore. Still not quite as nice as your helper makes it.
Thanks Mike! That’s much better than my initial attempt. Updated the post to point people to your example code.
you can store your functions in mongodb itself. there’s a spething thing for it,
[...] Making Server-Side MongoDB Functions Less Awkward « michael schurter [...]
There’s a ticket to track this feature now:
http://jira.mongodb.org/browse/PYTHON-86
And some sample code:
http://github.com/schmichael/mongo-python-driver/commit/57e6df4ca14314eb8c0a3a9e4b8c97c0186a0121
This functionality has been added in 1.4+, accessible through the db.system_js property:
>>> db.system_js.add = “function(a, b) { return a + b; }”
>>> db.system_js.add(5, 4)
9
>>> del db.system_js.add
Is there a way to execute a javascript function that’s stored on disk from the Mongodb shell?