Blog Zenika

#CodeTheWorld

Java

La persistance polyglotte avec Grails

Les bases de données relationnelles sont utilisées et maîtrisées depuis des années par les développeurs. Néanmoins, le modèle relationnel peut ne pas être optimal dans certains cas, notamment lors de traitements de très grosses données ou encore pour le stockage de fichier. Afin de répondre à ces problématiques mal gérées par les bases de données relationnelles, les développeurs ont assisté à l’émergence de nouveaux systèmes de gestion de bases de données (SGBD) dits NoSQL.

La persistance polyglotte

Le terme de persistance polyglotte (cf. Polyglot Persistence) a été introduit par Scott Leberknight dans le milieu des années 2000. Il tire son nom de la programmation polyglotte (cf. Polyglot Programming), un principe qui consiste à choisir le langage approprié pour répondre à une problématique spécifique. Ce principe sous-entend donc, la coexistence de différents langages dans une même application, en tirant profit des forces de chacun d’entre eux.
La persistance polyglotte ressemble en tout point à la programmation polyglotte sauf qu’elle se concentre non pas sur les langages de programmation mais sur les bases de données. Ainsi la persistance polyglotte consiste à faire coexister dans une même application plusieurs bases de données différentes. Évidemment, faire coexister des bases relationnelles ensemble comme MySQL et PostgreSQL, par exemple, n’a quasiment aucun intérêt (voire même tout simplement aucun). Cependant, coupler une base relationnelle à une base NoSQL comme MongoDB, Neo4J ou Redis prend tout son sens. Il revient alors au développeur de choisir le modèle de données adéquat à la résolution d’un problème donné.
Grails, à travers différents plugins, nous permet d’utiliser certains SGBD NoSQL. A l’heure actuelle, il existe un plugin pour MongoDB, Amazon DynamoDB, Amazon SimpleDB, Neo4j et Redis. Ces plugins s’intègrent très bien à GORM (Grails Object-Relational Mapping), l’implémentation de mapping objet-relationnel de Grails, basé sur Hibernate 3.

Installation et configuration

Afin d’utiliser la base de données MongoDB dans notre application Grails, nous allons avoir besoin d’installer le plugin correspondant. Pour cela, rien de plus simple, il suffit d’ouvrir une console Grails et d’exécuter la commande:
grails install-plugin mongodb
Vous devriez alors voir apparaître un message vous informant que le plugin a bien été installé.
A partir d’ici, vous avez déjà la possibilité d’utiliser la base MongoDB pour persister vos données dans votre application avec la configuration par défaut. Néanmoins, si vous souhaitez modifier la façon dont votre application se connecte au serveur, vous pouvez renseigner ces informations dans le fichier de configuration grails-app/conf/DataSource.groovy sous la forme :

grails {
   mongo {
       host = "localhost"
       port = 27017
       username = "username"
       password = "password"
       databaseName = "db_name"
   }
}

Renseigner la datasource à utiliser par nos domain-class

Actuellement, nous avons potentiellement deux SGBD différents qui peuvent être utilisés dans notre application. Cependant, nous devons définir quels objets seront stockés dans la base H2 et lesquels le seront dans la base MongoDB. Pour cela, nous avons deux possibilités:

  • La première consiste à renseigner directement dans nos domain-class quelle base de données sera utilisée grâce à l’attribut statique mapWith (La base de donnée H2 étant celle par défaut dans Grails, ne rien renseigner dans nos domain-class revient à l’utiliser).
static mapWith = "mongo"
  • La seconde consiste à renseigner, lors de chaque opération sur la base de données, celle qui doit être utilisée de la façon suivante (ne rien spécifier revient à utiliser la base de donnée par défaut soit H2).
def b = new Book()
b.mongo.save()

Le mapping de nos domain-class

