De MySQL à NoSQL avec Java Data Object (JDO) sous Google App Engine – Partie 2/2

Introduction

Nous avons vu dans la première partie du tutoriel, comment créer des objets Java annotés avec JDO pour être sauvegardés dans le datastore de Google App Engine. Pour cette seconde partie, nous allons voir comment faire des requêtes sur le datastore. Avec JDO, il y a plusieurs façons de faire de requêtes, ici, nous allons vous présenter la façon la plus orientée Java. Comme pour le premier article, nous allons faire la comparaison avec MySQL pour que l’on retrouve rapidement nos repères. Bien entendu, nous allons faire les explications par l’exemple et nous allons reprendre les mêmes classes que dans la première partie, à savoir User, Level et Score.

PersistenceManagerFactory

Une application interagit avec JDO en utilisant une instance de la classe PersistenceManager. On récupère cette instance en instanciant et en appellant une méthode sur la classe PersistenceManagerFactory. La fabrique utilise la configuration de JDO pour créer les instances de PersistenceManager.

Etant donné que le PersistenceManagerFactory prend beaucoup de temps pour être initialisé, il est préférable qu’une application réutilise la même instance. Une exception est levée lorsque l’application instancie plusieurs fois un PersistenceManagerFactory avec le même identifiant. Une manière simple de gérer l’instance de PersistenceManagerFactory est de créer une classe enveloppe singleton avec une instance statique. On crée donc la classe PMF que l’on utilisera à chaque fois que l’on souhaite obtenir notre fabrique pour pouvoir ensuite récupérer le PersistenceManager. C’est bon vous suivez ?

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

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

    private PMF() {}

    public static PersistenceManagerFactory get() {
        return pmfInstance;
    }
}

Select avec une condition

Afin d’organiser un peu son code, nous vous conseillons de créer une classe DBManager qui va contenir un ensemble de fonctions statiques pour gérer toutes les requêtes au datastore.

Allez, on démarre tout de suite avec un premier exemple, un simple SELECT avec une condition. Nous souhaitons retourner les Users qui ont un login correspondant à une certaine chaine de caractère. En SQL simple, nous ferions cette requête :

SELECT * FROM `user` WHERE login='bob'

En Java avec JDO, c’est un peu plus compliqué, mais pas insurmontable. Voici la fonction commentée pour faire la requête que nous souhaitons :

public static List<User> getLogin(String pLogin) {

    // on récupère notre fameux PersistenceManager
    PersistenceManager pm = PMF.get().getPersistenceManager();

    // on fait une requête sur la "table" User
    Query query = pm.newQuery(User.class);

    // on ajoute une condition WHERE
    // 'login' est le champ de la classe User
    // 'loginParam' est le paramètre définit dans declareParameters()
    query.setFilter("login == loginParam");

    // on déclare le paramètre de la condition en précisant son type
    query.declareParameters("String loginParam");

    // on exécute la requête on passant le paramètre
    return (List<User>) query.execute(pLogin);

}

Select avec plusieurs conditions

Passons à un exemple un tout petit peu plus compliqué avec deux conditions. En SQL, on ajoute un AND :

SELECT * FROM `user` WHERE login='bob' AND password='azertyuiop'

Pour reprendre notre exemple, il nous suffit de modifier l’expression booléenne de la méthode setFilter, de déclarer deux paramètres et de passer les deux valeurs au exécute :

public static List<User> getLogin(String pLogin, String pPassword) {

    PersistenceManager pm = PMF.get().getPersistenceManager();

    Query query = pm.newQuery(User.class);
    query.setFilter("login == loginParam && password == passwordParam");
    query.declareParameters("String loginParam, String passwordParam");

    return (List<User>) query.execute(pLogin, pPassword);
}

Select avec une condition sur un type non primitif

Là, vous allez nous dire, oui, c’est bien jolie vos filtres, mais dans le datastore, un objet ne contient pas que des données avec des types simples contrairement à MySQL. Ne vous inquiétez pas, tout est prévu. Si l’on veut passer un objet de type non primitif en paramètre d’une requête, il suffit simplement de faire un import de cette classe (ici Key) sur l’objet Query grâce à la méthode declareImports. Voici un exemple d’une requête où nous ajoutons une condition avec un objet Key.

public static List<Level> getLevel(Key pKey) {

    PersistenceManager pm = PMF.get().getPersistenceManager();

    Query query = pm.newQuery(Level.class);
    query.setFilter("key == keyParam");
    query.declareImports("import com.google.appengine.api.datastore.Key;");
    query.declareParameters("Key keyParam");

    return (List<Level>) query.execute(pKey);

}

