Skip to main content

@mdarcemont

Apprenez Scala grâce à Slick !

J’ai longtemps cru que Scala ne me deviendrait jamais familier.

Pourtant, j’ai occupé de nombreuses soirées durant presque trois ans à lire plusieurs livres et dizaines d’articles de blog sur le langage, à voir des vidéos, assister à des conférences, et bien évidemment développer, de petits programmes dans un premier temps puis un gros projet qui m’occupe toujours aujourd’hui et qui devrait enfin voir le jour d’ici quelques semaines (j’aurai l’occasion d’en reparler ici). Je sentais durant tout ce temps mon niveau nettement s’améliorer, ma productivité s’accroître … mais le langage me paraissait toujours distant, comme une vieille connaissance qu’on cotoie depuis des années mais qui garde toujours sa part d’étrangeté, avec qui on peine toujours à trouver les mots justes pour dialoguer. La folle profondeur de Scala me donnait l’impression d’être face à un mur que je ne réussirais à franchir pour de bon qu’avec un déclic. Et ce déclic fut Slick.

Jusqu’à ce jour, l’essentiel de mon code Scala concernait Play! 2.


J’ai passé des centaines et centaines d’heures sur le framework et je pense le connaître raisonnablement bien aujourd’hui. J’ai appris à gérer le JSON, à utiliser les Future, les jobs Akka, les templates, etc. Mais qu’en est-il de la richesse du code Scala écrit ?

Du fait de la nature du site, qui est une webapp finalement assez simple, je n’ai hélas pas souvent eu l’occasion d’exploiter la richesse du langage. D’un côté, je prenais connaissance dans le livre d’Odersky des possibilités innombrables concernant le langage, de l’autre je devais me contenter d’Option, de map, de match, et autres trivialités (trivialités tout de même bienvenues lorsqu’on vient de Java). La base de données derrière le site a longtemps été MongoDB. Je gérais les accès avec le framework Salat, sur lequel j’ai un avis mitigé aujourd’hui. Il m’a été très utile dans certains cas mais globalement je trouve qu’il ajoute beaucoup trop de complexité inutile. Si c’était à refaire, j’opterais pour ReactiveMongo. Bref, tout tournait bien ainsi jusqu’à ce que le modèle de données se complexifie et que la nécessité de faire de vraies relations entre les collections apparaisse. Pour cela, le plus sage était de revenir à ce bon vieux SQL. Et donc d’apprendre à comment gérer au mieux les accès à une base de données relationnelle en Scala.

A la découverte des bibliothèques facilitant l’accès SGBD en Scala


Je tenais à faire les choses bien, mais surtout vite. Des années d’expérience sur de gros projets Java m’ont appris à me méfier des ORM type Hibernate. Je reconnais évidemment leur utilité dans certain cas, mais j’ai souvent trouvé leur utilisation trop systématique, évacuant trop rapidement des solutions plus légères et convenant mieux dans certains cas comme JDBCTemplate par exemple. J’ai donc été agréablement surpris de voir comme solution par défaut dans Play! Anorm, qu’on peut considérer comme une surcouche de JDBC. C’est clair, robuste, et ça demande d’écrire peu de code boilerplate. C’est la solution idéale dans le cas de petits projets. Mais dans mon cas, entités complexes oblige, je souhaitas avoir un peu plus. Je me suis donc tourné vers l’autre framework majeur, à savoir Slick, autrefois ScalaQuery.

Slick n’est pas la bibliothèque par défaut dans Play! mais je suppose qu’il pourrait le devenir à terme dans le sens où il fait partie de la stack TypeSafe. Afin de bien intégrer Slick à mon projet, j’ai choisi d’utiliser le framework play-slick, qui fait de Slick un first-citizen de Play! en l’intégrant à son coeur, en lui faisant profiter de ce qu’il apporte au niveau de la gestion des session, des connexions, etc, et en le liant aux “Models” définis. Ce plugin est très peu intrusif et apporte beaucoup tout en laissant Slick libre. Une fois configuré on peut donc se contenter de la documentation Slick pour développer, le code Slick en tant que tel n’est pas influencé.

Les mains dans le moteur


