Blog tech

Sequel - Ruby et SQL

Rédigé par Nicolas Zermati | 12 juin 2012

Ruby offre bien des moyens de communiquer avec une base de données relationnelle.

Aujourd’hui, je vais vous présenter Sequel, une boite à outils permettant d’interagir avec ce type de bases via le langage SQL.

Introduction

Les programmes informatiques sont des algorithmes qui manipulent des données. Les données sont au donc au coeur des applications. Leurs sources se diversifient et leurs quantités augmentent.

Un accès efficace aux données est donc un facteur clé du bon fonctionnement d’une application. Le langage SQL est conçu pour manipuler les bases de données relationnelles.

Depuis 30 ans, des recherches fondamentales et des SGBD (Système de Gestion de Base de Données) sont réalisés dans le but de manipuler les données toujours plus efficacement. Le fruit de ce long travail réside dans un grand nombre de SGBD capables d’interpréter le SQL.

Le mapping objet-relationnel

Généralement je passe à travers un ORM comme ActiveRecord pour dialoguer avec une base de données relationnelle.

Cette technique permet une approche plus abstraite de la persistance qui facilite le développement. Même si les ORMs sont très flexibles ils ont leurs limites. Je suis souvent amené à écrire tout ou partie d’une requête à la main.

Durant les phases d’optimisation, je finis même par aller contre l’abstraction de l’ORM. J’inspecte les requêtes émises pour les rendre plus légères, moins nombreuses, etc.

Dans de telles conditions, je remets parfois en question l’intérêt d’une telle abstraction. Disposer d’une syntaxe agréable, de l’expressivité du SQL, des associations simplifiées ainsi que des crochets (hooks, callbacks) sur les modèles ne me semble pas surréaliste.

L’existence de projets comme ARel : un générateur de SQL, témoigne des lacunes de trop d’ORMs.

Sequel est différent

À mes yeux Sequel est le croisement d’un ORM comme DataMapper et d’ARel (bien qu’ARel soit plus récent). Je le trouve simple et complet.

Bien avant ARel, Sequel a introduit le chainage des méthodes pour construire des requêtes. Il en résulte un DSL expressif qui permet d’avoir un contrôle total sur le SQL généré.

Le coeur de la bibliothèque est le Dataset, il correspond à un ensemble de ligne dans la base. Par-dessus cette notion de Dataset, Sequel fournit une couche d’abstraction contenant validations, associations, crochets et plugins.

J’ai aussi remarqué qu’il y a quelques mois Matt Aimonetti a publié un article comparant l’initialisation des objets dans les ORMs. Sequel semble être celui dont l’instanciation est la plus légère.

Quelques exemples

Faire un simple OR

Pour arriver à cette requête plutôt simple :

  SELECT * FROM `posts` WHERE ((`title` = 'Post title') OR (`body` LIKE '%content%'))

ActiveRecord ne possède pas de OR, j’écris le SQL à la main :

  Post.where("(title = ?) OR (body LIKE ?)", "Post title", "%content%")

Sequel offre un moyen de construire des expressions SQL :

  Post.filter{{title => "Post title"} | body.like('%content%'))
  Post.filter(:title => "Post title").or(:body.like('%content%'))

Je rencontre souvent cette limitation dans l’écriture des contraintes.

Avec des outils comme Squeel on peut écrire en un peu plus succinct :

  Post.where{(title == "Post title") | (body =~ "%content%")}

Ne pas utiliser de modèle

DataMapper, ActiveRecord ou Squeel se focalisent sur le modèle, pas d’interaction avec la base sans lui ! Si je dois utiliser des données d’une source étrangère alors je devrais utiliser un modèle associé.

Le concept de Dataset de Sequel dépasse la notion d’objet :

  require 'sequel'
  DB = Sequel.connect('sqlite://tmp/development.sqlite', :encoding => 'utf-8')
  DB[:posts]        # #<Sequel::SQLite::Dataset: "SELECT * FROM `posts`">
  DB[:posts].all    # [{:id=>1, :title=>"Post title", :body=>"Post content"}]

Dans cet exemple je récupère les colonnes sous forme d’un Array contenant des Hash. Ainsi, si je dois importer des informations depuis une autre base, je ne suis pas contraint d’avoir un modèle. Idem lorsque je développe un petit script.

Ce point démontre bien que le spectre d’utilisation de Sequel ne se limite pas qu’à l’ORM.

Sequel offre une API de réflexion intéressante. On peut l’utiliser pour consulter un schéma existant :

  DB.tables
  # => [:migrations, :posts]

  DB.schema(:posts)
  # => [[:id,    {:allow_null=>true, :default=>nil, :primary_key=>true,  :db_type=>"integer", :type=>:integer, :ruby_default=>nil}],
  #     [:title, {:allow_null=>true, :default=>nil, :primary_key=>false, :db_type=>"string",  :type=>nil,      :ruby_default=>nil}],
  #     [:body,  {:allow_null=>true, :default=>nil, :primary_key=>false, :db_type=>"string",  :type=>nil,      :ruby_default=>nil}]]