Sachez tout de même que l’exemple précédent sert à vous montrer comment utiliser la méthode declareImports(), si il s’agit uniquement de faire une requête aussi triviale que récupérer un objet via sa clé, vous pouvez le faire beaucoup plus simplement :

public static Level getLevel(Key pKey) {
    PersistenceManager pm = PMF.get().getPersistenceManager();
    Level level = pm.getObjectById(Level.class, pKey);
    return level;
}

Enregistrement

Passons maintenant à l’enregistrement de données. En SQL, nous avons une requête d’insertion différente pour chaque table, le nom de la table étant différent à chaque fois, ainsi que ses colonnes. Si nous souhaitons enregistrer un User en SQL, nous ferions donc comme ceci :

INSERT INTO `user` (`user_id` ,`login` ,`password`)
VALUES ('' ,  'jean',  'qsdfghjklm');

Si nous voulons enregistrer un level, la requête n’a rien à voir :

INSERT INTO  `tuto_jdo`.`level` (`level_id` ,`name`)
VALUES ('' ,  'l003');

Avec NoSQL, c’est plus simple, il n’y a pas de notion de table, c’est comme si tout s’enregistrait dans la même table. C’est donc la même fonction qui va enregistrer tous les objets. Voici le code, c’est incroyablement simple :

public static void save(Object obj) {
    PersistenceManager pm = PMF.get().getPersistenceManager();
    try {
        pm.makePersistent(obj);
    } finally {
        pm.close();
    }
}

Notre tutorial en deux parties s’achève ici, nous espérons que ce tutorial vous aidera pour réaliser vos applications !

De MySQL à NoSQL avec Java Data Object (JDO) sous Google App Engine – Partie 1/2

Introduction à Google App Engine

L’été approche et chez Magetys ça bosse dur ! Nous sommes actuellement en train de développer deux applications Android qui vont arriver cet été et dans ce cadre, nous sommes confrontés à la problématique suivante : sachant que nos applications vont être des hits (haha), comment s’assurer d’une bonne qualité de service au fil du temps ?

Logo Google App EngineC’est dans cette optique que nous nous sommes penchés sur Google App Engine. Google App Engine permet de développer et d’héberger des applications ‘On the Cloud’. Google vous propose ainsi d’heberger vos web apps (dans notre cas, nos back office) sur la même infrastructure que le fameux moteur de recherche. Ainsi, une des grande force d’App Engine est de permettre aux développeurs de ne pas se soucier de la partie configuration du serveur, sécurité, monté en charge etc… Google s’occupe de tout, et il faut avouer qu’en la matière ils en connaissent un rayon !

Actuellement, pour profiter d’App Engine on peut développer soit en Java soit en Python. Etant donné notre expertise (enorme!) en Java, notre choix s’est porté naturellement vers ce langage. Pour développer pour App Engine, Google fourni un plugin Eclipse très pratique. Ce plugin permet de créer et faire tourner son projet App Engine en local, visualiser le contenu de son datastore, simuler un système d’identification via des comptes Google et bien sûr, mettre en ligne son projet en un clic. Vous pouvez trouver le plugin ici.

Le datastore sous Google App Engine

Bien entendu, comme toute application, nous avons besoin de stocker des données. Comme beaucoup de développeurs ayant un background web, nous sommes habitués à MySQL et son driver JDBC. Sous App Engine, MySQL n’existe pas, adieu les bases de données relationnelles au profit du NoSQL ! NoSQL a été inventé pour développer des bases de données plus performantes en terme d’accès aux données. Google a développé BigTable, un système NoSQL que l’on peut exploiter sous App Engine. Pour des développeurs comme nous qui connaissons bien les bases de données relationnelles, il est un peu difficile de se mettre au NoSQL. Pourtant, ce n’est pas si compliqué que ça et l’on peut retrouver les mêmes mécanismes.

Dans ce tutorial, nous allons partir d’une modélisation relationnelle « à la MySQL » pour développer des objets Java qui pourront être enregistrés dans ce que l’on appelle le ‘datastore’ App Engine. Pour la couche de persistance, App Engine intègre les api JDO et JPA. Dans notre exemple nous aborderons JDO pour faire des requêtes sur le datastore.

Modélisation relationnelle

Nous allons partir d’un exemple simple. Dans le cadre d’un jeu, nous avons des utilisateurs, des niveaux et les utilisateurs obtiennent des scores pour les différents niveaux. Nous avons donc une relation 1,n / 1,n entre les utilisateurs et les niveaux que l’on peut représenter de cette manière dans un schéma MCD :

