Publié par
Il y a 8 années · 10 minutes · Cloud

La persistance dans Google App Engine (Partie une : Le datastore)

Revue de Presse Xebia
Google App Engine (GAE) pour Java, sorti récemment, propose une solide offre d’hébergement de serveur d’applications Java/JEE. Cette solution de cloud computing est conçue comme « platform as a service » : Google fournit l’infrastructure complète, ainsi que l’environnement pour héberger l’application. App Engine propose ainsi de nombreux services, dont notamment un système de base de données, appelé datastore (basé sur Google Big Table). La gestion de la persistance est réalisée par l’ORM Datanucleus, qui supporte les implémentations JDO et JPA.

Je vais vous présenter de manière concise le fonctionnement du datastore. De par sa nature, il impose des contraintes fortes, qui nécessitent de revoir sa façon de modéliser les données et de gérer la persistance dans son application.

Datastore et Big Table

La base de données de GAE appelée datastore repose sur le SGBD Big Table développé en interne par Google. Celui-ci a la particularité d’être hautement scalable et s’appuie sur un système de fichiers distribués géré par GFS (Google File System).
Big Table est une Map ordonnée multi-dimensionnelle. Les données (ou entités) sont stockées sous forme de <Clé, Valeur> dans des partitions appelées tablets. Des tablettes META1 référencent l’ensemble des tablettes contenant les entités. Une tablette maître appelée META0 référence les tablettes META1 et permet au client d’accéder aux tablettes de la base. Pour optimiser les performances, un mécanisme de cache permet au client de s’adresser directement aux tablettes META1 et aux tablettes contenant les entités.

MapReduce

L’idée de Google a été de simplifier au maximum la gestion des données sur un nombre très important de clusters. Le but étant de pouvoir tirer parti des milliers de machines dont dispose Google pour former un cluster géant.
La solution trouvée par Google s’appelle MapReduce. Cet algorithme permet de paralléliser et de distribuer automatiquement les données du datastore.
Lorsqu’une requête (un select par exemple) est exécutée sur une entité, le MapReduce est alors lancé pour récupérer le résultat. Il se décompose en deux étapes (ou fonctions):

  • Une première fonction, Map, est appliquée sur les différentes partitions et renvoie une liste de valeurs intermédiaires (clé, valeur intermédiaire) correspondant aux critères du select.
  • Ensuite, une seconde fonction, Reduce, fusionne toutes les valeurs intermédiaires pour une même clé pour former le résultat final (une valeur finale).

Entité et groupe d’entités

Vous l’aurez compris, le datastore n’est donc pas une base de données de type relationnelle, mais de type hiérarchique.
Cela implique une manière totalement différente de modéliser et d’accéder aux données de la base. Les données sont modélisées sous forme d’entités et de groupes d’entités. Une entité qui n’a pas de parents est appelée ‘root entity’. Cette root entity peut avoir des enfants, on appelle alors cet ensemble un groupe d’entités (entity group). Chaque groupe d’entités constitue alors un ensemble cohérent et indépendant vis à vis d’autres entités. Ces grappes d’objets peuvent ainsi être clusterisées dans une tablette.

Chaque entité est constituée d’un ensemble d’attributs, certains pouvant faire référence à d’autres entités. Une entité est implémentée avec une simple classe POJO, annotée avec JDO ou JPA.
Voici un exemple d’implémentation d’une entité UserEntity avec JDO:

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class UserEntity {

	public UserEntity(){}

	@PrimaryKey
	@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
	private Key key;

	@Persistent
	private String email;

	@Persistent
	private List<FeedEntity> feeds;

	...
}

Clés primaires

Chaque entité dispose d’une clé qui l’identifie de manière unique parmi toutes les entités du data store. La clé contient un ensemble d’informations tel que l’application ID, la classe de l’entité et l’ID de l’entité. L’application doit fournir la partie ID de la clé via une propriété sur l’objet annoté avec @PrimatyKey avec JDO ou @Id avec JPA. Cette propriété peut être de type numérique (Long uniquement) ou de type String (unencoded String) et être générée automatiquement par le data store.
L’exemple ci-dessous représente un ID de type numérique implémenté via l’annotation @PrimaryKey (implémentation JDO). Au passage, on remarquera que la stratégie de génération des ID est dans tous les cas de type IDENTITY:

@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;

