Jongo, query in Java as in Mongo shell

yamsellem

Mongo — the document oriented NoSQL database supported by 10gen — offers a compact, easy to learn and well documented query language. Unfortunately, using Mongo with its Java driver can be tricky: querying, mapping results and handling polymorphism require lots of code. Some libraries aim to simplify this (like Morphia), but none allows to query in a shell fashion. Jongo tries to fill that need, querying with the use of strings and unmarshalling results into Java objects.

Mongo with its shell

Mongo stores its datas into BSON documents, an attribute:value tree (value can be simple, array or embedded object). Documents are stored into collections, collections into databases. Mongo is schemaless, documents of a same collection don't have to have the same attributes, they can be completely different.

The following commands add two documents into the friends collection. This collection is automatically created if it does not exist.

> db.friends.save({name:'Joe', age:17, address: {city:'Paris', zip:75018})
> db.friends.save({name:'Jack', age:19, address: {city:'New York', street:'22 Madison Ave'})

Once created, the collection can be requested through numerous operators (greater than, not empty, etc). Those requests can be on every attributes, even sub nodes.

> db.friends.find()
[
  {name:'Joe', age:17, address: {city:'Paris', zip:75018},
  {name:'Jack', age:19, address: {city:'New York', street:'22 Madison Ave'}
]
 
> db.friends.findOne({age: {$gt: 18}})
{name:'Jack', age:19, address: {city:'New York', street:'22 Madison Ave'}
 
> db.friends.find({'address.zip':{$exists:true}, 'address.city':'Paris'})
[
  {name:'Joe', age:17, address: {city:'Paris', zip:75018}
]

The online Mongo documentation lists all the available operators. It also explain how to install Mongo in seconds.

Mongo with Jongo

We will use two classes, Friend and Address, to manipulate and store data into Mongo in Java. Those classes will be used without modification by the Java driver and Jongo. Their attributes are privates; constructors with arguments are not shown here for brevity's sake.

public class Friend {
  private String name;
  private int age;
  private Address address;
 
  Friend() {}
  // constructor with attributes here
}
 
public class Address {
  private String city;
  private Integer zip;
  private String street;
 
  Address() {}
  // constructor with attributes here
}

Jongo get access to Mongo with its Java driver and relies upon the Jackson marshalling library — well known for its performance — to offer the comfortable Mongo shell experience in Java. The following test initiate a connection to the xebia base, and then to the friends collection; every document of this collection is removed after each test.

public class JongoTest {
  MongoCollection collection;
 
  @Before
  public void setUp() throws Exception {
    Mongo mongo = new Mongo("127.0.0.1", 27017);
    DB db = mongo.getDB("xebia");
 
    Jongo jongo = new Jongo(db);
    collection = jongo.getCollection("friends");
  }
 
  @After
  public void tearDown() {
    collection.drop();
  }
}

Once initialized, the MongoCollection class representing the friends collection, allows to directly save Java objects.

public class JongoTest {
  @Before
  public void setUp() throws Exception {
    collection.save(new Friend("Joe", 17, new Address("Paris", 75000)));
    collection.save(new Friend("Jack", 19, new Address("New York", "22 Madison Ave")));
  }
 
  @Test
  public void findAll() throws Exception {
    Iterable<Friend> friends = collection.find().as(Friend.class);
    assertThat(friends).hasSize(2);
  }
}

Once documents are created — no surprises — requests are the same as in the shell. Then, the as(Class) method define the type into which the results have to be converted (the class must have a no-args constructor — even private). Results missing attributes are left to null, result attributes not in the object are ignored (huge collection can be managed with tiny objects that way).

With our previous requests, it becomes the following.

public class JongoTest {
  @Test
  public void findOver18() throws Exception {
    Friend jack = collection.findOne("{age: {$gt: 18}}").as(Friend.class);
 
    assertThat(jack.getName()).isEqualTo("Jack");
    assertThat(jack.getAddress().getZip()).isNull();
    assertThat(jack.getAddress().getStreet()).isEqualTo("22 Madison Ave");
    // [...] other asserts
  }
 
  @Test
  public void findInParis18() throws Exception {
    Iterable<Friend> friends =
      collection.find("{'address.zip':{$exists:true},'address.city':'Paris'}")
        .as(Friend.class);
 
    Friend joe = friends.iterator().next();
    assertThat(joe.getName()).isEqualTo("Joe");
    assertThat(joe.getAddress().getZip()).isEqualTo(75000);
    assertThat(joe.getAddress().getStreet()).isNull();
    assertThat(friends.iterator().hasNext()).isFalse();
    // [...] other asserts
  }
}

Jongo offers the vast majority of Mongo operations — save, update, insert, remove, count, sort, distinct — and tries, for every one of them, to be as faithfully as possible to the Mongo shell spirit. The following tests give an insight.

public class JongoTest {
  @Test
  public void sort() throws Exception {
    Iterable<Friend> friends = collection.find().sort("{name: -1}").as(Friend.class);
    assertThat(friends).onProperty("name").containsExactly("Joe", "Jack");
  }
 
  @Test
  public void distinct() {
    List<Address> addresses = collection.distinct("address").as(Address.class);
    assertThat(addresses).onProperty("city").contains("Paris", "New York");
  }
 
  @Test
  public void count() {
    long count = collection.count("{name: 'Joe'}");
    assertThat(count).isEqualTo(1);
  }
 
  @Test
  public void crud() {
    Friend joe = collection.findOne("{name: 'Joe'}").as(Friend.class);
    assertThat(joe.getAge()).isEqualTo(17);
 
    collection.update("{name: 'Joe'}").with("{$inc: {age: 1}}");
    joe = collection.findOne("{name: 'Joe'}").as(Friend.class);
    assertThat(joe.getAge()).isEqualTo(18);
 
    collection.remove("{name: 'Joe'}");
    joe = collection.findOne("{name: 'Joe'}").as(Friend.class);
    assertThat(joe).isNull();
  }
}

Jongo is available in the maven central repository.

<dependency>
  <groupId>org.jongo</groupId>
  <artifactId>jongo</artifactId>
  <version>0.1</version>
</dependency>

Mongo with its Java driver

The best way to introduce the reasons behind Jongo is to not use it; let's see how to use Mongo directly with its Java driver.

public class MongoJavaDriverTest {
  DBCollection friends;
 
  @Before
  public void setUp() throws Exception {
    Mongo mongo = new Mongo("127.0.0.1", 27017);
    friends = mongo.getDB("xebia").getCollection("friends");
  }
 
  @After
  public void tearDown() {
    friends.drop();
  }
}

The driver do not offers to save Java objects; it requires a structure of BasicDBObject.

public class MongoJavaDriverTest {
  @Before
  public void setUp() throws Exception {
    friends.save(newBasicDBObject("Joe", "Paris", ...));
    friends.save(newBasicDBObject("Jack", "New York", ...));
  }
 
  private BasicDBObject newBasicDBObject(String name, String city, ...) {
    BasicDBObject obj = new BasicDBObject();
    obj.put("name", name);
    obj.put("address", new BasicDBObject("city", city));
    // [...] should adapt every field
    return obj;
  }
 
  @Test
  public void findAll() throws Exception {
    DBCursor find = friends.find();
    assertThat(find.size()).isEqualTo(2);
  }
}

The driver's results are BasicDBObject too. Thus, it is necessary to adapt every fields to the corresponding Java attribute.

public class MongoJavaDriverTest {
  @Test
  public void findOver18() throws Exception {
    DBObject findOne = friends.findOne(
      QueryBuilder.start("age").greaterThan(18).get()
    );
 
    Friend jack = adaptDBObjectToFriend(findOne);
    assertThat(jack.getName()).isEqualTo("Jack");
    // [...] test every field
  }
 
  private Friend adaptDBObjectToFriend(DBObject dbFriend) {
    String name = (String) dbFriend.get("name");
    DBObject dbAddress = (DBObject) dbFriend.get("address");
    Address address = new Address((String) dbAddress.get("city"), ...);
    // [...] should adapt every field
    return new Friend(name, age, address);
  }
}

At last, as shown in the previous example, requests have to be adapted too. The two initial requests, so concise, have to be rewritten with a QueryBuilder (or using a BasicBBObject structure) and looses their user-friendliness.

public class MongoJavaDriverTest {
  @Test
  public void findInParis18() throws Exception {
    DBCursor find = friends.find(
      QueryBuilder.start("address.zip").exists(true)
        .and("address.city").is("Paris").get()
    );
 
    assertThat(find.size()).isEqualTo(1);
 
    Friend joe = adaptDBObjectToFriend(find.next());
    assertThat(joe.getName()).isEqualTo("Joe");
    // [...] test every field
  }
}

Epilogue: Join Jongo

The online Jongo documentation presents all the operators with, notably, the way to handle polymorphism, identifiers and request parameters. Jongo is an open-source project, hosted on GitHub and under Apache 2.0 licensing. Its mailing-list is the best place to report an issue or submit a new functionality. Pull requests are welcomed; so as new commiters.

After been invited at the Paris Mongo User Group and at the MongoDB Paris 2012, Jongo will be at the next Paris Java User Group, the third of July 2012.

The 0.2 release will be out in a couple of days, it will notably bring support for the brand new aggregation framework that will come with the Mongo 2.2 future release.

Comments (0)

    Add a Comment