Pour les domain-class devant s’appuyer sur MongoDB pour les opérations de base de données, GORM va mapper ces classes en DBCollections. Chaque classe représentera donc une collection Mongo mais nous avons la possibilité de définir un mapping différent de celui par défaut. Par exemple, Mongo nous permet d’avoir des documents imbriqués dans d’autres. Afin d’effectuer ce type de mapping nous devons utiliser la propriété statique embedded de GORM. Cette propriété nous permet de mapper des List ou Set d’objets de nos domain-class ou tout simplement un domain-class.

Author author
List<String> strings
static embedded = ['strings', 'auhtor']

Le plugin nous permet aussi de modifier le type utilisé pour l’indéxation des données, modifier le nommage de nos objets et attributs mappés, utiliser la capacité de “Geospatial Indexing” de MongoDB…

Illustration avec un exemple concret

Nous allons maintenant appliquer ce qui a été vu précédemment à travers une petite application. Celle-ci aura pour but de gérer un catalogue de produits, ici, des voitures. Nous partons du principe qu’une voiture peut, ou non, posséder un certain nombre d’options. Nous allons stocker ces voitures dans une base MongoDB en tirant profit de l’architecture “schemaless” des collections Mongo. Parallèlement, nous allons stocker dans la base H2 les propriétaires de ces mêmes voitures. Voici à quoi ressembleront nos domain-class:

class Car {
    static mapWith= "mongo"
    String brand
    String model
    String color
    static mapping = {
        brand blank: false
        model blank: false
        color blank: false
    }
}
class Owner {
    String name
    static mapping = {
        name blank: false
    }
}

Nous allons ensuite créer un nouveau test d’intégration afin de tester l’ajout et le requêtage de nos objets. Commençons par instancier un objet Car et un objet Owner et sauvons les en base:

Owner owner = new Owner(name: 'John')
Car car = new Car(brand: 'Renault', model:  'C5', color:  'black', owner:  owner)
owner.save(flush:  true)
car.save(flush:  true)

Ouvrons maitenant une console Mongo afin de tester que l’ajout a bien été éffectué. En exécutant la commande db.car.find({}), on note bien l’existence d’un objet Car dans la base MongoDB:

{ "_id" : NumberLong(1), "brand" : "Renault", "color" : "black", "model" : "C5", "owner" : DBRef("owner", NumberLong(1)), "version" : 0 }

Nous venons de montrer que l’ajout de nos obets dans deux bases distinctes fonctionnait. Nous souhaitons maintenant ajouter des Car dans notre base en ajoutant dynamiquement des attributs correspondants aux diverses options que possède notre voiture. Dans l’exemple suivant, nous admettons que la voiture est équipée des options de vitre éléctrique et de transmission automatique:

def carWithOptions = new Car(brand: 'Peugeot', model: '607', color: 'grey', owner: owner)
carWithOptions['powerWindows'] = true                 // attribut ajouté dynamiquement
carWithOptions['automaticTransmission'] = true      // attribut ajouté dynamiquement
carWithOptions.save(flush: true)

En listant une nouvelle fois le contenu de la collection MongoDB on obtient le résultat suivant :

{ "_id" : NumberLong(9), "brand" : "Renault", "color" : "black", "model" : "C5", "owner" : DBRef("owner", NumberLong(1)), "version" : 0 }
{ "_id" : NumberLong(10), "brand" : "Peugeot", "color" : "grey", "model" : "607", "owner" : DBRef("owner", NumberLong(1)), "version" : NumberLong(1), "powerWindows" : true, "automaticTransmission" : true }

Nous avons ici une application (si on peut appeller ces quelques lignes de code une application) qui, grâce à l’utilisation de l
a base MongoDB, est très évolutive. En effet, de par son architecture “scemaless”, l’évolution des attributs ou l’ajout d’un attribut n’entraîne pas le refactoring de nos classes modèles.
L’un des objetifs principaux du plugin est de fournir une implémentation la plus compatible possible avec GORM pour Hibernate. Ainsi, il est possible d’utiliser la plupart des méthodes de requêtage fournies par GORM comme les dynamic finders, les where queries ou les Criteria. Cependant, la base MongoDB ne gérant pas les joitures, la requête suivante:

