Il y a 11 ans -

Temps de lecture 8 minutes

Jongo, requêter en Java comme dans Mongo shell

Mongo — la base de données NoSQL orientée document supportée par 10gen — propose un langage de requête compact, simple à appréhender et bien documenté. Les choses se corsent dès que l’on bascule en Java : effectuer une requête, mapper les résultats, utiliser de l’héritage requiert un code très verbeux. Des librairies essayent de simplifier cela (comme Morphia), mais aucune ne propose de requêter en natif. Jongo tente de répondre à ce besoin, utilisant des chaînes de caractères pour requête et unmarshallant les résultats en objets Java.

Mongo avec le shell

Mongo stocke ses données sous la forme de documents BSON, des arbres attribut:valeur (valeur de type simple, tableau ou objet imbriqué). Les documents sont enregistrés au sein de collections, les collections au sein de bases. Mongo est schemaless, les documents d’une même collection peuvent n’avoir aucun attribut en commun, aucune contrainte ne pèse sur eux.

Les commandes suivantes ajoutent deux documents dans la collection friends. Cette collection est automatiquement créée si elle n’existe pas déjà.

> 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'})

Une fois créée, la collection peut être requêtée à l’aide de nombreux opérateurs (plus grand que, non vide, etc). Ces requêtes peuvent porter sur tous les attributs, y compris ceux imbriqués.

> 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}
]

La documentation en ligne de Mongo liste tous les opérateurs disponibles. Elle explique également comment installer Mongo en quelques instants.

Mongo avec Jongo

Afin de manipuler et de sauvegarder des données dans Mongo en Java, nous allons définir deux classes, Friend et Address. Ces classes seront utilisées telles quelles par le driver Java et Jongo. Leurs attributs sont privés ; les constructeurs avec arguments ne sont pas répétés ici par soucis de concision.

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 accède à Mongo via le driver Java et s’appuie sur la librairie de marshalling Jackson — réputée pour sa performance — afin d’offrir une expérience d’utilisation de Mongo en Java aussi confortable que celle du shell. Le test qui suit initialise une connexion à la base xebia, puis à sa collection friends dont il supprime tous les documents après chaque 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();
  }
}

Une fois initialisée, la classe MongoCollection représentant la collection friends permet de sauvegarder directement des objets Java.

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);
  }
}

Une fois des documents créés — rien de bien sorcier — les requêtes sont les mêmes qu’avec le shell. Ensuite, chaîner la méthode as(Class) permet de spécifier le type dans lequel convertir les résultats obtenus (à la simple condition de la présence d’un constructeur vide pour cette classe — même privé). Les attributs absents du résultat de la requête sont laissés à null, ceux présents dans le résultat mais absents de l’objet sont ignorés (il est ainsi possible de manipuler des documents très volumineux avec des objets très simples).

Reprenons nos deux requêtes initiales.

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 met à disposition la majorité des opérations Mongo — save, update, insert, remove, count, sort, distinct — en essayant, pour chacune d’entres elles, de demeurer fidèle au shell. En voici un bref aperçu.

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 est disponible sur le repository central de maven.

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

Mongo avec le driver Java

Afin de présenter les motivations de Jongo, rien de mieux qu’effectuer les opérations précédentes sans ; en utilisant seulement le driver Java.

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();
  }
}

Le driver ne permet pas de sauvegarder des objets Java ; il nécessite la création d’une imbrication de 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);
  }
}

Les résultats retournés par le driver Java sont également des BasicDBObject. Il est donc nécessaire d’adapter chacun de leurs champs dans leurs homologues objets.

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);
  }
}

Enfin, comme l’exemple précédent le montrait déjà, les requêtes nécessitent, elles aussi, une adaptation. Les deux requêtes initiales, si concises, doivent ici être reconstruites à l’aide d’un QueryBuilder (ou via une imbrication de BasicDBObject) et perdent en lisibilité.

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
  }
}

Épilogue : rejoignez Jongo

La documentation en ligne de Jongo présente l’intégralité des opérations disponibles avec, notamment, la manière de gérer le polymorphisme, les identifiants et les requêtes paramétrées. Jongo est un projet open-source, hébergé sur GitHub et sous licence Apache 2.0. Sa mailing-list est l’endroit idéal pour reporter un problème ou proposer une nouvelle fonctionnalité. Les pull requests sont les bienvenues ; les nouveaux contributeurs aussi.

Après avoir été accueilli par le Paris Mongo User Group et le MongoDB Paris 2012, Jongo sera présenté au prochain Paris Java User Group, le 3 juillet 2012.

La version 0.2 sera livrée d’ici quelques jours, elle intègre notamment la gestion du nouveau framework d’agrégation qui sortira avec la future version 2.2 de Mongo.

Publié par Benoît Guérout

Benoît évolue depuis 7 ans — dont 2 chez Xebia — sur des architectures orientées Web et affectionne une approche simple et pragmatique des développements. HTML5, Backbone, CSS3, Mongo+Jongo, Rest sont ses passions du moment. Kiss !

Publié par Yves Amsellem

Développeur depuis 5 ans — les 2 derniers chez Xebia — Yves tire de son expérience sur des sites à fort trafic une culture de la qualité, de l'effort commun et de l'innovation. Spécialisé du style d'architecture ReST, il intervient sur des projets web à forte composante JavaScript et NoSQL. Avec Benoît Guérout, il développe la librairie open source Jongo — Query in Java as in Mongo shell

Commentaire

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.