Sous-requêtes

Certains pensent que les sous-requêtes ne sont pas aussi efficaces que les jointures. Je crois que la question n’est pas bonne. Je compte sur le SGBD pour optimiser les cas où les sous-requêtes peuvent s’exprimer sous forme de jointures.

Sequel permet d’écrire ceci :

  posts = Post.filter{updated_at > (Date.today - 1.week)}.select(:id)
  Comment.filter(:post_id => posts)
  # => "SELECT * FROM `comments` WHERE (`post_id` IN (SELECT `id` FROM `posts` WHERE (`updated_at` > '2012-06-04')))"

Sur cet exemple, on peut admettre qu’il relève du SGBD d’optimiser cette requête.

ActiveRecord ne permet pas d’écrire facilement ce genre de code ; Sequel, ARel et Squeel si.

Dans ce cas précis, j’écrirais manuellement la jointure avec AR :

  Comment.joins(:posts).where("posts.updated_at > ?", Date.today - 1.week)

Sequel permet une écriture encore plus rapide et lisible :

  Post.filter{updated_at > Date.today - 1.week}.comments

Ce dernier exemple utilise le plugin DatasetAssociations et génère une sous-requête.

Requêtes préparées

Sequel supporte les requêtes préparées. Il s’agit de donner à la base de données un modèle de requête qui va être réutilisé par la suite. Pour les futures requêtes, on évite d’envoyer la requête entière, mais on envoie simplement l’argument utilisé.

Aaron Patterson explique son récent ajout dans Rails dans une présentation de la RailsConf 2011.

Voilà comment s’en servir avec Sequel.

  ps = Post.filter{title.like(:$p)).prepare(:select, :posts_by_title)
  # => PREPARE posts_by_title: SELECT * FROM `posts` WHERE (`title` LIKE :p)
  ps.call(:p => "Post%") # == DB.call(:posts_by_title, :p => "Post%")
  # => EXECUTE posts_by_title; {"p"=>"Post%"}

C’est un peu moins magique qu’avec AR.

(Sinatra|Rails) + Sequel = ♥

Sequel est tout aussi facile à mettre en place dans une application que ses pairs.

Voilà une application Sinatra qui liste les éléments présents dans la table tbl_posts d’un base de données sqlite db/demo.sqlite.

  # Gemfile content

  source :rubygems

  gem 'sequel'
  gem 'sqlite3'
  gem 'sinatra'
  gem 'haml'
  # app.rb

  $LOAD_PATH.unshift('.')

  require 'rubygems'
  require 'sinatra'
  require 'sequel'
  require 'sqlite3'

  Sequel.connect('sqlite://db/demo.sqlite', :encoding => 'utf-8')

  Dir["models/**/*.rb"].each do |model|
    require model
  end

  get "/posts" do
    @posts = Post.all
    haml :posts
  end
  # models/post.rb

  class Post < Sequel::Model(:tbl_posts)
  end

Avec Rails, on peut :

La gem citée ci-dessus modifie les taches rake associées à la base de données et les générateurs. Elle tire profit du système de migrations intégré dans Sequel sans pour autant l’imposer. Attention à bien faire disparaitre les traces d’ActiveRecord dans les fichiers application.rb et les fichiers d’environnement.

Plugins

Sequel utilise un système de plugins pour enrichir son ORM. Pour la plupart, ils font partie de la bibliothèque et reçoivent le même niveau de support.

Le système de plugins permet de centraliser certains aspects redondants de ses modèles et de le réutiliser par la suite.

Ces plugins ajoutent des fonctionnalités phares comme :

Écrire un plugin Sequel est assez facile puisque les plugins existants sont assez nombreux. On comprend très vite en survolant le code source Sequel::Model comment le système de plugin fonctionne.

Pourquoi ne pas parler du NoSQL ?

Les bases de données orientées colonnes (Bigtable), orientées documents (CouchDB) ou le stockage clé/valeur (Redis) ne sont simplement pas le sujet de l’article.

Chaque innovation répond a un besoin bien spécifique. Ce besoin n’est pas forcément celui de tout le monde, aussi intéressant soit-il. SQL a toujours sa place dans bien des situations. Les bases relationnelles sont moins à la mode, mais il ne faut pas les oublier pour autant.

Conclusion

La documentation de Sequel est très complète, elle se base parfois sur les guides de Rails. Le projet a 5 ans d’âge et est toujours très actif. C’est, pour moi, une alternative valable à ActiveRecord ou à DataMapper.

L’équipe Synbioz.

Libres d’être ensemble.