Si nous transformons notre schéma MCD en schéma MLD, nous obtenons ce schéma :

La table Score est née de la relation many-to-many entre User et Level. Si nous avions utilisé MySQL, nous aurions pu déduire de ce schéma, trois tables MySQL et des clés étrangères sur la table Score. Le code MySQL aurait été le suivant :

CREATE TABLE user (
	user_id int NOT NULL AUTO_INCREMENT,
	login varchar(100),
	password varchar(100),
	PRIMARY KEY (user_id)
);

CREATE TABLE level (
	level_id int NOT NULL AUTO_INCREMENT,
	name varchar(100),
	PRIMARY KEY (level_id)
);

CREATE TABLE score (
	user_id int NOT NULL,
	level_id int NOT NULL,
	score int,
	PRIMARY KEY (user_id, level_id),
	FOREIGN KEY (user_id) REFERENCES user (user_id),
	FOREIGN KEY (level_id) REFERENCES level (level_id)
);

Passage au Java

Sous App Engine, nous allons déduire de ce schéma, non pas trois tables, mais trois types d’objets Java qui pourront être sauvegardés dans le datastore. Sur les classes de ces objets, nous allons ajouter différentes annotations JDO pour indiquer quelles sont les données de ces classes qui seront enregistrées dans le datastore. Les annotations les plus importantes sont @PersistenceCapable pour indiquer qu’une classe peut être enregistrée dans le datastore et @Persistent pour indiquer qu’une donnée membre de la classe est à sauvegarder. Comme avec MySQL on peut utiliser des identifiants en AUTO_INCREMENT, avec App Engine, nous allons avoir un identifiant pour chaque objet sous forme d’un objet Key et nous allons laisser le soin à JDO de générer une clé unique pour chaque objet grâce à l’annotation @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)

Voici le code source des classes User et Level avec les annotations JDO pour qu’elles puissent être enregistrées dans le datastore :

@PersistenceCapable
public class Level {

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

    @Persistent
    private String name;

    public Level(String name) {
        this.name = name;
    }

	public Key getKey() {
		return key;
	}

	public String getName() {
		return name;
	}

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

	@Override
	public String toString() {
		return "<level id=\"" + key + "\" name=\"" + name + "\" >";
	}
}
@PersistenceCapable
public class User {

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

    @Persistent
    private String login;

    @Persistent
    private String password;

	public User(String login, String password) {
		this.login = login;
		this.password = password;
	}

	public Key getKey() {
		return key;
	}

	public String getLogin() {
		return login;
	}

	public void setLogin(String login) {
		this.login = login;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	@Override
	public String toString() {
		return "<user id=\"" + key + "\" " + "login=\"" + login + "\" "
				+ "password=\"" + password + "\" >";
	}

Passons maintenant à la classe Score qui va avoir, en plus de son identifiant, deux objets Key afin d’avoir une référence vers un User et un Level. On peut donc assimiler ces deux objets Key aux FOREIGN KEY bien connues en MySQL. Comme vous pouvez le remarquer on ne demande pas à JDO de générer d’identifiants uniques pour ces deux clés puisque nous allons utiliser les clés existantes de l’user et du level. Nous créons donc les setters (mutateur en francais) nécessaires.

@PersistenceCapable
public class Score {

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

    @Persistent
    private Key userKey;

    @Persistent
    private Key levelKey;

    @Persistent
    private long score;

	public Score(Key userKey, Key levelKey, long score) {
		this.userKey= userKey;
		this.levelKey= levelKey;
		this.score = score;
	}

	public Key getKey() {
		return key;
	}

	public User getUser() {
		return DBManager.getUser(user);
	}

	public void setUserKey(Key userKey) {
		this.userKey= userKey;
	}

	public Level getLevel() {
		return DBManager.getLevel(level);
	}

	public void setLevelKey(Key levelKey) {
		this.setLevelKey= setLevelKey;
	}

	public void setScore(long score) {
		this.score = score;
	}

	@Override
	public String toString() {
		return "< time = " + user + " " + " " + level + " <" + time + "> >";
	}
}

Vous pouvez voir que dans cette classe, les getters (accesseurs en français) de User et Level passent par un DBManager. Nous vous parleront de cette classe dans la suite de ce tutorial où nous allons voir comment faire des requêtes sur le datastore avec JDO. J’espère que cette première partie du tutorial vous a plu et que vous allez vous aussi tenter de passer au ‘Cloud’ sur Google !

Vers la partie 2 !