Persistance et relations

Article d'origine: 

Persistance Java avec le magasin de données de Google App Engine

Résumé: 

La persistance des données est la pierre de touche d'une application qui passe à l'échelle dans des environnements d'entreprise. Dans ce dernier article de sa série d'introduction à Google App Engine for Java™, Rick Hightower aborde le défi que représente le framework actuel de persistance de l'App Engine, basé sur Java. Apprenez les raisons essentielles qui font que la persistance Java de la préversion actuelle n'est pas encore prête pour des applications sensibles, tout en suivant une démonstration fonctionnelle de ce que l'on peut faire pour rendre persistantes les données dans des application App Engine pour Java. Pour cela, vous aurez besoin que l'application de gestion des contacts du second article soit opérationnelle, tandis que vous utilisez l'API JDO pour rendre persistants, requêter, mettre à jour et supprimer des objets Contact.

L' App Engine pour Java cherche à nous décharger du souci de l'écriture d'une couche de persistance pour des applications Web passant bien à l'échelle, mais y arrive-t-il vraiment ? Dans cet article, je conclus mon introduction à l' App Engine pour Java par une vue d'ensemble de son framework de persistance, qui est basé sur Java Data Objects (JDO) et Java Persistence API (JPA). Bien qu'initialement prometteur, la persistance Java présente pour l'instant de sérieux inconvénients, que j'explique en donnant des exemples. Vous apprendrez comment la persistance de l' App Engine pour Java fonctionne, quels en sont les défis, et quels choix se présentent à vous quand vous travaillez sur la plateforme de cloud de Google pour les développeurs Java.

Tandis que vous lisez l'article et voyez les exemples fonctionner, gardez en mémoire que l' App Engine pour Java actuel n'est qu'une préversion. Bien que la persistance Java puisse ne pas correspondre pour le moment à ce que vous espériez, ou à ce dont vous avez besoin, cela pourrait et devrait changer à l'avenir. Ce que j'ai appris en écrivant cet article est qu'il ne faut être ni craintif ni conservateur pour utiliser l' App Engine pour Java afin de construire des applications effectuant beaucoup de manipulation de données et passant bien à l'échelle. Ce serait plutôt comme plonger du côté le plus profond de la piscine sans maitre-nageur en vue : il ne dépendra que de vous que le projet reste à flot ou coule comme une pierre.

Notez que les exemples de cet article sont basés sur l'application de gestion des contacts du second article. Vous aurez besoin que cette application tourne pour mettre à profit les exemples donnés ici.

Eléments de base et abstractions perméables (ndT: leaky abstraction)

Comme le Google App Engine d'origine, App Engine pour Java se repose sur l'infrastructure interne de Google en ce qui concerne les trois piliers du développement d'application qui passent à l'échelle: la distribution, la réplication et la répartition de la charge. Comme on travaille avec l'infrastructure de Google, le plus gros de cette magie se déroule en coulisse, et est accessible via les APIs basées sur des standards proposées par l' App Engine pour Java. L'interface du magasin de données est basée sur JDO et JPA, elles-mêmes se reposant sur le projet Open Source DataNucleus. L'App Engine pour Java fournit également une API de bas niveau pour attaquer directement le magasin de données, qui est basé sur l'implémentation de BigTable par Google (voir le premier article pour en savoir plus sur BigTable).

La persistance d'App Engine pour Java est loin d'être aussi simple que la persistance en pur Google App Engine, toutefois. Les interfaces JDO et JPA présentent quelques abstractions perméables dûes au fait que BigTable n'est pas une base de données relationnelle. Par exemple, avec App Engine pour Java, vous ne pouvez pas faire de jointures dans vos requêtes. Vous pouvez créer des relations avec JPA et JDO, mais elles sont utilisées seulement lors de la persistance. Et quand vous rendez des objets persistants, ils peuvent être persistés dans la même transaction seulement s'ils font partie du même groupe d'entités. Par convention, les relations filles sont dans le même groupe d'entités que leur mère. Inversement, des relations de même niveau sont dans des groupes distincts.

