image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Le stockage de données du modèle peut être réalisé directement en mémoire en utilisant des collections Java. Dans un tel cas, les données seraient perdues en cas d'extinction du serveur. Il est donc nécessaire de mettre en place un mécanisme de persistance pour les données.

Entités

L'API Java Enterprise Edition introduit l'annotation @Entity pour désigner une classe de modèle pour leur utilisation avec l'API de persistance Java Persistence API. Une telle classe représente un type de données manipulé par notre application.

Les entités sont considérées comme une sorte de Bean, un type de classe central pour JEE (il existe d'autres types de beans notamment utilisables pour les contrôleurs et services associés dans la spécification Enterprise Edition). Un bean entité contient traditionnellement différents champs représentant les données stockées associés à des getters et setters. Des frameworkds modernes tels que Play sont capables d'ajouter automatiquement dans le bytecode getters et setters s'ils ne sont pas présents dans le code. Sinon on peut les générer dans le code en s'aidant de son IDE favori (Eclipse, IntelliJ...).

Par exemple, pour représenter un utilisateur, on pourra utiliser l'entité suivante :

@Entity
public class User extends Model
{
	@Id
	private long id;
	
	@Required
	private String name;
	
	@Email
	@Required
	private String email;
	
	@Required
	private String password;
	
	// Getters and setters
	...
	// End of getters and setters
	
	public static Finder<Long, User> find = new Finder<Long, User>(User.class);
}

Il est conseillé que chaque entité dispose d'un champ d'identification annoté par @Id ; la valeur de ce champ est unique et sert à identifier l'objet dans la base lorsqu'il sera sauvegardé. On choisira généralement un type long (le type int peut quelquefois être trop limité).

Il est possible aussi de créer des entités référençant d'autres entités. Par exemple, il serait possible de créer un groupe d'utilisateurs (relation ManyToMany) :

@Entity
public class UserGroup extends Model
{
	private long id;
	
	@ManyToMany
	private List<User> users = new ArrayList<>();
	
	public List<User> getUsers() { return users; }
}

Notons qu'il serait également possible d'introduire une relation ManyToMany inverse dans la classe User pour indiquer à quels groupes appartient un utilisateur :

@ManyToMany(mappedBy="users")
private List<UserGroup> groups = new ArrayList<>();

L'indication du paramètre mappedBy est indispensable pour indiquer à quel champ de UserGroup se rapporte la relation.

Le fait d'hériter de Model et d'introduire un champ statique find est une spécificité pour la bibliothèque d'ORM Ebean (qui ne serait pas utilisée avec Hibernate). Il faut noter qu'aussi bien Ebean qu'Hibernate implantent la spécification JPA mais que chaque bibliothèque peut apporter des fontionnalités supplémentaires qui lui sont propres. Pour en savoir plus sur JPA et l'utilisation de l'ORM Hibernate, on pourra consulter cette page. Play utilise préférentiellement Ebean mais il est également possible d'employer Hibernate. Ebean présente l'avantage de ne pas nécessiter l'utilisation d'une session avec conservation d'état (EntityManager) : les objets gèrent eux-même leur persistence. Une limitation de cette approche est que les types scalaires (qui ne sont pas des entités) doivent obligatoirement avoir un contenu immuable (sinon il faut appeler soi-même la méthode markAsDirty() de l'entité pour signaler que le contenu a changé).

Il est possible de manipuler les entités en émettant manuellement des requêtes SQL. Cependant cette approche, si elle permet un contrôle fin sur le schéma de la base de données s'avère rapidement rébarbatif et sujet à des erreurs et failles de sécurité potentielles (notamment par injection SQL de valeurs non déspécialisées). L'utilisation d'un ORM est donc plutôt à recommander.

Utilisation de Ebean

L'utilisation de l'ORM Ebean avec Play est plutôt aisée. Il faut tout d'abord suivre l'étape de configuration spécifiée sur la documentation de Play (qui consiste principalement à déclarer des plugins à utiliser pour SBT ainsi qu'indiquer les bases de données où doivent être enregistrées chacune des classes).

Une fois les entités définies, nous pouvons ajouter des nouveaux objets persistants. Les classes héritant de Model disposent d'une méthode save() pour sauvegarder le contenu de l'objet dans une transaction séparée. Cette méthode peut aussi être indifféremment utilisée pour mettre à jour un objet en base. On peut aussi forcer une transaction en passant un Runnable à la méthode statique Ebean.execute(Runnable r) :

Ebean.execute(() -> {
    System.out.println(Ebean.currentTransaction()); // display the used transaction

    User user = User.find.byId(42L);
    user.setEmail("newemail@example.com");
    user.save();
    // if we want to delete the user from the base:
    // user.delete()
});

L'intérêt d'une transaction est de pouvoir enregistrer atomiquement différents objets ; en cas de problème (exception ou demande de rollback), aucune opération de la transaction n'est enregistrée et on revient à l'état antérieur. Il est également possible d'annoter une méthode avec @Transactional pour signaler qu'une transaction doit être ouverte sur une méthode.

L'intérêt d'Ebean par rapport à Hibernate est notamment un support plus aisé pour la récupération paresseuse de données. Par exemple le code suivant ne permet de récupérer que les adresses email des utilisateurs (dont l'adresse est sur le domaine example.com) sans leur nom :

List<User> users = select("email").where().email.iendsWith("example.com").orderBy().name.asc().findList();

On notera également que l'on peut réaliser des requêtes uniquement en chaînant des appels de méthodes alors qu'Hibernate fait usage du langage de requêtage JPQL (langage standard de JPA). La récupération paresseuse n'empêche pas par la suite de consulter le nom d'un utilisateur : dans ce cas, une nouvelle requête SQL sera automatiquement générée lorsque nous appelerons la méthode getName(). Pour en savoir plus sur la récupération paresseuse, on pourra consulter cette page de la doc.

Il est également possible de définir des requêteurs assurant une sureté du typage : cela nécessite d'utiliser un plugin générant automatiquement des classes de requêtage pour chacun des types (cf documentation de Ebean).

Les évolutions de schéma

Il est quelquefois nécessaire de pouvoir faire évoluer un modèle. Par exemple nous pourrions être intéressé à rajouter un champ sur l'utilisateur indiquant la date de sa dernière connexion, son adresse IP, d'autres informations de contact que son email... Ce genre de modification peut être réalisé avec une nouvelle classe reliée à la classe User par une relation OneToOne... or alors nous pouvons modifier le schéma de la table User pour intégrer ces nouveaux champs.

Modifier un modèle peut donc impliquer l'ajout de nouvelles colonnes à une table, la suppression de colonnes existantes, l'ajout de tables, la suppression de tables... Le mode d'auto-génération DDL est activé par défaut avec Play et peut s'avérer destructeur en cas de changement de modèle car des tables entières peuvent être détruites avant d'être recréées vide avec un nouveau schéma. Avant une mise à jour de modèle, il est donc indispensable de sauvegarder la base utilisée et ensuite de la restaurer (sachant que cela peut nécessite une adaptation des données si des nouvelles contraintes ont été ajoutées et qu'elles enfreignent les anciennes données).

En production, il est préférable de désactiver la génération automatique des schémas pour les mises à jour (evolutionplugin=disabled dans application.conf) et de créer manuellement des scripts SQL de miseà jour de la base de données utilisant notamment des instructions ALTER. Mais cela nécessite d'avoir une bonne connaissance du fonctionnement de l'ORM (traduction classe → schéma de table) pour que les tables coïncident avec les modèle défini par les classes. On pourra consulter à ce sujet la page de documentation consacrée aux évolutions de base : ce mécanisme permet d'intégrer au projet des scripts de mise à jour incrémentale de base.

Mais dans tous les cas, une précaution s'impose : sauvegarder régulièrement la base et a fortiori avant de mettre à jour l'application.

Les formulaires

Utilisation de formulaires

Les formulaires nous permettent de récupérer des données envoyées par un client web. Mettre en place un formulaire implique généralement la gestion de deux requêtes différentes. La première est une requête GET afin d'obtenir une page avec les champs du formulaire à remplir. La seconde est une requête POST où l'on poste effectivement le contenu du formulaire.

Manipuler un formulaire implique préalablement de créer une classe contenant les différents champs de celui-ci. On peut utiliser une entité mais il peut être possible de créer une classe spécifique pour plus de clarté qui ne correspondent pas à une entité stockée en base. En effet un formulaire peut ne pas demander tous les champs d'une entité. Par exemple pour un formulaire d'authentification d'un utilisateur, nous ne demanderons que son email et son mot de passe. L'id ou le nom ne sont donc pas demandés. D'autre part le mot de passe sur l'entité est stocké (normalement) sous forme chiffrée tandis que le mot de passe communiqué sur le formulaire l'est en clair. La validation des champs peut différer selon les cas.

A titre d'exemple, nous allons créer un formulaire de création d'utilisateur ; nous pouvons créer une classe RegisteringUser reprenant les champs de User et ajoutant un champ de vérification de mot de passe :

public class RegisteringUser
{
	... 
	
	@Required
	protected String passwordConfirmation;
}

La validation du formulaire (vérification de la validité des champs) doit toujours être réalisée côté serveur. Les annotations de contraintes sur les champs (@Required, @Email, @MinLength...) sont pour cela utilisées. Il est aussi possible d'utiliser une méthode de validation nommée validate et présente dans la classe manipulée par le formulaire pour un processus de validation plus complexe (par exemple pour vérifier la concordance des deux mots de passe entrés) :

/** If the validate method returns null, there is no problem and the form can be validated */
public List<ValidationError> validate()
{
	List<ValidationError> errors = new ArrayList<>();
	if (! Objects.equals(password, passwordConfirm))
		errors.add(new ValidationError("passwordConfirm", "The second typed password does not match the first one"));
	return (errors.size() > 0)?errors:null;
}

La méthode de validation peut également retourner un String tout simplement ; l'intérêt d'utiliser une liste de ValidationError est que l'on peut lier une erreur à un champ particulier.

Il existe également un plugin pour injecter les contraintes formulées sur les champs de la classe dans les champs HTML du formulaire : Play2-HTML5Tags.

Pour manipuler un formulaire, on utilise un FormFactory que l'on déclare comme champ du contrôleur (avec injection automatique) :

@Inject
private FormFactory formFactory;

On peut ensuite utiliser cette factory pour créer un Form<RegisteringUser> que l'on va remplir avec les données obtenues par la requête POST reçue :

Form<RegisteringUser> f = formFactory.form(RegisteringUser.class);
f = f.bindFromRequest(request());

Il faut ensuite vérifier si des erreurs de validation n'ont pas été relevées et dans ce cas ré-afficher la vue d'enregistrement d'utilisateur avec le formulaire annoté :

if (f.hasErrors())
	8return badRequest(views.html.login.render(f));

Si par contre il n'y a pas d'erreur, on peut afficher une vue félicitant l'utilisateur pour s'être enregistré :

if (! f.hasErrors())
{
	registerUser(f.get());
	return ok("Congratulations, your are now registered dear " + f.get().name());
}

La méthode registerUser(RegisteringUser ru) peut être écrite pour créer un User à partir d'un RegisteringUser et l'enregistrer en base :

public User registerUser(RegisteringUser ru)
{
	User u = new User();
	u.setName(ru.getName());
	u.setEmail(ru.getEmeil());
	u.setPassword(BCrypt.hashpw(ru.getPassword(), BCrypt.gensalt()));
	u.save(); // to save using the persistence API
}

On notera que le code est un peu rébarbatif car nous sommes obligés de copier les champs de RegisteringUser vers User. Notons également que nous hachons le mot de passe pour des raisons de sécurité afin de ne pas le conserver en clair en base. On pourra par la suite vérifier si un mot de passe fourni est identique avec le mot de passe original grâce à la méthode (que l'on pourra ajouter dans la classe User) :

public boolean checkPassword(String suppliedPassword)
{
	return BCrypt.checkpw(suppliedPassword /* in cleartext */, this.getPassword() /* hashed version */);
}

Cela nécessite d'installer une bibliothèque fournissant la classe JBCrypt (réalisant un hachage en utilisant l'algorithme Blowfish) comme indiqué ici.

Attaques sur formulaires

Les formulaires peuvent faire l'objet de différentes attaques malveillantes, parmi elles :

Un formulaire doit systématiquement être soumis avec la méthode POST (jamais GET) dès lors qu'il contribue à la modification d'un état sur le serveur ; ne pas le faire compromet la confidentialité des données émises (conservées dans l'historique) et facilite grandement l'exploitation d'attaques CSRF.