Writing and maintaining stored procedures in a SQL database was both painful and annoying to manage, test, and easily encapsulate in code. I usually ended up with an interface defining all of the stored procedures and a lengthy class implementation with JDBC templates or lines of setup and teardown code for each procedure to run. If the procedure changed on the server, I needed to change the interface and a bit of code in the implementation. As much as I hated maintaining this, I know that stored procedures are necessary.
MongoDB has a version of stored procedures. This is javascript code that you can store in your database and call using the eval command. It’s a very simple system to use, although not completely intuitive to SQL people. Just write your javascript code and then save it into the system.js collection within the database you want to call it from.
> db.system.js.save({
_id: 'hello_world',
value: function() {
print('hello world!')
}
})
When using the MongoDB shell, you have to realize that these functions will not be available to call directly. You must use an eval. So to call our hello_world function, we must do this:
> db.eval('hello_world()')
This will print out “null” in the shell window (since we don’t return a value) and “hello world!” in the server log (or server output if you ran it from a command shell). You can return any value that you want except cursors. So returning the number 5 or {value: 5} is allowed.
Now, onto the Java code. Let’s assume that you have two methods saved in your test database defined as follows:
> db.system.js.save({
_id: 'getCount',
value: function() {
return db.data.count()
}
})
> db.system.js.save({
_id: 'getData',
value: function(count) {
return db.data.find().limit(count).toArray()
}
})
Your code might look something like this:
public class Data {
private static Logger _logger = Logger.getLogger(Data.class);
private final DB _database;
public Data(DB database) {
_database = database;
}
void doSomething() {
Integer count = (Integer)_database.eval("return getCount()");
if (count == null) {
_logger.error("Could not execute getCount");
return;
}
List data = (List)_database.eval(
"function(c){return getData(c)}",
Math.min(20, count)
);
if (data == null)
return;
if (data != null) {
for (DBObject obj : data) {
System.out.println(obj);
}
}
}
public static void main(String[] args) {
Mongo mongo;
try {
mongo = new Mongo();
} catch (MongoException e) {
// ...
} catch (UnknownHostException e) {
// ...
return;
}
DB database = mongo.getDB("test_db");
Data data = new Data(database);
data.doSomething();
}
}
With GuiceyMongo your code could look like this:
public interface DataQuery {
List getData(int count);
int getCount();
}
public class Data {
private static Logger _logger = Logger.getLogger(Data.class);
private final DataQuery _query;
@Inject
Data(DataQuery query) {
_query = query;
}
void doSomething() {
try {
int count = _query.getCount();
List data = _query.getData(Math.min(20, count));
if (data != null) {
for (DBObject obj : data) {
System.out.println(obj);
}
}
// throwing from a server-side method will be caught in a
// GuiceyMongoEvalException rather than a MongoEvalException
} catch (GuiceyMongoEvalException e) {
_logger.error(e);
}
}
public static void main(String[] args) {
Injector injector = Guice.createInjector(
GuiceyMongo.configure("Test")
.mapDatabase("Main").to("test_db"),
GuiceyMongo.javascriptProxy(DataQuery.class, "Main"),
GuiceyMongo.chooseConfiguration("Test")
);
Data data = injector.getInstance(Data.class);
data.doSomething();
}
}
For more on configuring your databases and collections, check out GuiceyMongo – Injecting databases and collections.
Changes in the server method only need to be reflected in the interface definition. All implementation code is taken care of for you.
Using a query interface and Google Guice injection also makes mocking and writing tests much easier. This approach as well as some other tricks I’ll post in the future have saved me a whole lot of time.
Enjoy!
If you would like to use GuiceyMongo, it is available to the public:
- information: Related Posts
- source: GuiceyMongo source
- jars: GuiceyMongo jars
I plan on maintaining and enhancing this library regularly and would love to hear your input! If you use my library, please leave me a comment so I know that people are finding it useful. Thanks!