Sur ce, venons-en à Slick avec un exemple simple. Imaginons un blog multi-auteurs très sommaire, avec deux entités seulement : Author et Article. Un Author est défini par son id (author_id - clé primaire), son nom (last_name) et son prénom (first_name) Un Article est défini par son id (article_id - clé primaire), son titre (title), son contenu (content) et son auteur (author_id - clé étrangère).

Le mapping objet/table en tant que tel est très simple et classique.

Commençons par l’objet Author.

{% highlight java %} case class Author( id: Option[Int] = None, lastName: String, firstName: String )

class Authors(tag: Tag) extends TableAuthor {

def id = column[Int]("author_id", O.PrimaryKey, 0.AutoInc)

def lastName = column[String]("last_name")

def firstName = column[String]("first_name")

def * = (
    id.?,
    lastName,
    firstName) <> (Author.tupled, Author.unapply _
)

}

val authors = TableQuery[Authors]

{% endhighlight %}

Quelques remarques à propos de ce code :

  • J’ai choisi directement de mapper mon modèle (défini par une case class) à la table (“extends Table[Author]")

  • L’attribut “id” d’Author est défini comme Option en vue des futurs inserts étant donné que sa valeur n’est pas connue à l’avance, du fait de sa nature de champ incrémenté (cf. l’attribut “0.AutoInc”). Ce champ n’étant évidemment pas optionnel en base de donnée, il ne devait pas être déclaré comme tel, d’où le def id = column[Int] et non def id = column[Option[Int]]. C’est au niveau de la projection que le mapping “not null => option” se fait, avec l’ajout de l’attribut “.?” au champ id (“id.?")

L’objet Article suit une logique similaire.

{% highlight java %} case class Article( id: Option[Int] = None, title: String, content: String, date: Date, authorId: Int )

class Article(tag: Tag) extends TableArticle {

def id = column[Int]("article_id", O.PrimaryKey, 0.AutoInc)

def title = column[String]("title")

def content = column[String]("content")

def date = column[Date]("date")

def authorId = column[Int]("authorId")

def author = foreignKey("authorFk", authorId, Authors.Authors)(_.id)

def * = (
    id.?,
    title,
    content,
    date,
    authorId) <> (Article.tupled, Article.unapply _
    )

}

val articles = TableQuery[Articles] {% endhighlight %}

La nouveauté ici réside dans la présence d’une clé étrangère sur la classe Author. Vous noterez que dans la définition d’Article, seul un attribut AuthorId de type Int est présent. Il n’y a pas directement d’attribut de type Author. C’est en effet quelque chose de déconseillé par Slick qui recommande plutôt de résoudre ce mapping directement dans les requêtes (nous verrons plus tard comment). C’est un point qui m’a surpris au début, étant habitué au mapping objet total proposé par Hibernate.

Venons en maintenant à l’écriture des requêtes proprement dites, que je choisis de placer dans un objet compagnon (“object Articles”, “object Authors”). Commençons simplement ; comment chercher un article par son ID ?

{% highlight java %} def findById(id: Long) = Articles.where(_.id === id) {% endhighlight %}

Si on n’est pas sûr d’obtenir un résultat (ce qui est plus sage !) :

{% highlight java %} def findById(id: Long) = Articles.where(_.id === id).firstOption {% endhighlight %}

Si souhaite plusieurs résultats : {% highlight java %} def findByFirstName(firstName: String) = Authors.where(_.firstName === firstName).list {% endhighlight %}

C’est simplissime et ça fait pourtant tout ce qu’on lui demande, sans pourtant s’être prémuni d’une machine de guerre. Pour rappel, avec la Criteria API de JPA, ça donnerait :

{% highlight java %} CriteriaQuery q = cb.createQuery(Author.class); Root author = q.from(Author.class); q.where(cb.equal(author.get(Author_.firstName), firstName)); {% endhighlight %}

Mais ces cas-là ne sont pas je vous l’accorde forcément intéressants. Comment ajouter un ordre de tri par date à la liste d’Articles retournés ?

{% highlight java %} def findById(id: Long) = Articles.where(.id === id).sortBy(.date).list {% endhighlight %}

Avec la Criteria API : {% highlight java %} CriteriaQuery

q = cb.createQuery(Article.class); Root
article = q.from(Article.class); q.select(article); q.orderBy(q.desc(article.get(Article_.date))); {% endhighlight %}

Et maintenant, comment retourner la liste de tous les titres articles, avec pour chaque le nom de l’auteur associé ? {% highlight java %} val join = for { art <- articles aut <- authors if art.authorId === aut.id } yield (art.title, aut.lastName) {% endhighlight %}

Avec la Criteria API de JPA, cela donnerait : {% highlight java %} CriteriaQuery

cq = cb.createQuery(Article.class); Root
article = q.from(Article.class); Join<Article, Author> author = article.join(Article_.authorId); {% endhighlight %}

En vrac, différentes actions possibles autour de l’opération SELECT :

Union : {% highlight java %} val q1 = Authors.where(.firstName === “Jack”) val q2 = Authors.filter(.firstName === “John”) val union = q1 union q2 {% endhighlight %}

Count : {% highlight java %} Articles.length {% endhighlight %}

Limit / Offset : {% highlight java %} Articles.drop(5).take(5) {% endhighlight %}

Enfin, les opérations INSERT, UPDATE et DELETE : {% highlight java %} def save(author: Author)(implicit s: Session) = { Authors returning Authors.map(_.id) += author } {% endhighlight %}

{% highlight java %} def update(author: Author)(implicit s: Session) = { Authors.where(_.id === author.id).update(Author) } {% endhighlight %}

{% highlight java %} def delete(author: Author)(implicit s: Session) = { Authors.where(_.id === author.id).delete } {% endhighlight %}

L’expressivité de Scala saute ici aux yeux. On obtient ici une syntaxe proche du SQL, et jamais l’implémentation n’est plus complexe que le problème réglé. On n’a pas non plus l’impression d’être face à un super-Java, ou à un dérivé de Java, mais bel et bien à un langage unique, affranchi de son vieux père. C’est pour cela que son utilisation est si plaisante : elle permet vraiment de jouer avec les possibilités du langage, de faire ce qu’on n’aurait jamais pu faire en restant sur Java. Elle permet de mettre en pratique pour de bon toutes les belles choses vendues par Odersky et co dans leurs livres (case class, options, implicits, API collections, etc.), et de joyeusement exploser son taux de diabète en abusant du sucre syntaxique dont regorge le langage.

Certains développeurs confirmés Scala trouveront peut-être ma réaction exagérement positive, d’autres pointeront peut-être du doigt les défauts de Slick, mais je tiens à préciser que je n’ai pas écrit cet article pour évoquer de fond en comble Slick, mais pour exprimer ma reconnaissance à son égard pour avoir élargi mon champ de vision sur Scala et m’avoir donné les armes nécessaires pour aller plus loin. C’est tout simplement le seul framework “majeur” de l’écosystème Scala qui m’a donné la sensation d’écrire du vrai code Scala idiomatique. Bref, si vous êtes un débutant sur le langage, ou si vous vous sentez toujours hésitant lorsqu’il s’agit d’écrire du code, lâchez Redis ou Cassandra quelques heures et apprenez Slick !

Addendum :


Toujours plus intéressé par Clojure, j’ai récemment eu l’occasion d’utiliser Korma ( http://sqlkorma.com/ ). Je pourrais lui faire les mêmes éloges que ceux adressés à Slick, en plus fort encore, du fait de l’homoiconicité de Clojure et de sa philosophie “code is data”. Les requêtes SQL sont ici exprimées avec des vectors et des maps, à savoir les structures de base de Clojure. Le boilerplate est minimal, pour ne pas dire inexistant.

{% highlight clojure %} (def query (-> (select* “user”) (fields :id :username) (where {:email [like “*@gmail.com”]}))) {% endhighlight %}


Il y a deux DSL SQL que je retrouve régulièrement, jOOQ et QueryDSL, mais j’avoue ne les avoir jamais utilisés sur de vrais projets. Tous deux sont essentiellement utilisés en support d’Hibernate et de JPA et cherchent à remplacer la Criteria API (contrairement à Slick qui remplace tout)

QueryDSL : {% highlight java %} List persons = query.from(person) .where( person.firstName.eq(“John”), person.lastName.eq(“Doe”)) .orderBy(person.lastName) .list(person); {% endhighlight %}

jOOQ : {% highlight java %} create.selectFrom(PERSON) .where(PERSON.FIRST_NAME.eq(“John)) .and(PERSON.LAST_NAME.eq(“Doe”)) .orderBy(PERSON.LAST_NAME) {% endhighlight %}