mercredi 25 mai 2011

Scala: persistance avec OrBroker

Aujourd'hui, mon exploration de Scala m'amène sur le terrain de la persistance, élément indispensable pour le développement d’applications d’entreprise. Certes, il y a JDBC avec son cortège d'exceptions à gérer, ses ResultSet à mapper...

Ne peut-on utiliser un framework ORM avec Scala? Un framework qui permettrait de conserver des entités bien écrites?

Pour ce premier article, je vais examiner OrBroker, un petit framework de persistance orienté Scala. L'exercice consistera à écrire les fonctions CRUD classiques qui feront le lien entre une table "Book" qui contient... des livres et des objets Book.

Table

J’utilise PostgreSQL, version 8.4. Dans le cadre de cet article, je veux juste des mappings simples entre des tables et des objets, et non des relations dans un graphe d’objets.

La table utilisée est basique:

CREATE TABLE book
(
 id serial NOT NULL,
 title character varying(255) NOT NULL,
 author character varying(255),
 isbn character varying(13) NOT NULL,
 CONSTRAINT book_pk PRIMARY KEY (id)
)

Une fois créée, je remplis la table avec quelques données:

insert into book (title,author,isbn) values ('Dune','Frank Herbert','1234567891231');
insert into book (title,author,isbn) values ('Le seigneur des anneaux','JRR Tolkien','1234567891232');
insert into book (title,author,isbn) values ('La carte et le territoire','Michel Houellebeck','1234567891444');

En suivant cette procédure, les ids attribués sont 1, 2 et 3 dans l’ordre des insertions.

Note: le code ISBN n'est pas correctement formaté, mais ce n'est pas très important dans le cadre de cet exercice.

Configuration de projet