Repenser la normalisation des données

Travailler avec le magasin de données de l'App Engine requiert de votre part que vous repensiez à votre endoctrinement concernant les bénéfices de la normalisation des données. Bien sûr, si vous travaillez depuis assez longtemps dans le monde réel, vous avez probablement déjà sacrifié la normalisation sur l'autel des performances une fois ou deux. La différence est qu'en rapport avec le magasin de données de l'App Engine, il faut dénormaliser tôt et souvent. La dénormalisation n'est plus un gros mot mais un outil de conception que vous appliquerez dans bien des aspects de vos applications.

Le principal inconvénient de cette persistance perméable intervient quand vous essayez de porter une application écrite pour un SGBDR vers l'App Engine pour Java. Le magasin de données de l'App Engine pour Java ne remplace pas aisément une base de données relationnelle, et ce que vous faites avec l'App Engine pour Java peut être difficile à traduire pour un SGBDR. Prendre un schéma existant et le porter dans le magasin de données est un scénario encore moins sympathique. Si vous décidez de porter une application d'entreprise Java existante sur l'App Engine, il vous faudra prendre des précautions et faire une analyse. Google App Engine est une plateforme destinée aux applications qui sont spécialement conçues pour elle. Le support de JDO et JPA permet à ces applications d'être portées plus facilement d'être portées, tout en restant non-normalisées, vers des applications d'entreprise plus traditionnelles.

Le problème des relations

Un autre point faible de la préversion de l'App Engine pour Java est sa gestion des relations. Afin de créer des relations, il faut pour le moment utiliser des extensions de JDO spécifiques à l'App Engine pour Java. Etant donné que les clés sont générées sur la base d'artifacts BigTable — c'est-à-dire que la clé primaire d'une fille inclut la clé primaire de sa mère — il est nécessaire de gérer les données dans une base de données qui n'est pas relationnelle. La persistance de données est une autre limitation. Des problèmes complexes voient le jour si vous utilisez la classe non-standard Key de l'App Engine pour Java. Tout d'abord, comment se servir de Key quand vous portez votre modèle vers un SGBDR ? Ensuite, la classe Key ne peut pas être traduite par le moteur GWT, ce qui signifie qu'un objet du modèle utilisant cette classe ne peut pas être utilisé dans une application GWT.

Bien sûr, au moment où cet article est écrit, il s'agit plutôt d'une préversion de Google App Engine pour Java. Elle n'est pas considérée comme prête pour la production. Cela devient particulièrement évident quand on étudie la documentation des relations en JDO, qui est rare et contient des exemples incomplets.