def cars = Car.where {
     owner {
         name : 'John'
     }
}

lèvera l’exception: java.lang.UnsupportedOperationException: Join queries are not supported by MongoDB
Il revient alors au développeur d’effectuer deux requêtes distinctes pour arriver au résultat escompté, comme suit:

def owner = Owner.findByName('John')
def cars  = Car.findAllByOwner(owner)

Enfin, un petit mot sur la gestion des transactions. Même si la base MongoDB ne gère pas les transactions directement, GORM regroupe les opérations d‘insert et d‘update jusqu’à ce que la session ne soit flushée. Ce système permet un rollback des opérations antérieures dans le cas où l’une d’elle échouerait. En effet, dans l’exemple suivant, nous voyons que dû aux contraintes définies dans nos domain-class (ici, l’attribut color ne peut être vide et l’attribut owner ne peut être NULL), notre objet car2 ne sera pas persisté en base et, grâce au méchanisme de transaction de GORM, l’objet car1 ne le sera pas non plus:

def car1 = new Car(brand: 'Test', model: 'Test', color: 'test', owner: owner)
car1.save(failOnError: true)
def car2 = new Car(brand: 'Test', model: 'Test')

Aller plus loin avec le plugin

Le plugin MongoDB ne se limite pas uniquement à permettre aux développeurs de mapper leurs classes et d’effectuer des requêtes de façon similaire à ce que l’on fait habituellement avec Hibernate mais il nous permet d’utiliser d’autres fonctionnalités propres à MongoDB. Dans ce sens, nous avons par exemple la possibilité d’utiliser la fonctionnalité de “Geosptatial Query” de MongoDB. Cette fonctionnalité nous permet de doter nos objets d’un attribut représentant des coordonnées géospatiales (longitude/latitude), très utiles lorsque l’on souhaite travailler avec des positions géographiques. Pour activer cette fonctionnalité, il suffit d’attribuer la valeur true à la propriété geoIndex dans le bloc de code responsable du mapping:

class Hotel {
    String name
    List location
    static mapping = {
        location geoIndex: true
    }
}

Enfin, le plugin nous offre l’accès à une API “bas niveau” basée sur le projet GMongo qui est lui même un wrapper autour du driver Mongo Java. Afin d’utiliser cette API, il suffit de d’instancier un objet représentant la base de donnée MongoDB de la façon suivante :
def db = mongo.getDB("gmongo")
Nous pouvons ensuite créer des requêtes et des opérations de la même manière qu’avec le driver Java pour MongoDB. Voici un exemple d’instructions possibles (exemple tiré de la page de documentation du plugin).

// Collections can be accessed as a db property (like the javascript API)
assert db.myCollection instanceof com.mongodb.DBCollection
// They also can be accessed with array notation
assert db['my.collection'] instanceof com.mongodb.DBCollection
// Insert a document
db.languages.insert([name: 'Groovy'])
// A less verbose way to do it
db.languages.insert(name: 'Ruby')
// Yet another way db.languages << [name: 'Python']
// Insert a list of documents
db.languages << [[name: 'Javascript', type: 'prototyped'], [name: 'Ioke', type: 'prototyped']]

Conclusion

Le plugin MongoDB, ainsi que les plugins pour Neo4J, Redis… nous permettent d’utiliser les fonctionnalités de ces bases de données NoSQL couplées à GORM. Ainsi, passer d’une application classique basée sur Hibernate à une application n’utilisant qu’une base de donnée NoSQL se fait quasiment sans douleur. Bien que le plugin soit actuellement utilisable, il manque tout de même quelques fonctionnalités intéressantes de MongoDB comme GridFS pour le stockage de fichier. Néanmoins, d’après le chef de projet de Grails et créateur des différents plugins NoSQL, c’est en prévision.

Auteur/Autrice

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.