Malheureusement, si vous comptez gérer des relations entre entités, il n’est pas possible d’utiliser les types d’ID décrits précédemment. Il est nécessaire d’utiliser une instance de l’objet Key (com.google.appengine.api.datastore.Key) fourni par l’API de GAE. Il est tout de même possible d’utiliser une forme réencodée en String de cet objet Key afin de ne pas être dépendant de l’API GAE et de faciliter la migration de l’application hors de GAE.
L’objet Key inclut la clé de l’entité parent si elle existe, ainsi que l’ID. L’ID est fourni soit de manière manuelle par l’application sous forme de String, soit de manière automatique par le data store sous forme numérique. Dans ce dernier cas, il suffit de laisser la valeur à null.

Dans l’exemple ci-dessous, on peut voir qu’il est possible d’ajouter un accès direct à l’ID (numérique ou String) via un attribut supplémentaire id:

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class FeedEntity {

	public FeedEntity(){}

	@PrimaryKey
	@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
	private Key key;

	@Persistent
	@Extension(vendorName = "datanucleus", key = "gae.pk-id", value = "true")
	private Long id; 
	...
}

Par ailleurs, GAE offre une API pour manipuler les clés.
Pour créer une clé manuellement, il suffit d’indiquer la classe de l’entité et l’ID, de type numérique ou String, qui l’identifie de manière unique:

	Key k = KeyFactory.createKey(UserEntiy.class.getSimpleName(), "capitaine.haddock@xebia.fr");

Il est aussi possible de convertir une clé en String ou l’inverse via les méthodes KeyFactory.keyToString() et KeyFactory.stringToKey().

Les relations

Les relations sont modélisées de manière classique via les propriétés de l’entité qui référencent d’autres instances. Il est important de modéliser les relations en gardant à l’esprit que le data store est un système hiérarchique constitué de parents-enfants et de groupes.

GAE décrit les relations de deux manières : les owned relationships et les unowned relationships. On dit qu’une relation est owned lorsqu’une des entités de la relation ne peut exister sans l’autre. Ceci arrive dans le cas d’une entité enfant qui est liée à l’entité parent. L’entité parent peut elle-même être enfant d’une autre entité ou être une root entity. On dit qu’une relation est unowed lorsque les entités sont indépendantes : elles n’appartiennent pas au même groupe d’entités.

  • owned relationship: GAE gère nativement les relations de type one-to-one, one-to-many (unidirectionelle et bidirectionelle), mais pas les many-to-many. Lors d’une opération de mise à jour ou de suppression, une cascade est appliquée aux enfants de la relation. Dans le cas d’une création, les enfants sont créés automatiquement et l’ensemble appartient au même groupe d’entités.
  • unowned relationship: on ne peut pas modéliser la relation via une instance, mais uniquement via l’objet Key référençant l’entité. Il est alors possible de mapper les différents types de relations.

Pour réaliser des many-to-many, il faut que chaque entité référence l’autre entité via sa Key. On entre alors dans le cas d’une unowed relationship. On peut voir ci-dessous un exemple de many-to-many:

import com.google.appengine.api.datastore.Key;
import java.util.List;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class UserEntity {

	public UserEntity(){}

	//...

	@Persistent
	private List<Key> feeds;

	//...
}
import com.google.appengine.api.datastore.Key;
import java.util.List;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class FeedEntity {
	
	public FeedEntity(){}

	//...

	@Persistent
	private List<Key> users;

	//...
}

Les requêtes

Les requêtes sont écrites en JDOQL ou JPQL selon que l’on utilise l’implémentation JDO ou JPA. Il est possible de faire des jointures entre des entités appartenant au même groupe et de filtrer via des propriétés des entités parent et enfant.
Le data store gère les sélections, les filtres et les tris. Il est aussi possible de définir des plages (range) de résultat.
Cependant, oubliez les group by, les aggrégations, et les fonctions, ainsi que l’opérateur !=. Autre contrainte : il n’est possible de définir qu’un seul filtre d’inégalité par requête, et il faut impérativement ajouter la propriété filtrée comme première clause dans le tri.

import java.util.List;
import javax.jdo.Query;
// ...

Query query = pm.newQuery(FeedEntity.class);
query.setFilter("lastUpdateDate >= dateParam");
query.setOrdering("lastUpdateDate desc");
query.declareParameters("Date dateParam");
query.setRange(0,10);

List<FeedEntity> results = (List<FeedEntity>) query.execute(new Date());