Le kit de développement de l'App Engine pour Java est fourni avec une série de programmes d'exemple. La plupart de ces exemple utilisent JDO, et aucun JPA. Aucun de ces exemples (y compris celui qui s'appelle jdoexamples) ne montre même une relation simple. Au lieu de cela, tous ces exemples utilisent un objet unique pour stocker les données dans le magasin. Le groupe Google App Engine for Java discussion group est plein de questions sur la manière de faire fonctionner une relation simple, avec peu de réponses. Quelques développeurs sont arrivés à le faire, mais ces complications ont nécessité beaucoup d'huile de coude.

La conclusion en ce qui concerne les relations dans l'App Engine pour Java est qu'il faut les gérer sans beaucoup d'aide de JDO ou de JPA. Cependant, BigTable est une technologie éprouvée pour ce qui est de résister à la montée en charge et vous pouvez bâtir sur ce fondement. S'appuyer sur BigTable libère de l'obligation d'employer une API façade qui est loin d'être terminée. Par contre, il faudra faire avec une API de plus bas niveau.

Java Data Objects et App Engine pour Java

Bien qu'il n'y ait peut-être pas grand sens à porter une application Java traditionnelle vers l'App Engine, et en prenant également en compte le défi posé par les relations, il existe tout de même des scénarios de persistance pour lesquels l'utilisation de la plateforme a du sens. je conclurais cet article avec un exemple qui devrait vous donner un avant-goût de la manière dont la persistance de l'App Engine fonctionne. Nous démarrerons avec l'application de gestion de contacts de la seconde partie, mais cette fois nous ajouterons le support de la persistance des objets Contact dans le magasin de données de l'App Engine.

Dans l'article précédent, vous avez créé une IHM GWT pour effectuer des opérations CRUD sur les objets Contact. Vous avez défini l'interface du Listing 1:

Listing 1. L'interface ContactDAO

package gaej.example.contact.server;

import java.util.List;

import gaej.example.contact.client.Contact;

public interface ContactDAO {
	void addContact(Contact contact);
	void removeContact(Contact contact);
	void updateContact(Contact contact);
	List<Contact> listContacts();
}

Ensuite, vous avez créé un simulacre qui utilisait des données se trouvant dans une collection en mémoire, comme le rappelle le Listing 2.

Listing 2. Simulacre implémentant ContactDAO

package gaej.example.contact.server;

import gaej.example.contact.client.Contact;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class ContactDAOMock implements ContactDAO {
    Map<String, Contact> map = new LinkedHashMap<String, Contact>();
    {
        map.put("rhightower@mammatus.com", new Contact("Rick Hightower", 
                                 "rhightower@mammatus.com", "520-555-1212"));
        map.put("scott@mammatus.com", new Contact("Scott Fauerbach", 
                                 "scott@mammatus.com", "520-555-1213"));
        map.put("bob@mammatus.com", new Contact("Bob Dean", 
                                 "bob@mammatus.com", "520-555-1214"));
    }
    
    public void addContact(Contact contact) {
        String email = contact.getEmail();
        map.put(email, contact);
    }

    public List<Contact> listContacts() {
        return Collections.unmodifiableList(new ArrayList<Contact>(map.values()));
    }

    public void removeContact(Contact contact) {
        map.remove(contact.getEmail());
    }

    public void updateContact(Contact contact) {
        map.put(contact.getEmail(), contact);
    }
}

Maintenant voyons ce qui se passe si nous remplaçons cette implémentation simulacre avec une implémentation qui interagit avec le magasin de données de l'App Engine. Dans cet exemple, nous utiliserons JDO pour rendre la classe Contact persistente. Une application écrite avec le plugin Google pour Eclipse a déjà toutes les librairies nécessaires à l'utilisation de JDO. Elle inclut également un fichier jdoconfig.xml, donc une fois que la classe est annotée, nous sommes prêts à utiliser JDO.

Le Listing 3 montre l'interface implémentée afin d'utiliser JDO pour rendre persistents, requêter, mettre à jour et supprimer des objets:

Listing 3.  ContactDAO avec JDO

package gaej.example.contact.server;

import gaej.example.contact.client.Contact;

import java.util.List;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;

public class ContactJdoDAO implements ContactDAO {
    private static final PersistenceManagerFactory pmfInstance = JDOHelper
			.getPersistenceManagerFactory("transactions-optional");
    
    public static PersistenceManagerFactory getPersistenceManagerFactory() {
		return pmfInstance;
	}

    public void addContact(Contact contact) {
        PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
        try {
            pm.makePersistent(contact);
        } finally {
            pm.close();
        }
    }

    @SuppressWarnings("unchecked")
    public List<Contact> listContacts() {
        PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
        String query = "select from " + Contact.class.getName();
        return (List<Contact>) pm.newQuery(query).execute();
    }

    public void removeContact(Contact contact) {
        PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
        try {
            pm.currentTransaction().begin();

            // Nous n'avons pas de référence vers le Contact sélectionné.
            // Il nous faut d'abord le retrouver
            contact = pm.getObjectById(Contact.class, contact.getId());
            pm.deletePersistent(contact);

            pm.currentTransaction().commit();
        } catch (Exception ex) {
            pm.currentTransaction().rollback();
            throw new RuntimeException(ex);
        } finally {
            pm.close();
        }
    }
    
    public void updateContact(Contact contact) {
        PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
        String name = contact.getName();
        String phone = contact.getPhone();
        String email = contact.getEmail();
        
        try {
            pm.currentTransaction().begin();
            // Nous n'avons pas de référence vers le Contact sélectionné.
            // Il nous faut d'abord le retrouver
            contact = pm.getObjectById(Contact.class, contact.getId());
            contact.setName(name);
            contact.setPhone(phone);
            contact.setEmail(email);
            pm.makePersistent(contact);
            pm.currentTransaction().commit();
        } catch (Exception ex) {
            pm.currentTransaction().rollback();
            throw new RuntimeException(ex);
        } finally {
            pm.close();
        }
    }
}

Méthode par méthode

Maintenant examinons ce qui se passe dans chacune des méthodes du Listing 3. Vous vous rendrez compte que, bien que les noms de méthodes puissent être nouveaux, leur action est pour la plus grande part familière.

Tout d'abord, afin d'accéder au PersistenceManager, nous avons créé une instance statique de PersistenceManagerFactory. Si vous avez déjà travaillé avec JPA, PersistenceManager est l'équivalent d'un EntityManager. Si vous connaissez Hibernate, PersistenceManager est l'équivalent d'une session. L'idée importante est que PersistenceManager est l'interface principale vers le système de persistance JDO. Il représente une session de base de données. La méthode PersistenceManagerFactory retourne le qui a été initialisé de manière statique, comme le montre le Listing 4:

Listing 4. getPersistenceManagerFactory() retourne l'instance de PersistenceManagerFactory

private static final PersistenceManagerFactory pmfInstance = JDOHelper
		.getPersistenceManagerFactory("transactions-optional");

public static PersistenceManagerFactory getPersistenceManagerFactory() {
	return pmfInstance;
}

La méthode addContact() ajoute de nouveaux contacts dans le magasin de données. Pour ce faire, elle crée une instance PersistenceManager de puis appelle sa méthode makePersistence(). La méthode makePersistence() prend l'objet transient Contact (que l'utilisateur aura rempli dans l'IHM GWT) et en fait un objet persistant. Tout ceci est codé dans le Listing 5:

Listing 5. addContact()

public void addContact(Contact contact) {
    PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
    try {
    	pm.makePersistent(contact);
    } finally {
    	pm.close();
    }
}

Remarquez que dans le Listing 5, la fermeture persistenceManager de se trouve dans un bloc finally. Ceci assure que les ressources qui lui sont associées seront bien nettoyées.

La méthode listContact(), qui se trouve dans le Listing 6, crée une requête au moyen de persistenceManager. Elle invoque ensuite la méthode execute(), qui retourne la liste de se trouvant dans le magasin de données.

Listing 6. listContacts()

@SuppressWarnings("unchecked")
public List<Contact> listContacts() {
    PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
    String query = "select from " + Contact.class.getName();
    return (List<Contact>) pm.newQuery(query).execute();
}

La méthode cherche un contact par son ID avant de le supprimer du magasin, comme vous le voyez au Listing 7. Une suppression directe du contact ne fonctionnerait pas puisque le Contact en provenance de l'IHM ne sait rien de JDO. Il faut obtenir un Contact associé à un PersistenceManager avant de pouvoir le supprimer.

Listing 7. removeContact()

public void removeContact(Contact contact) {
    PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
    try {
	pm.currentTransaction().begin();
	// Nous n'avons pas de référence vers le contact sélectionné.
	// Donc nous devons d'abord le récupérer
	contact = pm.getObjectById(Contact.class, contact.getId());
	pm.deletePersistent(contact);
	pm.currentTransaction().commit();
    } catch (Exception ex) {
	pm.currentTransaction().rollback();
	throw new RuntimeException(ex);
    } finally {
	pm.close();
    }
}

La méthode updateContact() du Listing 8 est similaire à la méthode removeContact() en ce qu'elle recherche d'abord le Contact. Elle copie ensuite les propriétés du Contact. Ces propriétés sont passées en argument au Contact qui a été récupéré par le gestionnaire de persistance. Les objets qui ont été ramenés sont analysés par le PersistenceManager. Si un objet a été modifié, les modifications sont écrites dans la base de données par le PersistenceManager quand la transaction est validée.

Listing 8. updateContact()

public void updateContact(Contact contact) {
    PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
    String name = contact.getName();
    String phone = contact.getPhone();
    String email = contact.getEmail();

    try {
	pm.currentTransaction().begin();
	// Nous n'avons pas de référence vers le contact sélectionné.
	// Donc nous devons d'abord le récupérer
	contact = pm.getObjectById(Contact.class, contact.getId());
	contact.setName(name);
	contact.setPhone(phone);
	contact.setEmail(email);
	pm.makePersistent(contact);
	pm.currentTransaction().commit();
    } catch (Exception ex) {
	pm.currentTransaction().rollback();
	throw new RuntimeException(ex);
    } finally {
	pm.close();
    }
}

Annotations pour la persistance des objets

Afin de permettre à un Contact de persister, il faut préciser que celui-ci en est capable avec l'annotation
@PersistenceCapable. Il faut ensuite annoter tous les champs persistants, comme vous le montre le Listing 9:

Listing 9. Contact peut maintenant persister

package gaej.example.contact.client;

import java.io.Serializable;

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Contact implements Serializable {
    private static final long serialVersionUID = 1L;
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;
    @Persistent
    private String name;
    @Persistent
    private String email;
    @Persistent
    private String phone;

    public Contact() {
    }

    public Contact(String name, String email, String phone) {
    	super();
    	this.name = name;
	this.email = email;
	this.phone = phone;
    }

    public Long getId() {
	return id;
    }

    public void setId(Long id) {
	this.id = id;
    }

    public String getName() {
	return name;
    }

    public void setName(String name) {
	this.name = name;
    }

    public String getEmail() {
	return email;
    }

    public void setEmail(String email) {
    	this.email = email;
    }

    public String getPhone() {
	return phone;
    }

    public void setPhone(String phone) {
	this.phone = phone;
    }
}

Par la grâce de la programmation orientée-objet et de la conception par interface, vous pouvez tout simplement remplacer le ContactDAOMock de départ par le nouveau ContactJdoDAO. L'IHM GWT marchera avec JDO sans modification.

En fin de compte, la seule modification à faire pour provoquer cet échange se situe dans la manière dont le DAO est instantié dans le service, comme le montre le Listing 10:

Listing 10. RemoteServiceServlet

public class ContactServiceImpl extends RemoteServiceServlet implements ContactService {
    private static final long serialVersionUID = 1L;
    //private ContactDAO contactDAO = new ContactDAOMock();
    private ContactDAO contactDAO = new ContactJdoDAO();
    ...

En conclusion

Dans cette troisième partie, j'ai introduit le support actuel la persistance dans Google App Engine pour Java, qui constitue une des pierres angulaires du déploiement d'applications qui passent à l'échelle. Le constat général est assez décevant, bien qu'il soit important de garder à l'esprit qu'il s'agit d'une plateforme en évolution. Les applications écrites pour cette préversion de l'App Engine sont liées à son infrastructure de persistance, même si elles utilisent JDO ou JPA. De même, cette préversion n'offre que peu de documentation sur son framework de persistance, et les exemples fournis montrent qu'il est à peu près impossible de faire fonctionner même les relations les plus simples.

Même si les implémentations de JDO et JPA étaient complètement terminées, il serait à l'heure actuelle bien difficile de porter une application écrite pour l'App Engine vers une application d'entreprise classique, utilisant un SGBD. A tout le moins, cela nécessiterait un recodage assez costaud pour que cela fonctionne.

J'espère que la persistance va se bonifier avec le temps. Si vous devez vraiment travailler avec l'App Engine maintenant, vous devriez probablement court-circuiter l'utilisation des APIs Java et travailler directement avec les APIs de bas niveau du magasin de données. Il est possible de travailler avec la plateforme App Engine pour Java, mais il faut s'attendre à un certain travail d'apprentissage, dû à la fois à la perméabilité des abstractions décrites au début de cet article et aux fonctionnalités qui soit ne fonctionnent pas encore bien, soit qui ne sont pas encore bien documentées.