Dans mon
billet précédent, j'expose mon retour d'expérience concernant Scala. Maintenant que je possède le langage (enfin quelques éléments), que me manque-t-il pour coder une application de la vraie vie? Un outil de build, une couche pour faciliter les accès JDBC et un framework web me paraissent être le minimum. Je vais me pencher sur les cas de
SBT,
Slick et
Play Framework 2.1 (what else?).
SBT
C'est l'outil de build qui me semble le plus répandu pour le développement Scala. Il est incontournable pour Play Framework (la console se base dessus). Si on regarde un peu en arrière, pour nous délivrer de javac, Apache à créé Ant. C'était bien mais trop freestyle et des configurations complexes ont été engendrées en masse. Qu'à cela ne tienne, puisque la gestion technique du projet nécessite un outil plus coercitif, Apache nous apporte une fois de plus la lumière avec Maven (on va dire que l'ampoule a été vissée à partir de Maven 2). Aujourd'hui les critiques fusent autour du manque de flexibilité. Comme tous, j'ai du faire l'investissement autour de Maven il y a quelques années, il n'est pas parfait mais je le maîtrise et il fait le taf, au prix de quelques acrobaties parfois.
Je trouve que l'outil de build est un non sujet. Que l'application soit construite avec Ant, Maven, Graddle ou des scripts shell, quelle différence pour l'utilisateur? Pire: pour le code? Devoir encore passer du temps sur SBT me paraît être une perte sèche et m'irrite un peu je l'avoue. Je me limite donc à des copier-coller de config et aux commandes compile, test et idea. Pas d'appréciation.
Slick
Anciennement ScalaQuery, Slick est un framework d'accès aux données en Scala. Il propose trois voies d'accès:
- plain SQL queries: à priori peu d'intérêt, je ne me suis peu attardé dessus
- lifted embedding: j'ai présenté les possibilités de la boucle for en Scala dans mon précédent billet et notamment le sentiment que l'on peut interroger les collections comme des tables relationnelles. Et c'est ce que ce mode propose et c'est ce qui m'a d'ailleurs attiré vers Slick:
//Domain definition
case class Supplier(id:Int,name:String)
//DAO
object Suppliers extends Table[Supplier]("SUPPLIER"){
//Column definition
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")
//Projection definition
def * = id ~ name <>(Supplier, Supplier.unapply _)
}
//Interrogation
val q = for {
s < - Suppliers
if s.name === "my supp"
} yield s.id
q.list/* have fun here with Scala lists!*/.foreach(println)
Slick propose un troisième mode, direct embedding, l'exemple va vous rappeler quelqu'un:
@table(name="COFFEES")
case class Coffee(
@column(name="NAME")
name : String,
@column(name="PRICE")
price : Double
)
Et là comme j'ai pu le faire, vous êtes en train de vous dire que c'est du JPA! Enfin ça le sera peut être un jour car la doc stipule deux bloqueurs:
The direct embedding currently only supports database columns, which can be mapped to either
String, Int, Double.
Ouch même pas les dates!
The direct embedding currently does not feature insertion of data. WTF? On peut interroger des données qu'on a pas pu insérer? Pas exactement, il faut utiliser un des deux autres modes pour persister.
Mes attentes principales autour d'un outil d'accès aux données sont de générer le SQL pour me faire rêver que le changement de SGBD sera sans impact sur le code et des fonctions de mapping relationnel-objet pour limiter le code boilerplate. Du coup des trois modes il n'en reste qu'un: lifted embedding.
Le langage de requêtage basé sur la boucle for est fort séduisant:
- il est très concis et s'intègre agréablement dans le code, après je ne l'ai pas challengé avec un schéma de 200 tables non plus...
- il est statiquement typé: pour avoir la même chose avec JPA 2.0, il faut générer un méta modèle et utiliser l'API criteria (mon royaume pour une corde et un arbre que j'aille me pendre…)
Je mène ces explorations par pure convenance personnelle et ce sont les technologies traditionnelles qui me permettent de ramener le pain quotidien, il est donc facile de deviner que ma référence en la matière est JPA. D'ailleurs dans les différences fondamentales, on notera deux points marquants:
Les entités mappées ne sont pas managées: le framework n'a aucune idée de la situation d'un objet du domaine au regard de son état en base; de plus il n'est même pas obligatoire d'associer un DAO avec une classe, il peut tout aussi bien exploiter des tuples.
Les relations ne sont donc pas portées par le modèle métier mais par les DAO, ce qui implique que la navigation dans le graphe passe nécessairement par une interrogation explicite. Imaginons l'extension de l'exemple précédent:
object Coffees extends Table[Coffee]("COFFEE"){
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")
def supplierId = column[Int]("sup_id", O.NotNull)
def * = id ~supplierId~name <>(Coffee,Coffee.unapply _)
def supplier = foreignKey("fk_supId", supplierId,Suppliers)(_.id)
}
(Suppliers.ddl ++ Coffees.ddl).create
Suppliers.insert(Supplier(1, "my supp"))
Coffees.insert(Coffee(1,1,"Ristretto"))
val q = for {
c < - Coffees
s < - c.supplier
} yield (s.name, c.name)
q.list.foreach(println)
}
De prime abord, ces points semblent être des lacunes comparés à JPA. Mais en prenant un peu de recul, lors de la plupart (hmm… peut être même la totalité?) de mes interventions sur les projets, j'ai pu observer que l'ORM était souvent hors de contrôle. Oui, "sans maîtrise la puissance n'est rien" (j'adore), et toute la magie apportée par l'instrumentation des classes, les invocations d'assesseurs interrogeant automatiquement la BDD, la fusion d'entités et j'en passe, éclipsent totalement la mécanique sous-jacente. Le résultat: des torrents de SQL noient littéralement nos bases, devant également faire face à des requêtes monstrueuses aux jointures improbables, impliquant des plans d'exécution moins accessibles que le Saint Graal lui même, et souvent pour afficher moins de colonnes que les doigts d'une main. Ne nous trompons pas, je ne dresse pas le procès de JPA, une très belle technologie, mais celui de ses utilisateurs.
Alors finalement, moins de fonctionnalités réduit la fracture entre les développeurs et la modélisation relationnelle. Slick semble parvenir à ce compromis: il concilie un niveau d'abstraction acceptable en gardant à sa charge les basses besognes, telles que la génération du SQL, avec un code qui laisse l'informaticien conscient des concepts mis en oeuvre. A méditer.
Chers internautes, je me dois maintenant d'interrompre ce séjour sur l'île aux enfants pour nous ramener dans la vraie vie, alors dites au revoir à Casimir et préparez vous à la descente.
D'abord les snippets présentés sont évidemment résumés dans un souci de lisibilité. Slick exploite le côté obscur de Scala (selon moi évidemment), les wrappers implicites. Pour que notre boucle for appliquées aux tables compile il faut importer un driver permettant d'intégrer le langage de Slick avec la source de données JDBC (à l'image du dialecte Hibernate). Ensuite, il faut ouvrir ou récupérer la session courante:
import Database.threadLocalSession
import scala.slick.driver.H2Driver.simple._
Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver") withSession {
val q = for(....
}
Admettons devoir intégrer ce code dans une application, l'URL et le driver JDBC, on peut les caser dans un fichier de conf, mais pour le driver Slick on fait quoi? On ne va pas laisser des H2Driver traîner dans toutes les classes, et puis c'est bon pour les tests ou les POC mais quand le temps viendra d'utiliser une vraie base on fera quoi? La solution se base sur le cake pattern qui modélise l'injection de dépendance statiquement typée en Scala (plus de détails sur
http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di/). Je vous laisse consulter l'exemple fourni sur
https://github.com/slick/slick-examples/blob/master/src/main/scala/com/typesafe/slick/examples/lifted/MultiDBCakeExample.scala#L61. Il présente un gâteau avec trois ingrédients: l'encapsulation du driver Slick, le compsant d'accès aux données et les DAO. Je le trouve nettement plus indigeste que:
@PersistenceContext
private EntityManager em;
Affaire de goût?
Les exemples fournis se basent uniquement sur la session, quid de la gestion transactionnelle? En parcourant la documentation de référence, je n'ai rien trouvé. Cependant j'ai réussi à dénicher qu'il fallait d'abord ouvrir une session puis regrouper toutes les commandes dans un bloc transactionnel:
Database.forURL("jdbc:h2:mem:test1", driver = "org.h2.Driver") withSession {
threadLocalSession.withTransaction{
[...]
}
}
Personne ne leur à montré @Transactionnal? Nous voilà quelques années en arrière…
Slick propose les query templates qui permettent d'exécuter une requête plusieurs fois avec des paramètres différents. Nous savons tous combien les optimiseurs SQL apprécient cette attention et nous la rendent avec des temps d'exécution diminués. Seulement les query templates ne couvrent que l'interrogation (select), ce qui implique que les manipulations (update/delete) paramétrées ne sont disponibles qu'au travers du mode plain SQL queries. Une lacune difficilement excusable, gageons qu'il s'agit d'un problème de jeunesse.
La documentation d'API n'a que le mérite d'exister, les développeurs doivent respecter une des lois XP qui précise que si des commentaires sont nécessaires c'est que le code n'est pas clair… Bref, une Scala Doc qui est tout bonnement anémique et qui daigne nous lâcher royalement une phrase de description dans quelques une des classes.
Je trouve Slick prometteur, car une fois que tous les petits défauts de jeunesse seront comblés, Scala bénéficiera d'une technologie d'accès aux données qui sera parfaitement intégrée au langage. En revanche passer de la version 0.11 à la version 1.0 (releasée il y a peu) est un peu rapide. 1.0 me paraît être un jalon dans lequel les fonctionnalités de base sont comblées, avec éventuellement quelques bogues ou une intégrabilité balbutiante après tout 1.0 est un gage de jeunesse. Seulement voilà, l'embedded mode est tout simplement inexploitable et le lifted mode incomplet. N'aurait-il pas été plus utile au projet de finaliser le deuxième avant de s'aventurer sur le premier? Mais ne jetons pas le bébé avec l'eau du bain, l'évolution de ce framework reste définitivement à surveiller, même si son numéro de version actuel devrait être 0.8 au lieu de 1.0.
M'épancher sur le cas de Play ferait un billet assez long, donc je m'arrête là et vous proposerais la suite rapidement c'est promis!