Développer une application avec le framework Play !


précédentsommairesuivant

III. Premiers pas pour le modèle de données

Dans ce chapitre, nous allons écrire le modèle pour notre moteur de blog.

III-A. Introduction à JPA

La couche modèle occupe une place centrale au sein des applications Play (c'est également le cas pour les applications correctement architecturées). Il s'agit d'une représentation des données en langage dédié (domain-specific language) à partir de laquelle l'application travaille. Notre but étant de créer un moteur de blog, la couche modèle contiendra des classes telles que User, Post ou Comment.

Parce qu'il est préférable que la plupart des objets du modèle persistent même après un redémarrage de l'application, nous devons les stocker dans une base de données persistante. Un choix assez courant est d'utiliser une base de données relationnelle. Java étant un langage orienté objet, nous utiliserons un mapping objet-relationnel (Object Relational Mapper) afin de limiter la différence d'impédance (voir Wikipédia).

JPA est une spécification Java qui définit l'API standard pour le mapping objet-relationnel. Play choisit le célèbre framework Hibernate pour l'implémentation de cette spécification. L'un des avantages à utiliser JPA plutôt que l'API standard d'Hibernate est que tout le mapping est déclaré directement dans les objets Java.

Si vous avez déjà utilisé Hibernate ou JPA auparavant, vous serez surpris de la simplicité proposée par Play concernant ce point. Il n'est plus nécessaire de configurer quoique ce soit. JPA fonctionne sans aucune configuration ou installation supplémentaire (out of the box).

Si vous souhaitez avoir plus d'informations sur JPA, vous pouvez consulter le site de Sun à ce propos.

III-B. Commençons avec la classe User

Nous allons commencer par coder notre moteur de blog en créant la classe User. Tout d'abord, créons le fichier /yabe/app/models/User.java, et écrivons une première implémentation de cette classe :

 
Sélectionnez
package models;
 
import java.util.*;
import javax.persistence.*;
 
import play.db.jpa.*;
 
@Entity
public class User extends Model {
 
    public String email;
    public String password;
    public String fullname;
    public boolean isAdmin;
    
    public User(String email, String password, String fullname) {
        this.email = email;
        this.password = password;
        this.fullname = fullname;
    }
 
}

L'annotation @Entity marque cette classe comme une entité JPA. La classe parente, Model, offre un ensemble d'assistants pour JPA que nous découvrirons plus tard. Tous les champs de cette classe seront automatiquement persistés en base.

Il n'est pas nécessaire que cette classe étende play.db.jpa.Model. Toutefois, cela peut être une bonne idée car cela offre un certain nombre d'outils JPA.

Si vous avez utilisé JPA auparavant, vous devez savoir que chaque entité JPA doit fournir une propriété marquée par l'annotation @Id. Ici, notre classe parente Model fournit automatiquement un ID numérique auto-généré, ce qui est suffisant dans la plupart des cas.

Il ne faut pas voir ce champ généré comme un identifiant fonctionnel, mais bien comme un identifiant technique. Il est généralement conseillé de conserver ces deux concepts séparés, et ainsi de conserver un ID numérique auto-généré comme identifiant technique.

Un point qui ne vous a sans doute pas échappé, surtout si vous êtes un développeur confirmé, c'est que toutes les propriétés de notre classe sont publiques ! En Java (ainsi que pour les autres langages orientés objet), une bonne pratique stipule de mettre les propriétés en accès privé, mais de fournir des méthodes d'accès (les getters et setters), cela dans le but de promouvoir le principe important d'encapsulation. En réalité, Play se charge pour nous de générer les getters et setters afin de préserver cette encapsulation ! Nous verrons plus tard comment cela fonctionne exactement.

Nous pouvons désormais rafraîchir la page d'accueil et vérifier le résultat. A moins d'avoir fait une erreur lors de l'écriture de la classe User, nous ne devrions voir aucun changement. Effectivement, Play a automatiquement compilé et chargé la classe User mais pour l'instant, cela n'apporte aucune fonctionnalité particulière à l'application...

III-C. Ecriture du premier test

La meilleure méthode pour vérifier la classe User est d'écrire un cas de test JUnit. De cette façon, nous pourrons compléter progressivement notre modèle de données, et tester que tout se passe bien.

Avec Play, le lancement d'un test unitaire se fait par le démarrage de l'application dans un mode spécial, le mode "test". Nous devons tout d'abord stopper notre application, puis la redémarrer avec la commande suivante :

 
Sélectionnez
> play test
Image non disponible

La commande play test est très similaire à la commande play run à l'exception qu'il démarre également un module d'exécution de tests qui nous permet alors de lancer des tests depuis l'interface web !

Lorsque nous lançons une application Play en mode de test, Play utilise automatiquement l'ID du framework de test et charge le fichier application.conf automatiquement. Des informations supplémentaires sont disponibles ici.

Ouvrons maintenant notre navigateur à la page http://localhost:9000/@tests afin de voir l'exécuteur de tests. Sélectionnons tous les tests par défaut, et exécutons-les. Normalement, tous les indicateurs devraient virer au vert... Cependant, ces tests par défaut ne testent pas grand chose !

Image non disponible

Pour tester la partie du modèle de données, nous allons utiliser des tests JUnit. Comme vous pourrez le constater, il existe déjà un fichier de test, /yabe/test/BasicTest.java :

 
Sélectionnez
import org.junit.*;
import play.test.*;
import models.*;
 
public class BasicTest extends UnitTest {
 
    @Test
    public void aVeryImportantThingToTest() {
        assertEquals(2, 1 + 1);
    }
 
}

Supprimons ce test (c'est-à-dire supprimons la méthode aVeryImportantThingToTest() et non la classe), puis écrivons un test qui vise à vérifier la création d'un nouvel utilisateur ainsi que sa récupération :

 
Sélectionnez
@Test
public void createAndRetrieveUser() {
    // Create a new user and save it
    new User("bob@gmail.com", "secret", "Bob").save();
    
    // Retrieve the user with email address bob@gmail.com
    User bob = User.find("byEmail", "bob@gmail.com").one();
    
    // Test 
    assertNotNull(bob);
    assertEquals("Bob", bob.fullname);
}

Nous constatons ici deux méthodes utiles proposées par la classe parent Model : save() et find().

Vous pouvez obtenir plus d'informations sur ces méthodes dans la documentation de Play sur JPA.

De retour sur la page web de l'exécuteur de test, choisissons le fichier BasicTests.java, démarrons-le, et vérifions que les indicateurs restent verts.

Nous souhaitons maintenant ajouter une méthode dans la classe User qui a pour but de vérifier l'existence d'un utilisateur et d'un mot de passe donnés. Ecrivons-la, puis testons-la. Pour ce faire, nous écrivons la méthode connect() à la classe User ainsi que son test JUnit correspondant :

 
Sélectionnez
public static User connect(String email, String password) {
    return find("byEmailAndPassword", email, password).first();
}
 
Sélectionnez
@Test
public void tryConnectAsUser() {
    // Create a new user and save it
    new User("bob@gmail.com", "secret", "Bob").save();
    
    // Test 
    assertNotNull(User.connect("bob@gmail.com", "secret"));
    assertNull(User.connect("bob@gmail.com", "badpassword"));
    assertNull(User.connect("tom@gmail.com", "secret"));
}

A chaque fois que nous modifions notre code, nous devrions lancer les tests afin de s'assurer que nous n'avons pas ajouté de régressions.

III-D. La classe Post

La classe Post représente un billet de notre blog. Voyons une première implémentation de cette classe :

 
Sélectionnez
package models;
 
import java.util.*;
import javax.persistence.*;
 
import play.db.jpa.*;
 
@Entity
public class Post extends Model {
 
    public String title;
    public Date postedAt;
    
    @Lob
    public String content;
    
    @ManyToOne
    public User author;
    
    public Post(User author, String title, String content) {
        this.author = author;
        this.title = title;
        this.content = content;
        this.postedAt = new Date();
    }
 
}

Nous utilisons ici l'annotation @Lob qui indique à JPA que nous utilisons ici un type de données permettant le stockage d'une grande quantité de texte. Cette propriété correspond au contenu du billet. Nous déclarons ensuite la relation entre le User et notre classe, en utilisant le @ManyToOne, ce qui signifie qu'un utilisateur peut être à l'origine de plusieurs billets, mais qu'un billet n'a qu'un seul rédacteur.

Nous écrivons maintenant un nouveau cas de test permettant de nous assurer que cette classe fonctionne comme nous l'avions prévu. Mais avant cela, nous allons apporter une modification au niveau de notre classe JUnit. Dans l'état actuel du test, le contenu de la base de données n'est jamais supprimé, ce qui signifie qu'à chaque exécution, de nouveaux objets sont stockés dans la base de données. Cela devient problématique assez rapidement dès le moment où l'on disposera de tests qui auront besoin de compter le nombre d'éléments en base pour s'assurer que tout s'est bien passé.

Ecrivons donc une méthode setup() qui supprimera les données avant chaque test :

 
Sélectionnez
public class BasicTest extends UnitTest {
 
    @Before
    public void setup() {
        Fixtures.deleteAll();
    }
    
    ...
 
}

@Before est une annotation proposée par JUnit indiquant que notre méthode doit être appelée avant chaque méthode de test de notre classe.

Comme nous le voyons, la classe Fixtures est un assistant qui vient nous aider à gérer la base de données pendant les phases de tests. Nous pouvons à nouveau lancer les tests afin de nous assurer que nous n'avons rien cassé de l'existant, et commencer à écrire un nouveau test :

 
Sélectionnez
@Test
public void createPost() {
    // Create a new user and save it
    User bob = new User("bob@gmail.com", "secret", "Bob").save();
    
    // Create a new post
    new Post(bob, "My first post", "Hello world").save();
    
    // Test that the post has been created
    assertEquals(1, Post.count());
    
    // Retrieve all post created by bob
    List<Post> bobPosts = Post.find("byAuthor", bob).fetch();
    
    // Tests
    assertEquals(1, bobPosts.size());
    Post firstPost = bobPosts.get(0);
    assertNotNull(firstPost);
    assertEquals(bob, firstPost.author);
    assertEquals("My first post", firstPost.title);
    assertEquals("Hello world", firstPost.content);
    assertNotNull(firstPost.postedAt);
}

Comme nous n'utilisons pas forcément un IDE pour écrire notre code Java, il ne faut pas oublier ici d'inclure dans nos imports la classe java.util.List. Autrement, nous aurons une erreur de compilation.

III-E. Terminons avec Comment

Avant de considérer notre premier brouillon de modèle de données terminé, nous allons y ajouter la classe Comment et autoriser l'ajout de commentaires à un billet. L'écriture de cette classe ne révèle aucune difficulté particulière :

 
Sélectionnez
package models;
 
import java.util.*;
import javax.persistence.*;
 
import play.db.jpa.*;
 
@Entity
public class Comment extends Model {
 
    public String author;
    public Date postedAt;
     
    @Lob
    public String content;
    
    @ManyToOne
    public Post post;
    
    public Comment(Post post, String author, String content) {
        this.post = post;
        this.author = author;
        this.content = content;
        this.postedAt = new Date();
    }
 
}

Comme auparavant, nous rédigeons notre test unitaire :

 
Sélectionnez
@Test
public void postComments() {
    // Create a new user and save it
    User bob = new User("bob@gmail.com", "secret", "Bob").save();
 
    // Create a new post
    Post bobPost = new Post(bob, "My first post", "Hello world").save();
 
    // Post a first comment
    new Comment(bobPost, "Jeff", "Nice post").save();
    new Comment(bobPost, "Tom", "I knew that !").save();
 
    // Retrieve all comments
    List<Comment> bobPostComments = Comment.find("byPost", bobPost).fetch();
 
    // Tests
    assertEquals(2, bobPostComments.size());
 
    Comment firstComment = bobPostComments.get(0);
    assertNotNull(firstComment);
    assertEquals("Jeff", firstComment.author);
    assertEquals("Nice post", firstComment.content);
    assertNotNull(firstComment.postedAt);
 
    Comment secondComment = bobPostComments.get(1);
    assertNotNull(secondComment);
    assertEquals("Tom", secondComment.author);
    assertEquals("I knew that !", secondComment.content);
    assertNotNull(secondComment.postedAt);
}

Comme nous pouvons le constater, naviguer entre le Post et ses Comments n'est pas aisé. Nous devons ainsi passer par une requête pour retrouver l'ensemble des commentaires liés au billet. Nous pouvons cependant améliorer les choses en définissant, au niveau du Post le lien avec les commentaires. Cela se fait par l'ajout d'une propriété comments dans la classe Post :

 
Sélectionnez
...
@OneToMany(mappedBy="post", cascade=CascadeType.ALL)
public List<Comment> comments;
 
public Post(User author, String title, String content) { 
    this.comments = new ArrayList<Comment>();
    this.author = author;
    this.title = title;
    this.content = content;
    this.postedAt = new Date();
}
...

Notons l'utilisation de l'attribut mappedBy que nous avons faite pour dire à JPA que c'est la classe Post qui maintient la relation. Lorsqu'on définit une relation bidirectionnelle en JPA, il est très important de dire de quel côté la relation est maintenue. Dans notre cas, puisque les commentaires appartiennent à un billet, il est préférable que ce soit la classe Comment qui maintienne cette relation.

La propriété cascade indique à JPA que nous souhaitons que dans le cas où un billet est supprimé, alors tous les commentaires qui lui sont liés soient aussi supprimés.

Maintenant que nous avons établit cette relation, nous pouvons ajouter une méthode à la classe Post pour simplifier l'ajout de nouveaux commentaires :

 
Sélectionnez
public Post addComment(String author, String content) {
    Comment newComment = new Comment(this, author, content).save();
    this.comments.add(newComment);
    return this;
}

A nouveau, écrivons un nouveau cas de test :

 
Sélectionnez
@Test
public void useTheCommentsRelation() {
    // Create a new user and save it
    User bob = new User("bob@gmail.com", "secret", "Bob").save();
 
    // Create a new post
    Post bobPost = new Post(bob, "My first post", "Hello world").save();
 
    // Post a first comment
    bobPost.addComment("Jeff", "Nice post");
    bobPost.addComment("Tom", "I knew that !");
 
    // Count things
    assertEquals(1, User.count());
    assertEquals(1, Post.count());
    assertEquals(2, Comment.count());
 
    // Retrieve Bob's post
    bobPost = Post.find("byAuthor", bob).first();
    assertNotNull(bobPost);
 
    // Navigate to comments
    assertEquals(2, bobPost.comments.size());
    assertEquals("Jeff", bobPost.comments.get(0).author);
    
    // Delete the post
    bobPost.delete();
    
    // Check that all comments have been deleted
    assertEquals(1, User.count());
    assertEquals(0, Post.count());
    assertEquals(0, Comment.count());
}

Est-ce que nos indicateurs sont toujours dans le vert ?

Image non disponible

III-E. Utilisation de Fixtures pour l'écriture de tests plus complexes

Lorsqu'on aborde l'écriture de tests plus complexes, la question d'avoir un vrai jeu de données se pose. Fixtures nous autorise à écrire notre jeu de données en se basant sur le format YAML. Ces données seront ainsi chargées en base avant chaque test.

Editons le fichier /yabe/test/data.yml et décrivons un utilisateur :

 
Sélectionnez
User(bob):
    email: bob@gmail.com
    password: secret
    fullname: Bob
 
...

Pour la suite de notre tutoriel, vous pouvez télécharger le fichier Yaml final.

Nous rédigeons maintenant un nouveau cas de test qui utilise ces données, et réalise des vérifications sur celles-ci :

 
Sélectionnez
@Test
public void fullTest() {
    Fixtures.load("data.yml");
 
    // Count things
    assertEquals(2, User.count());
    assertEquals(3, Post.count());
    assertEquals(3, Comment.count());
 
    // Try to connect as users
    assertNotNull(User.connect("bob@gmail.com", "secret"));
    assertNotNull(User.connect("jeff@gmail.com", "secret"));
    assertNull(User.connect("jeff@gmail.com", "badpassword"));
    assertNull(User.connect("tom@gmail.com", "secret"));
 
    // Find all bob's posts
    List<Post> bobPosts = Post.find("author.email", "bob@gmail.com").fetch();
    assertEquals(2, bobPosts.size());
 
    // Find all comments related to bob's posts
    List<Comment> bobComments = Comment.find("post.author.email", "bob@gmail.com").fetch();
    assertEquals(3, bobComments.size());
 
    // Find the most recent post
    Post frontPost = Post.find("order by postedAt desc").first();
    assertNotNull(frontPost);
    assertEquals("About the model layer", frontPost.title);
 
    // Check that this post has two comments
    assertEquals(2, frontPost.comments.size());
 
    // Post a new comment
    frontPost.addComment("Jim", "Hello guys");
    assertEquals(3, frontPost.comments.size());
    assertEquals(4, Comment.count());
}

III-F. Sauvegardons notre travail

Nous avons maintenant terminé une partie importante de notre application de blog. Comme nous avons créé et testé toutes ces choses, nous allons à présent nous occuper de l'application web à proprement parler.

Mais avant cela, il est temps pour nous de sauvegarder notre travail dans notre gestionnaire de configuration, Bazaar. Exécutons la ligne de commande bzr st afin de voir les modifications apportées depuis le précédent commit :

 
Sélectionnez
> bzr st

Comme nous pouvions nous y attendre, certains nouveaux fichiers ne sont pas encore versionnés. Le répertoire test-result ne devant pas être versionné, nous indiquons à Bazaar de l'ignorer :

 
Sélectionnez
> bzr ignore test-result

Nous pouvons dès à présent ajouter les autres fichiers grâce à la commande bzr add :

 
Sélectionnez
> bzr add

Enfin, le commit final peut être réalisé, avec le petit commentaire qui va bien :

 
Sélectionnez
> bzr commit -m "The model layer is ready"

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.