C’est un projet SBT, développé avec Eclipse (voir http://architecte-software.blogspot.com/2011/05/environnement-de-developpement-scala.html). Le projet s’appelle LibraryOrBroker. Soit HOME le répertoire de base du projet.

Dans le répertoire HOME/project/build/, je crée la classe de build:

import sbt._
import de.element34.sbteclipsify._
class LibraryOrbrokerBuild(info:ProjectInfo) extends DefaultProject(info) with Eclipsify{
    val postgres = "postgresql" % "postgresql" % "8.4-701.jdbc3"
       val orBroker = "org.orbroker" % "orbroker" % "3.1.1"
}

afin d’ajouter dans les dépendances le driver postgresql et le jar d’OrBroker.

Dans la console sbt, un petit reload, suivi d’un update et d’un eclipse.

J’importe ensuite le projet dans Eclipse (http://architecte-software.blogspot.com/2011/05/environnement-de-developpement-scala.html).

Création de l’entité

Dans un package entities, la classe scala s’appelle Book.

package entities

class Book (var id:Option[Long], var title:String, var author:String, val isbn:String){
  def this(title:String, author:String, isbn:String) = this(None,title,author,isbn)
  
  override def toString = title+" de "+author+ " (ISBN: "+isbn+", id: "+id+")"
}

Je ne sais pas encore si c’est la meilleure manière de procéder, mais l’id étant auto-généré (par une séquence), on peut avoir des objets Book sans id (transients) ou avec id (persistants). Avec un type Option[Long], je peux avoir une valeur None (transient) ou un Some[Long] (persistant).

Les deux constructeurs traduisent ces deux possibilités.

De la manière dont l’entité est définie, l’id peut être modifié (lorsqu’il est attribué), de même que le titre et l’auteur s’ils sont édités. Par contre, une fois l’isbn attribué, il ne peut être modifié.

Premier test de select

En suivant l’exemple basique donné dans le wiki d'OrBroker (http://code.google.com/p/orbroker/wiki/Example1) et après avoir rencontré quelques problèmes (notamment le fait qu’OrBroker ne trouve pas les fichiers SQL dans le ClassPath), j’opte pour la définition de deux objets.

Le premier est un RowExtractor (pattern RowMapper) qui va transforme le résultat d'un select en objet Book:

import org.orbroker._
import entities._

object BookExtractor extends RowExtractor[Book]{
    def extract(row:Row) =  new Book(row.bigInt("ID"),row.string("TITLE").get,row.string("AUTHOR").get,row.string("ISBN").get)
}

Le deuxième est mon application:

import entities._
import org.orbroker._
import org.orbroker.config._

object TestCrud {
     val ds = new SimpleDataSource("jdbc:postgresql://localhost:5432/formation")
     val builder = new BrokerBuilder(ds)
     builder.setUser("formation","formation")
     val SelectBook = Token[Book]("SELECT id, title, author, isbn FROM book WHERE ID = :bookID", 'selectBook, BookExtractor)
     val SelectAllBooks = Token[Book]("SELECT id, title, author, isbn FROM book", 'selectAllBooks, BookExtractor)
     val broker = builder.build

     def main(args:Array[String]) = {
         println(find(1))
         println(find(4785))
         println("Liste des livres")
         val books = findAll
         books.foreach(println(_))
     }
  
   def find(id:Long) =  broker.readOnly() {session =>
       session.selectOne(SelectBook,"bookID"->id)/
   }
  
   def findAll = broker.readOnly() { session =>
     session.selectAll(SelectAllBooks)
   }

}

L'exécution de TestCrud écrit sur la console:

Some(Dune de Frank Herbert (ISBN: 1234567891231, id: Some(1)))
None
Liste des livres
Dune de Frank Herbert (ISBN: 1234567891231, id: Some(1))
Le seigneur des anneaux de JRR Tolkien (ISBN: 1234567891232, id: Some(2))
La carte et le territoire de Michel Houellebeck (ISBN: 1234567891444, id: Some(3))

Quelques remarques

Pour commencer, le selectOne renvoie un Option[Book]. Cela lui permet de renvoyer None si la requête ne renvoie aucune ligne. Ce qui m'ennuie, c'est que les méthodes find et findAll renvoient des "types" différents. Bien sûr, la deuxième renvoie une List, mais de Book, alors que la deuxième, plutôt que renvoyer un livre, renvoie une Option[Book]. D'où une différence dans la gestion des résultats. Avec le find, je dois faire un get sur le résultat pour avoir le livre alors que je peux itérer directement sur les livres de la liste renvoyée par findAll. Est-ce gênant?

Toujours est-il qu'une possibilité plus "Java" consiste à renvoyer null si l'id n'existe pas.

D'où la nouvelle méthode select:

def select(id:Long):Book = broker.readOnly() {session =>
       session.selectOne(SelectBook,"bookID"->id) match {
         case Some(b) => b
         case None => null
       }
   }

Une autre possibilité (que je n'aime pas), lancer une exception après le case None.

Une deuxième remarque est à propos de la gestion de la connexion. La documentation est assez floue sur le sujet, mais il apparaît que la session, utilisée de cette manière, gère automatiquement l'ouverture et la fermeture de la connexion.

La dernière remarque est d'ordre pratique: écrire le SQL dans les Token risque d'alourdir le code si les requêtes deviennent complexes.

Deuxième test: externalisation des requêtes

L'exemple donné sur le site d'OrBroker travaille avec un fichier sql mais en pratique, il ne fonctionne pas tel quel (en tout cas, pas avec sbt).

En fait, la doc est assez silencieuse sur le fait que les ressources sql doivent être enregistrées dans le brokerBuilder. Il y a bien un exemple (http://code.google.com/p/orbroker/wiki/JoinExample) mais il enregistre les fichiers via le file system, pas via le classpath, ce qui échoue plus facilement qu'il ne réussit...

En explorant les API et le code source, je vois qu'il faut utiliser ClasspathRegistrant (au lieu de FileSystemRegistrant, ce n'était pas sorcier) qui enregistre une map de ressources (symbol -> fichier de la ressource) dans le builder... Manque de documentation à nouveau, les api ne font que lister les classes et les méthodes, sans autres commentaires.

Quoiqu'il en soit, allons-y et ajoutons par la même occasion la sauvegarde d'une entité, c'est-à-dire son insertion ou son update et le delete pour terminer le CRUD.

Fichiers sql

Quatre fichiers sql doivent être créés dans le répertoire /src/main/resources, à la racine par exemple. On y retrouve les requêtes sql qui étaient dans le code auparavant, plus l'insert et l'upadte.

selectBook.sql
SELECT id, title, author, isbn FROM book WHERE ID = :bookID;

selectAllBooks.sql
SELECT id, title, author, isbn FROM book;

insertBook.sql
insert into book (title,author,isbn) values (:title,:author,:isbn);


updateBook.sql
update book SET title=:title,author=:author WHERE id = :bookID;

deleteBook.sql
delete from book where id=:bookID;


L'object TestCrud

On garde le BookExtractor, qui ne change pas. Par contre, l'object TestCrud devient:

object TestCrud {
     val ds = new SimpleDataSource("jdbc:postgresql://localhost:5432/test")
     val builder = new BrokerBuilder(ds)
     builder.setUser("postgres","postgres")
     val resources = Map(
         'selectBook -> "/selectBook.sql",
         'selectAllBooks -> "/selectAllBooks.sql",
         'insertBook -> "/insertBook.sql",
         'updateBook -> "/updateBook.sql",
         'deleteBook -> "/deleteBook.sql"
         )
     ClasspathRegistrant(resources).register(builder)
     val SelectBook = Token[Book]('selectBook, BookExtractor)
     val SelectAllBooks = Token[Book]('selectAllBooks, BookExtractor)
     val InsertBook = Token[Book]('insertBook, BookExtractor)
     val UpdateBook = Token[Book]('updateBook)
     val DeleteBook = Token[Book]('deleteBook)

     val broker = builder.build

     def main(args:Array[String]) = {
         println(find(1))
         println(find(4785))
         println("Liste des livres")
         var books = findAll
         books.foreach(println(_))
         val newBook = new Book("Test","moi","0000000")
         save(newBook)
         println("Id du nouveau livre: "+newBook.id)
         println("Liste des livres")
         books = findAll
         books.foreach(println(_))
         println("Suppression du nouveau livre")
         delete(newBook)
         println("Liste des livres")
         books = findAll
         books.foreach(println(_))
     }
  
   def find(id:Long) =      broker.readOnly() {session =>
       session.selectOne(SelectBook,"bookID"->id) match {
         case Some(b) => b
         case None => null
       }
   }
  
   def findAll = broker.readOnly() { session =>
     session.selectAll(SelectAllBooks)
   }

   def save(book:Book) = book.id match {
     case None =>
       broker.transactional() {transaction =>
         val b =transaction.executeForKey(InsertBook,"title" -> book.title,"author" -> book.author,"isbn" -> book.isbn)
         transaction.commit
         book.id = b.get.id
       }
     case _ =>
       broker.transactional() {transaction =>
         transaction.execute(UpdateBook,"bookID" -> book.id.get, "title" -> book.title,"author" -> book.author)
         transaction.commit
       }
   }
  
   def delete(book:Book) = broker.transactional() { transaction =>
       transaction.execute(DeleteBook,"bookID" -> book.id.get)
       transaction.commit
   }
}

Le résultat de l'exécution donne:

Dune de Frank Herbert (ISBN: 1234567891231, id: Some(1))
null
Liste des livres
Dune de Frank Herbert (ISBN: 1234567891231, id: Some(1))
Le seigneur des anneaux de JRR Tolkien (ISBN: 1234567891232, id: Some(2))
La carte et le territoire de Michel Houellebeck (ISBN: 1234567891444, id: Some(3))
Id du nouveau livre: Some(4)
Liste des livres
Dune de Frank Herbert (ISBN: 1234567891231, id: Some(1))
Le seigneur des anneaux de JRR Tolkien (ISBN: 1234567891232, id: Some(2))
La carte et le territoire de Michel Houellebeck (ISBN: 1234567891444, id: Some(3))
Test de moi (ISBN: 0000000, id: Some(4))
Suppression du nouveau livre
Liste des livres
Dune de Frank Herbert (ISBN: 1234567891231, id: Some(1))
Le seigneur des anneaux de JRR Tolkien (ISBN: 1234567891232, id: Some(2))
La carte et le territoire de Michel Houellebeck (ISBN: 1234567891444, id: Some(3))
 

Conclusions

Si l'on exclut la configuration, l'utilisation du framework est assez simple. Les méthodes "find" et "findAll" s'écrivent facilement, de même que le RowMapper.

Mais la configuration est le gros point noir. Elle est peu intuitive et le manque de documentation n'arrange rien.
Quelle que soit l'option choisie, c'est la partie "lourde". Dans le premier cas, les requêtes sql sont des String définis dans les Token. Dans le deuxième, ce sont autant de fichiers que de requêtes, mais une configuration un peu plus lourde. Peut-être faut-il d'ailleurs combiner les deux. Garder les requêtes simple "inline" et externaliser dans des fichiers sql les requêtes complexes.

D'autres questions se posent comme "combien faut-il d'instance du BrokerBuilder"? Une seule, sans doute. Encore que... Et combien de Broker?

Des idées pour un prochain article...

En attendant, pas vraiment convaincu...

mercredi 18 mai 2011

Maven: quand test et compile s'emmêlent

Voici un problème un peu vicieux auquel j’ai été confronté récemment. En cause, un comportement inattendu (du moins à mes yeux) de Maven.

Le contexte, un petit projet Web avec, entre autres, de l’Hibernate. Dans mon pom.xml, j’ai donc les lignes suivantes:

<dependency>
         <groupId>org.hibernate</groupId>
         <artifactId>hibernate-annotations</artifactId>
         <version>3.4.0.GA</version>
         <scope>compile</scope>
</dependency>

Cette dépendance est suffisante pour obtenir toutes les librairies nécessaires à Hibernate. Elle importe notamment Hibernate-core.

Pour les tests de mes dao, j’utilise HSQL comme DB mémoire et DBCP pour me fournir un pool de connexions.

Il y a aussi du spring dans mon projet, ce qui a finalement peu d'importance. Je configure les fichiers web.xml, applicationContext.xml... et j’essaye de déployer "à blanc" l’application sur un Tomcat. C’est une opération que j’effectue toujours avec un nouveau projet, car elle me permet de valider que la configuration de base est correcte.

Et là, l’application refuse de se déployer. Dans les logs, je vois un java.lang.NoClassDefFoundError: org/apache/commons/collections/map/LRUMap. C'est Hibernate qui cherche cette classe mais ne la trouve pas.

C’est une classe des commons-collections. Eclipse, avec le plugin Maven m'indique pourtant qu'elle se trouve dans mes dépendances. Une autre vérification m'indique que le jar est effectivement déployé dans le répertoire WEB-INF/lib. Cependant, la version déployée ne contient pas la classe LRUMap.

Il s'agit de la version 2.1 et en y regardant de plus près, je vois que Hibernate demande (dépendance transitive) la version 3.1.

Comment cela se fait-il?

Une version peut en masquer une autre

Le problème vient de la version de DBCP utilisée pour les tests, la 1.2.1, une "vieille".

<dependency>
         <groupId>commons-dbcp</groupId>
         <artifactId>commons-dbcp</artifactId>
         <version>1.2.1</version>
         <type>jar</type>
         <scope>test</scope>
</dependency>

Celle-ci a comme dépendance transitive la version 2.1 des commons-collections.

Du point de vue Maven, il y a donc conflit entre les deux versions. Pour le résoudre, Maven va choisir la dépendance la plus proche de la racine.

La version 3.1 de commons-collection est une dépendance transitive d’une dépendance transitive (hibernate-core) d’hibernate-annotations. Donc, un niveau 2. Par contre, la version 2.1 est une dépendance transitive directe de dbcp, donc de niveau 1.

C’est la version 2.1 qui est choisie.

Oui mais !

Le mécanisme de résolution est clair, mais dbcp est en test ! Et quand je fais un package, les dépendances de test ne sont pas incluses dans mon War final (de même que les classes et les ressources de test). C’est normal.

Et pourtant...

Si on fait un mvn  dependency:resolve, on obtient le résultat suivant (je ne garde que les lignes intéressantes):

[INFO] The following files have been resolved:
...
[INFO]    commons-collections:commons-collections:jar:2.1:compile
[INFO]    commons-dbcp:commons-dbcp:jar:1.2.1:test
...
[INFO]    org.hibernate:hibernate-annotations:jar:3.4.0.GA:compile
[INFO]    org.hibernate:hibernate-commons-annotations:jar:3.1.0.GA:compile
[INFO]    org.hibernate:hibernate-core:jar:3.3.0.SP1:compile
…

Le scope de dbcp est bien test. Le scope d’hibernate est bien compile. De même pour commons-collection... sauf qu’il est dans la version 2.1, celle qui vient de test.

Et si je fais mvn dependency:resolve -DincludeScope=compile, je n’ai pas les jars de scope test (heureusement !). Commons-collections y est bien, puisqu’il est une dépendance transitive d’un jar en scope compile (hibernate)  MAIS sa version est celle qui vient de dbcp, lequel est en test.

Est-ce un bug? Toujours que le résultat est le même avec Maven version 2.2.1 et 3.0.3.

Le compile et le packaging ne devraient-ils se baser uniquement sur les dépendances compile et provided (hors test) pour résoudre les dépendances et leurs versions?

Quoi qu’il en soit, dans mon cas, la solution au problème était assez simple, puisqu’il suffisait de changer la version de dbcp:

<dependency>
       <groupId>commons-dbcp</groupId>
       <artifactId>commons-dbcp</artifactId>
       <version>1.3</version>
       <type>jar</type>
       <scope>test</scope>
</dependency>

Cela met les pendules à l’heure puisque la dépendance transitive vers les commons-collection est aussi 3.1.

N’empêche, le risque est  là. Dans le cas présent, le problème empêchait le déploiement, mais ne peut-on pas imaginer que le problème se produirait plus tard, à l'exécution d'une obscure méthode (forcément jamais testée...), avec alors beaucoup de difficultés pour déterminer la cause profonde.

Voilà qui fait froid dans le dos, non?

lundi 16 mai 2011

Environnement de développement Scala: sbt + Eclipse

Dans le cadre de tests de persistance avec Scala, je me suis rapidement rendu compte que travailler avec sbt (incontournable) et Notepad++ était assez inconfortable.
C'est d'autant plus vrai qu'Eclipse dispose d'un plugin qui fonctionne bien et qu'il existe pour sbt un plugin permettant de rendre un projet sbt compatible avec Eclipse: Eclipsify.
Dans un document où je tiens la liste des les ressources Scala que je trouve, j’ai indiqué le site d’Eclipsify (http://github.com/musk/SbtEclipsify) et j’ai ajouté comme commentaire "ça marche, bien lire!". Malgré tout, j’ai dû m’y reprendre à plusieurs reprises. Comme quoi "bien lire" est plus compliqué qu’il n’y paraît...
Voici la procédure que j’ai suivie. Bien lire...

Le plugin scala pour Eclipse

Pour ma part, j’utilise Eclipse 3.6 (Helios). Quant au plugin Scala, j'utilise la version 2.0.0, qui est certes en bêta, mais qui fonctionne correctement. Le site d'update se trouve à http://download.scala-ide.org/releases/2.0.0-beta/
L'installation du plugin se fait comme pour tous les plugins dans Eclipse.

Simple Build Tool - sbt

Le site de sbt se trouve à http://code.google.com/p/simple-build-tool/. Pour le faire fonctionner, il suffit de dowloader la dernière version du jar et de créer un script de lancement (indiqué sur le site).
Voici la version windows, sbt.bat
set SCRIPT_DIR=%~dp0
java -Dhttp.proxyHost=proxy -Dhttp.proxyPort=80 -Xmx512M -jar "%SCRIPT_DIR%sbt-launch-0.7.7.jar" %*
Il est bien entendu que la version de sbt-launch.jar correspond à la version utilisée.
Pour terminer, il faut ajouter le répertoire qui contient le ".bat" dans la variable d'environnement path.

Eclipsify

Voilà deux bons outils, mais qui ne sont pas liés. Par défaut, sbt utilise une structure de répertoires du genre Maven, là où Eclipse s'en tient à un simple répertoire src. Les deux peuvent être liés avec le plugin Eclipsify. Le terme "plugin" peut être ambigu ici: il s’agit d’un plugin sbt et non d'Eclipse.
Son utilisation doit être configurée dans un projet sbt.
Par convention, "HOME" est le répertoire de base du projet.

Config du plugin

Dans HOME/project/plugins (plugins doit sans doute être créé), créer un ficher NomDuProjectPlugins.scala. C’est là qu’on va configurer l’utilisation du plugin:

import sbt._
class NomDuProjetPlugins(info: ProjectInfo) extends PluginDefinition(info) {
    lazy val eclipse = "de.element34" % "sbt-eclipsify" % "0.7.0"
}

Config du projet

Dans HOME/project/build, créer le fichier de Build du nom NomDuProjectBuild.scala:

import sbt._
import de.element34.sbteclipsify._
class NomDuProjectBuild(info:ProjectInfo) extends DefaultProject(info) with Eclipsify{
    //dépendances du projet, configuration du projet...
}

A noter le "with Eclipsify" puisque c'est celui que j’oublie à chaque fois... :-)

Recharger la configuration

Dans la console sbt, taper reload. sbt recharge la configuration du projet. Si tout s’est bien passé, vous avez désormais une action “eclipse” disponible.
Il suffit de taper "eclipse" dans la console sbt pour “éclipsifier” le projet. Si vous n’en êtes pas là, "actions" vous montrera la liste des actions disponibles: "eclipse" devrait être dedans....

Importer le projet dans Eclipse

Dans Eclipse, import existing project... Choisir le HOME. Le projet est importé avec la nature scala.

Au boulot maintenant ! Ou presque.

En fait, il faut développer dans eclipse et utiliser sbt pour lancer les tests, packager...

Integration plugin

Il existe un plugin Eclipse permettant l'intégration de sbt avec Eclipse. Pour le moment, il sent encore le neuf et la documentation est un peu faible.
Voici l'adresse: http://www.assembla.com/spaces/sbt-eclipse-integration/wiki. Un site d'update existe momentanément car le responsable de ce projet souhaite le lier à Eclipsify. http://sbtei.element34.de/updatesite/
Le plugin s'installe sans problème et permet d'ajouter une nature "sbt" au projet. Cela se fait depuis la package view, un clic droit sur le projet. Une entrée sbt s'affiche dans le menu contextuel.
Dans le préférence, il faut aussi configurer le sbt-launcher.jar utilisé.
Ce qu'il fait d'autre? Pour le moment, je l'ignore...

jeudi 17 mars 2011

Traits, philosophie et stratégie en Scala

Les traits en Scala sont vraiment intéressants. Cette fois, j'ai envie de m'en servir pour ajouter des comportement à des classes.

Le comportement, c'est "Philosophe" et je veux l'ajouter à... à des grenouilles. Ce comportement se traduira par une méthode “philosophe”, sans arguments et qui renverra la String "Je pense donc je suis".

Commençons avec une Grenouille:
class Grenouille(nom:String) {
   def croasse() = "Croua, croua! dit "+nom
}

val g = new Grenouille(“Reinette”)
println(g croasse)
Croua, croua! dit Reinette

(Quand je disais que c’était bête...)

Et maintenant, voici le comportement “Philosophe”, défini dans un trait:
trait Philosophe {
   def philosophe() = "Je pense donc je suis"
}

Si je veux une Grenouille philosophe, il me suffit d’écrire:
class GrenouillePhilosophe(nom:String) extends Grenouille(nom) with Philosophe
Ce qui me donne les résultats suivants:
val g = new GrenouillePhilosophe(“Reinette”)
println(g croasse)
Croua, croua! dit Reinette
println(g philosophe)
Je pense donc je suis

L’intérêt du comportement, c’est que je peux l’ajouter à n’importe quoi.

A une vache par exemple:
class Vache(nom:String) {
   def meugle() = "Meuhh! dit "+nom
}

class VachePhilosophe(nom:String) extends Vache(nom) with Philosophe

Ou encore à un rocher:
class Rocher(nom:String) {
   def roule() = nom + " n'amasse pas mousse"
}

class RocherPhilosophe(nom:String) extends Rocher(nom) with Philosophe

Je vous passe les arbres, les astres... (mais si un jour quelqu’un doit écrire une application dans laquelle des grenouilles et des rochers deviennent philosophes, qu’il me contacte !)

L'équivalent en Java

Le code Scala se compile en bytecode exécutable par une JVM. La question est alors "comment obtenir le même effet en Java?"
Soit trois classes de base à transformer en philosophes. Les voici, toutes à la suite les unes des autres:
public class Grenouille {
   private final String nom;
   
   public Grenouille(String nom){
       this.nom = nom;
   }
   
   public String croasse(){
       return "Croua, croua! dit "+nom;
   }
}

public class Vache {
   private final String nom;
   
   public Vache(String nom){
       this.nom = nom;
   }
   
   public String meugle(){
       return "Meuhh! dit "+nom;
   }
}

public class Rocher {
   private final String nom;
   
   public Rocher(String nom){
       this.nom = nom;
   }
   
   public String roule(){
       return nom + " n'amasse pas mousse";
   }
}
A noter au passage la longueur du code Java par rapport à celui en Scala...

A présent, imaginons les différentes façons d'ajouter un comportement "philosophe" à nos classes. La bonne solution devrait sauter aux yeux de tout le monde, mais l’expérience m’a appris que ce n’était pas toujours le cas.

L’héritage

Par exemple:
public class GrenouillePhilosophe extends Grenouille {
   public GrenouillePhilosophe(String nom){
       super(nom);
   }
   
   public String philosophe(){
       return "Je pense donc je suis";
   }
}

Si je pose la question autour de moi, je suis sûr que c’est la réponse qui va revenir le plus souvent. Les développeurs orientés objet aiment l’héritage, qu’ils considèrent comme le fond même de l’orienté objet. Il faut reconnaître que cette méthode semble fonctionner.

Je peux bien sûr l’appliquer à Vache et à Rocher (mais je vous passe le code).

Néanmoins, voici quelques objections:
  1. Java ne permet pas l’héritage multiple. Dans un exemple aussi simple, ça ne pose pas de problème, mais dans une structure complexe, composée de différentes hiérarchies de classes, cette méthode ne sera pas possible.
  2. Chaque classe implémente sa méthode “philosophe”. D’où un coût de maintenance élevé s’il faut modifier le comportement, avec des risques d’erreur.
  3. Les classes “philosophes” ne sont pas de type Philosophe. Elles n’ont rien en commun. En Scala, je peux écrire le code suivant:
    val phi = Set(new VachePhilosophe("Rita"), new GrenouillePhilosophe("Reinette"), new RocherPhilosophe("Pierre"))
    phi.foreach((p) =>println(p philosophe))
    Je pense donc je suis
    Je pense donc je suis
    Je pense donc je suis
    Dans la solution proposée ci-dessus en Java, c’est impossible (à moins de passer par la réflexion).

Bref, cette solution n’est pas équivalente à Scala.

Implémentation d’une interface

Etre Philosophe, c’est implémenter l’interface suivante:
public interface Philosophe {
   String philosophe();
}
Voici son utilisation:
public class VachePhilosophe extends Vache implements Philosophe {
   public VachePhilosophe(String nom) {
       super(nom);
   }
   
   public String philosophe() {
       return "Je pense donc je suis";
   }
}

Cette solution a le mérite de résoudre deux des précédentes objections: le problème de l’héritage multiple est résolu et toutes les classes “philosophes” sont bien des Philosophe(s).

En fait, on très proche des traits Scala qui sont souvent proposés en remplacement des interfaces (lesquelles n’existent pas en Scala).

Il reste cependant la dernière objection: chaque classe devra implémenter sa méthode philosophe, avec duplication de code et problèmes de maintenance.

C’est mieux, mais pas encore ça.

La composition "simple"

A la base, la composition permet d’encapsuler un comportement dans une classe et d’utiliser cette classe pour "hériter" du comportement.

Par exemple, le comportement du philosophe est le suivant:
public class Philosophe {
   public String philosophe(){
       return "Je pense donc je suis";
   }
}
La création d’un Rocher philosophe, se ferait ainsi:
public class RocherPhilosophe extends Rocher {
   private Philosophe philosopheComportement = new Philosophe();
   
   public RocherPhilosophe(String nom) {
       super(nom);
   }
   
   public String philosophe(){
       return philosopheComportement .philosophe();
   }
}

Est-ce mieux? Pas vraiment. Certes, le comportement est encapsulé et s’il est modifié, les modifications seront répercutées automatiquement. On peut éventuellement objecter qu’une partie de code sera dupliquée (l’utilisation de la composition), mais c'est finalement peu de chose.

Par contre, notre RocherPhilosophe n’est pas un Philosophe. Petit problème simple à résoudre...

La composition améliorée



C’est beaucoup mieux à présent:
  • le comportement "philosophe" est isolé dans une classe, comme avec la composition simple ci-dessus
  • notre RocherPhilosophe est un Philosophe
  • cette relation “isA”, parce qu’elle vient de l’implémentation d’une interface, n’interfère pas avec l’héritage

Il reste un point noir: dans chaque classe “Philosophe” (comme RocherPhilosophe), il faudra implémenter la logique de composition. Je ne vois pas de solution en Java.

A ce niveau, le trait de Scala est bien sûr supérieur.

Une petite remarque en passant: il n’est pas vraiment nécessaire que PhilosopheImpl et RocherPhilosophe implémentent la même interface (Philosophe). Le but de l'exercice était de trouver l'équivalent d'une utilisation des traits Scala en Java, ce à quoi répond le schéma ci-dessus.

Néanmoins, il y a mieux à faire, car ce schéma a des airs de pattern Strategy. Alors, poussons la logique plus loin, même si cela nous éloigne momentanément de code Scala initial.

Pattern strategy

Les philosophes que sont nos vaches, grenouilles ou rochers peuvent appartenir à différentes écoles de pensée. Tous sont philosophes, mais philosophent différemment. Mieux, ce comportement (leur école) peut être modifié au runtime.

Voici l’implémentation Java (toutes les classes à la suite des unes des autres, à l'exception des classes de base déjà écrites plus haut):
public abstract class EcolePhilosophique {
   public abstract String penser();
}

public class EcoleCartesienne extends EcolePhilosophique {
   public String penser() {
       return "Je pense donc je suis";
   }
}

public class EcoleExistentialiste extends EcolePhilosophique {
   public String penser() {
       return "Le faire est révélateur de l'être";
   }
}

public interface Philosophe {
   String philosophe();
}

public class VachePhilosophe extends Vache implements Philosophe {
   private EcolePhilosophique ecolePhilosophique = new EcoleExistentialiste();

   public VachePhilosophe(String nom) {
       super(nom);
   }
   
   public String philosophe() {
       return ecolePhilosophique.penser();
   }
}

public class GrenouillePhilosophe extends Grenouille implements Philosophe {
   private EcolePhilosophique ecolePhilosophique = new EcoleCartesienne();

   public GrenouillePhilosophe(String nom) {
       super(nom);
   }
   
   public String philosophe() {
       return ecolePhilosophique.penser();
   }
}

L’implémentation est simple. Ici, la séparation entre la stratégie (l’école philosophique) et le contexte (les objets qui l’utilisent) est claire. Le comportement est défini de manière statique, mais il est possible d'apporter quelques modifications au code pour pouvoir le changer au runtime.

Le seul point noir est que la composition doit encore une fois être explicitement écrite.

Est-ce que Scala apporte une solution?

Implémentation du pattern Strategy en Scala

Le voilà donc, tel qu’il est décrit dans le schéma. Les classes Vache, Grenouille et Rocher sont celles données tout au début de cet article.

abstract class EcolePhilosophique {
   def penser() : String
}

class EcoleCartesienne extends EcolePhilosophique {
   override def penser () = "Je pense donc je suis"
}

class EcoleExistentialiste extends EcolePhilosophique {
   override def penser () = "Le faire est révélateur de l'être"
}

trait Philosophe {
   def ecolePhilosophie: EcolePhilosophique
   def philosophe() = ecolePhilosophie.penser
}

class GrenouillePhilosophe(nom:String) extends Grenouille(nom) with Philosophe{
   def ecolePhilosophie = new EcoleCartesienne
}

class VachePhilosophe(nom:String) extends Vache(nom) with Philosophe{
   def ecolePhilosophie = new EcoleExistentialiste
}

Quelques remarques:
  • la stratégie (l’école) est choisie lors de la construction de l’objet et est donc statique. Néanmoins, le code peut être facilement modifié pour que le comportement soit modifiable dynamiquement.
  • EcolePhilosophique est une classe abstraite. Elle pourrait être un trait, mais je n’en ai pas vu l’intérêt. De plus, dans le schéma UML, elle n’est pas définie comme une interface.

Grâce à l’utilisation du trait “Philosophe”, les classes “philosophes” ne doivent plus implémenter la logique de la composition (une référence vers l’école et l’invocation de la méthode “penser” dans l’implémentation de la méthode “philosophe”). A cause de cela, l’implémentation en Scala, bien que ni plus ni moins souple qu’en Java, est meilleure.

Et puis, franchement, c'est beaucoup plus court en Scala !

mardi 22 février 2011

Relations sans foreign keys en Hibernate


En Hibernate, une relation entre deux entités s'exprime en générale à l'aide d'une foreign key. Néanmoins, de nombreux développeurs s'imaginent à tort qu'Hibernate a besoin d'une contrainte de foreign key entre les tables pour mapper une relation.

Certes, c'est une bonne idée. Prenons par exemple le cas de deux tables (MASTER et DOG, tables utilisées dans un précédent article). DOG ne contient que deux colonnes: ID et NAME. MASTER en contient trois: ID, NAME et DOG_ID, cette dernière contenant un ID de la table DOG.

Un schéma classique qui se mappe comme suit (cf. toujours le même article):
@Entity
public class Dog {
     @Id @GeneratedValue(strategy=GenerationType.AUTO)
     private Integer id;

     private String name;

     //Setters, getters, equals, hashcode...   
}
et
@Entity
public class Master {

     @Id @GeneratedValue(strategy=GenerationType.AUTO)
     private Integer id;

     private String name;

     @OneToOne
     private Dog dog;

     //setters getters, equals, hashcode    
}
Si on laisse à Hibernate le soin de générer le schéma, il ajoutera une contrainte FK sur la colonne DOG_ID pour que les valeurs qu'elle contient soient toujours des clés primaires de DOG.

Sans contrainte

Cependant, cette contrainte n'est absolument pas nécessaire et cela n'empêchera pas Hibernate de fonctionner (de la même manière, une propriété annotée @Id ne doit pas forcément correspondre à une clé primaire, mais c'est une autre histoire).

Curieusement, ce qui ressemble à une mauvaise pratique est plus courant qu'on ne le croit, même si elle ne se justifie souvent que par le poids de l'héritage (legacy).

Cela pose quand même un problème. Imaginons que notre DB contiennent les informations suivantes:
  • dans DOG, une ligne ID=1, NAME=Brutus
  • dans MASTER, une ligne ID=2, NAME=Toto, DOG_ID=1 et une ligne ID=3, NAME=Totor, DOG_ID=7

Nous avons donc une ligne master dont le DOG_ID ne référence aucun DOG.

Que va-t-il se passer avec le code suivant?
public class TestContrainte {
    public static void main(String[] args) {
        Configuration cfg = newAnnotationConfiguration().configure(); 
        SessionFactory sf = cfg.buildSessionFactory();
        Session session = sf.openSession();

        Master maitre = (Master)session.get(Maitre.class, 3);

        System.out.println(maitre.getNom());
        if(maitre.getChien() == null){ 
                System.out.println("Chien introuvable");
        } else {
                System.out.println("Chien: "+maitre.getChien().getNom());
        }

        session.close();
    }
} 

En fait, le code lance une exception:
Exception in thread "main" org.hibernate.ObjectNotFoundException: No row with the given identifier exists: [entities.Chien#7]

Evidemment, si on avait demandé l'id 2, on aurait obtenu la réponse:
Toto
Chien: Brutus

La solution

Le fait est qu'Hibernate considère que la référence vers Dog DOIT exister si une “FK” existe. Une exception est donc lancée. Ce comportement par défaut est généralement correct, mais dans les cas où la contrainte de foreign key n'existe pas, le code risque de planter.

Heusement, Hibernate propose un moyen de s'en sortir par le biais de ses annotations propres (hibernate-annotations).

Voici comment modifier la référence dans Master vers Dog:
@OneToOne
@NotFound(action=NotFoundAction.IGNORE)
private Dog dog;
L'annotation @org.hibernate.annotations.NotFound prend l'enum org.hibernate.annotations.NotFoundAction en paramètre. La valeur prise par défaut est NotFoundAction.EXCEPTION, ce qui correspond au comportement généralement observé. Mais quand "action" est en "IGNORE", comme dans ces quelques lignes, si Hibernate rencontre une "foreign key" ne pointant vers aucune ligne, il ne lancera pas d'exception, mais se contentera mettre la référence à null.

Ainsi donc, le test donné ci-dessus donnera comme résultat:
Totor
Chien introuvable

Ce qui n'est pas faux...

samedi 19 février 2011

NonUniqueObjectException, cascade et evict

Notre équipe d'architectes (Yannick et moi-même) a volé à la rescousse par un développeur qui ne savait plus à quel Saint se vouer.

Son code, à base d'Hibernate et de Spring, lançait une org.hibernate.NonUniqueObjectException et il n'en comprenait pas l'origine et pouvait encore moins s'en débarrasser.

Je l’avoue, dans les standards de développement mis en place (et que certains appellent à tort "architecture"), c’est la première fois que je rencontre ce cas.

L’API d’Hibernate décrit l'exception comme suit:
This exception is thrown when an operation would break session-scoped identity. This occurs if the user tries to associate two different instances of the same Java class with a particular identifier, in the scope of a single Session.

Reproduction simple

L'erreur est facile à reproduire. Utilisons pour l'exemple une entité Dog, simpliste:
@Entity
public class Dog {
     @Id @GeneratedValue(strategy=GenerationType.AUTO)
     private Integer id;

     private String name;

     //Setters, getters, equals, hashcode...   
}

Dans la base de données, il y au moins un entrée d’id 1 et de name "Bill". Voici comment reproduire l’erreur:
public class NonUniqueChien {
     public static void main(String[] args) {
          SessionFactory sf = new AnnotationConfiguration().configure().buildSessionFactory();
          Session session = sf.openSession();
          Transaction tx = session.beginTransaction();

          Dog oldDog = (Dog) session.get(Dog.class, 1);

          Dog dog = new Dog();
          dog.setName("Totor");
          dog.setId(1);

          session.saveOrUpdate(dog);

          tx.commit();
          session.close();
          sf.close();
     }
}

L’exécution de ce script provoque la org.hibernate.NonUniqueObjectException.

Explications:
  1. A la ligne 7, le code va rechercher l’entité Dog correspondant à l’id 1 (ce brave Bill). La session contient donc une entité Dog d’id 1.
  2. De 9 à 11, le code construit ensuite un objet Dog transient, avec un autre nom, mais le même id.
  3. A la ligne 13, il appelle saveOrUpdate. Dans la mesure où il y a un id, déclaré comme étant auto-généré, Hibernate part du principe que c’est un update. Par sécurité, le framework vérifie si l’objet existe déjà en session, ce qui est le cas (il s'agit de Bill). Ce n’est donc pas la même référence. Du point de vue d’Hibernate, il y a un risque: deux objets, représentant la même entité, peuvent potentiellement contenir des champs différents (ce qui est le cas ici). Quelle entité persister? Le chien qui s’appelle “Bill” ou celui qui s’appelle “Totor”? Ne pouvant décider, Hibernate lance la NonUniqueObjectException.

A noter que le problème se pose aussi avec "update" (bien sûr), mais pas avec "save" (Hibernate considère qu’il s’agit d’une nouvelle entité et génère un index à la place de celui fourni), ni avec merge évidemment.

Le problème est apparemment simple, mais détecter où il se produit est plus difficile. Le développeur, débutant en Spring et en Hibernate, a perdu le contrôle sur son application. Le modèle est complexe, avec des références vers de nombreuses autres entités dans des relations ToMany ou ToOne, toujours bidirectionnelles et du cascading CascadeType.ALL sur toutes les relations. C’est un cauchemar. Les méthodes s’enchaînent, passent d’un service ou d’un DAO à l’autre, sautant du transactionnel au non transactionnel...

Reproduction du problème complexe


Après un long moment de debugging, le noeud du problème est enfin trouvé.

Le développeur, parce qu'il ne maîtrise pas les transactions, le dirty checking et le lazy-loading, a fait beaucoup d'erreurs. Pour tenter de résoudre une des difficultés rencontrées, il a utilisé à plusieurs reprises des "evict" sur la session.

Une petite parenthèse s'impose ici: que ne nous a-t-il pas appelé à l'aide plus tôt, dés qu'il a eu des soucis! Au lieu de ça, il s'est enfoncé dans du bricolage fait du bricolage, pour obtenir au final un code spaghetti, non maintenable, d'une grande fragilité... et qui ne fonctionne pas. Fin de la parenthèse.

Voici comment on peut reproduire le problème tel qu'il l'a, mais d'une manière hautement simplifiée...

Une deuxième entité est nécessaire: le maître du chien. Présentation donc de Master:

@Entity
public class Master {

     @Id @GeneratedValue(strategy=GenerationType.AUTO)
     private Integer id;

     private String name;

     @OneToOne(cascade=javax.persistence.CascadeType.ALL)
     private Dog dog;

     //setters getters, equals, hashcode    
}


En DB, une ligne est créée dans chacune des tables. Nous avons maintenant notre Dog d'id=1 (Bill) et un Master d'id=2 (Boule), fier maître de Bill grâce à sa foreignkey vers Dog valant 1.

Le code suivant va provoquer l’erreur:

public class NonUniqueObjectTest {
    public static void main(String[] args) {
        SessionFactory sf = new AnnotationConfiguration().configure().buildSessionFactory();
        Session session = sf.openSession();
        Transaction tx = session.beginTransaction();
       
        Master boule = (Master) session.get(Master.class,2);
        session.evict(boule.getDog());
       
        Dog bill = (Dog) session.get(Dog.class, 1);
       
        tx.commit();
        session.close();
        sf.close();
    }
}


Explications
La cause du problème est due à une combinaison du "evict" et du cascading. Des deux, seul le "evict" est réellement erroné. Si le cascading était supprimé, il n'y aurait plus d'erreur, mais la logique du code resterait douteuse.
  1. A la ligne 7, l'entité Master (Boule) est récupérée. Elle contient une référence vers Dog (Bill).
  2. A la ligne 8, l'entité Dog (Bill) est détachée de la session avec "evict".
  3. A la ligne 10, l'entité Dog (Bill) est récupérée à nouveau. Il y a alors deux objets Dog (Bill): un est référencé par la propriété dog de Master (Bill), l'autre par la variable bill.
  4. A la ligne 12, la transaction est commitée. La session est flushée. La cascading amène Hibernate à vérifier le statut de l'objet référencé par dog. Voyant qu'il n'est pas attaché, il détecte qu'il est détaché (et non transient) et vérifie que la session ne contient pas déjà cette entité. Or, comme nous l'avons rechargée à la ligne 10, elle la contient. Hibernate, ne pouvant décider quel objet est le bon, lance une exception. L'ironie dans le cas présent est que les deux objets sont rigoureusement identiques...

Solution

Malheureusement pour le développeur, il n'y a pas de solution miracle. Même si dans le code qui précède, on pourrait résoudre le problème de différentes manières (retirer le "evict", retirer le cascading, limiter le cascading pour ne pas considérer les updates...), ce n'est pas aussi simple dans la réalité.

Le code développé est fragile, les effets de bord sont nombreux. S'il y a un evict, c'est parce qu'il en a eu besoin à ce moment-là. Même chose pour le cascading.

Pour lui, une seule solution, tout recommencer et mieux maîtriser la succession des opérations.

Pour les autres, une règle de base: éviter le evict. C'est rarement utile.

vendredi 18 février 2011

"Si ça marche, ce n'est pas une erreur"

Un des points qui me tiennent à coeur, c'est la qualité des développements. Et une grande partie de ma fonction consiste justement à veiller à cette qualité. La question toutefois est de savoir ce qu'est la qualité d'une application.

Un incident récent suggère que pour certains, il suffit que l'application fasse ce qu'on lui demande. Je suis d'accord que c'est un point essentiel, mais il ne suffit cependant pas à attribuer un label de qualité à une application.

La preuve avec un exemple bien réel.

L'analyse quotidienne faite par Sonar révèle un problème potentiel sur une application. Voici le code :
Integer i = Integer.valueOf(0);
Integer j = Integer.valueOf(1)
//Opérations sur i et j
if(i == j){
    //la suite du code
Le problème, c'est le "==" entre deux objets. Sonar soupçonne, avec raison, que c'est l'égalité des objets qui est testée et non l'égalité des références. Le code correct est donc:
if(i.equals(j))
C'est la correction la plus directe. Néanmoins, dans le cas présent, une meilleur approche est de transformer les Integer en int car ils ne se justifient pas et entraînent une perte de performance.

L'erreur est signifiée au développeur qui hausse les épaules et répond: "pourtant, ça marche".

Le fait est que la classe Integer, pour des valeurs comprises entre -128 et 127 utilise un cache, à condition de passer par valueOf (ce qui est le cas ici, mais aussi des opérations de boxing/unboxing).
Dans ces conditions, plusieurs Integer encapsulant le même "int" compris entre ces valeurs auront la même référence. Or, dans les opérations effectuées ci-dessus avec i et j, leurs valeurs sont comprises entre 0 et environ 10. Conséquence: bien qu'incorrect d'un point de vue OO, le test fonctionne.

Le développeur se tourne vers son chef d'équipe qui rétorque: "Si ça fonctionne, je ne vois pas où est le problème. De toute façon, on n'a pas le temps (sic)."

Au-delà de l'incident, on est sans doute tenté de se poser la question "pourquoi modifier ce code puisque ça marche?".

Voici quelques réponses:
  1. parce que si les valeurs des Integer dépassent les limites du cache, le code ne fonctionnera plus
  2. parce que si les valueOf sont remplacés par des new Integer(), ça ne fonctionnera plus
  3. parce que c'est incorrect
Les objections à ces arguments sont d'intéressantes.

En ce qui concerne le premier point, le développeur fait remarquer qu'il est "peu probable" que les valeurs dépasseront 10 ou 12. De son côté, le chef d'équipe explique que, dans une approche "pragmatique", lorsque ça ne fonctionnera plus, la correction sera apportée.

Je ne peux être d'accord avec ces deux objections. "Peu probable" me semble "peu rassurant". Quant à l'approche qui consiste à réparer lorsque ça posera problème, elle risque d'entraîner une perte de temps pour retrouver où ça ne marche pas.

En ce qui concerne le deuxième point, c'est simple. Puisque nous (la cellule d'architecture) recommandons l'usage de valueOf plutôt que de new Integer(), la problème ne se présentera pas.

Quant à la troisième réponse, l'objection est simple: en quoi est-ce une erreur, puisque ça marche?

Le plus terrible dans cette histoire, c'est que la correction aurait pris, commit sur Subversion compris, une trentaine de secondes. Notre discussion a duré un quart d'heure.

Alors, c'est quoi finalement la qualité? C'est un sujet sur lequel j'aurai l'occasion de revenir.