GAE génèrent des index automatiquement pour chaque requête de l’application. Il est possible de créer ses propres index dans le fichier datastore-indexes.xml selon les besoins. Attention, les entités dont les propriétés ne sont pas indexées ou dont les propriétés n’existent pas seront ignorées par le data store.

À ce jour, Google ne fournit aucun outil pour visualiser et requêter le datastore en environnement de développement.
Cependant, dans l’interface d’administration du serveur fourni par Google, il est possible de requêter sur le datastore de production via le langage GQL (Google Query Langage) qui ressemble beaucoup à SQL.

Les transactions

Le datastore permet d’effectuer un ensemble d’opérations au sein d’une même transaction. Si une transaction échoue, toutes les opérations réalisées sont annulées. Le datastore réalise ainsi des opérations atomiques.

Une des principales restrictions du datastore est l’obligation de ne manipuler dans une transaction qu’une seule entité ou un seul groupe d’entités.
Si vous ne respectez pas cette restriction, vous obtiendrez l’erreur suivante: javax.jdo.JDOFatalUserException: can't operate on multiple entity groups in a single transaction.

Voici un exemple (implémentation JDO) d’opération incorrecte qui génère une exception:

import java.util.List;
import javax.jdo.Query;
import javax.jdo.Transaction;
import javax.jdo.PersistenceManager;
// ...
       
// Example of a failing transaction ... 

Transaction tx = getPersistenceManager().currentTransaction();
tx.begin();

Query query = getPersistenceManager().newQuery(FeedEntity.class);
List<FeedEntity> feeds =  (List<FeedEntity>) query.execute();		
for (FeedEntity feedEntity : feeds) {
    // some operations
	feedEntity.setLastUpdateDate(new Date());
	getPersistenceManager().makePersistent(feedEntity);
	}
tx.commit();

Autre restriction : l’impossibilité de mettre à jour ou de créer plus d’une entité ou groupe d’entités au sein d’une transaction. En effet, les entités sont stockées dans des tablettes potentiellement différentes.
Enfin, Google avertit qu’une requête peut échouer dans certains cas : dépassement d’un quota, niveau de contention trop élevé ou encore nombre d’utilisateurs trop important.

Quotas

Bien que GAE soit gratuit, des quotas sont définis pour le datastore. Ils sont assez pénalisants. Ainsi, je vous conseille de ne pas les négliger. Google limite le nombre d’entités à 1000 par requête. Par ailleurs, une entité a une taille maximum fixée à 1 Mo. Enfin, le temps d’exécution d’une requête envoyée au serveur ne doit pas dépasser 30 secondes.

Conclusion

Avec GAE, oubliez les bonnes vieilles modélisations à la Merise ou UML. Dorénavant, vous manipulez des groupes d’entités, des root entity, ou encore des relations de type owned-unowed. Le data store impose de nombreuses contraintes, qui pourraient en rebuter plus d’un. Cependant, il faut garder à l’esprit les objectifs de Google : fournir une plateforme hautement disponible et scalable.
Bien que la persistance soit gérée via l’ORM DataNucleus, il est possible d’utiliser une API de bas de niveau. Cependant, très peu de documentation est fournie.
Suite à cette présentation plutôt théorique du datastore, j’aborderai prochainement la persistance dans GAE avec les implémentations JDO et JPA.

Liens utiles

7 réflexions au sujet de « La persistance dans Google App Engine (Partie une : Le datastore) »

  1. Publié par Sébastien LORBER, Il y a 8 années

    J’attends la suite avec impatience ;)

  2. Publié par Philippe Anes, Il y a 8 années

    Belle présentation. Vivement la persistance dans GAE avec JPA. :)

  3. Publié par Charpentier Damien, Il y a 8 années

    Bon article comme d’habitude !

  4. Publié par Lionel, Il y a 8 années

    Très bon article, dommage que la suite ne vienne pas.

  5. Publié par diablo, Il y a 8 années

    Trés Bien !!!

  6. Publié par Jean-Charles, Il y a 8 années

    Hello,

    Très bon article. Mais j’aimerai bien trouver un article qui explique comment faire des recherche complexe ?
    Du genre :
    – SELECT * FROM Clients WHERE prenom LIKE ‘%Charles%’

    J’ai malheureusement l’impression que Google ne veut pas :-(

Laisser un commentaire

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