Nicolas Cavigneaux
04 07 2012

ActiveRecord c'est aussi ARel !

posté par dans les catégories ruby, rails

Dans un article précédant, Nicolas vous présentait Sequel et vous démontrait ses possibilités dans le domaine de l’intéraction avec les SGBD.

Aujourd’hui place à ARel qui est une librairie gérant toute l’abstraction bas niveau SQL. L’idée d’ARel est de fournir une première couche d’abstraction permettant d’écrire son propre ORM en se concentrant sur le développement des fonctionnalitées spécifiques et novatrices de celui-ci.

ActiveRecord repose entièrement sur ARel pour ses fondements. Il est donc important de connaître ARel si vous voulez maîtriser pleinement ActiveRecord.

ARel dans ActiveRecord ? Où ça ?

Prenons l’exemple d’une table “products” classique, contenant nom, description, prix, etc. Dans votre code un Product.columns fera appel à ARel pour récupérer les infos concernant les colonnes. En fait, l’objet Arel::Table est lui même stocké dans l’objet ActiveRecord :

>> Product.arel_table
=> #<Arel::Table:0x007fbff55887b8 @name="products", @engine=Product(id: integer, brand_id: integer, name: string, created_at: datetime, updated_at: datetime), @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>

>> Product.arel_engine
=> Product(id: integer, brand_id: integer, name: string, created_at: datetime, updated_at: datetime)

Cet objet Arel::Table peut nous servir directement pour générer du SQL qui sera utilisé par ActiveRecord. Nous pouvons récupérer l’objet Arel::Table depuis ActiveRecord comme au dessus ou l’initialiser nous même :

>> t = Arel::Table.new(:products)
=> #<Arel::Table:0x007fbff5855fe0 @name="products", @engine=ActiveRecord::Base, @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>

Sur cette table, nous pourrons récupérer les colonnes ainsi que leurs propriétés (nom, relation, …)

>> t.columns
=> [#<struct Arel::Attributes::Integer relation=#<Arel::Table:0x007fbff5e82a80 @name="products", @engine=ActiveRecord::Base, @columns=[...], @aliases=[], @table_alias=nil, @primary_key=nil>, name=:id>, #<struct Arel::Attributes::Integer relation=#<Arel::Table:0x007fbff5e82a80 @name="products", @engine=ActiveRecord::Base, @columns=[...], @aliases=[], @table_alias=nil, @primary_key=nil>, name=:brand_id>, #<struct Arel::Attributes::String relation=#<Arel::Table:0x007fbff5e83a80 @name="products", @engine=ActiveRecord::Base, @columns=[...], @aliases=[], @table_alias=nil, @primary_key=nil>, name=:name>, #<struct Arel::Attributes::Time relation=#<Arel::Table:0x007fbff5e82a80 @name="products", @engine=ActiveRecord::Base, @columns=[...], @aliases=[], @table_alias=nil, @primary_key=nil>, name=:created_at>, #<struct Arel::Attributes::Time relation=#<Arel::Table:0x007fbff5e82a80 @name="products", @engine=ActiveRecord::Base, @columns=[...], @aliases=[], @table_alias=nil, @primary_key=nil>, name=:updated_at>]

>> t.columns.map &:name
=> [:id, :brand_id, :created_at, :updated_at, :shape, :slug, :kind_id, :oxylane_kind]

Génération de conditions

Tout l’intérêt d’ARel réside dans sa capacité à générer du SQL pour vous et de manière agnostique. Plus besoin de gérer les “LIKE” / “ILIKE” en fonction du SGBD, vos conditions sont portables.

>> t[:name].eq("iPhone").class
=> Arel::Nodes::Equality

>> t[:name].eq("iPhone").to_sql
=> "`products`.`name` = 'iPhone'"

>> t[:email].matches("%iPhone%").class
=> Arel::Nodes::Matches

>> t[:email].matches("%iPhone%").to_sql
=> "`products`.`email` LIKE '%iPhone%'"

>> t.where(t[:name].eq("iPhone")).class
=> Arel::SelectManager

>> t.where(t[:name].eq("iPhone")).to_sql
=> "SELECT FROM `products`  WHERE `products`.`name` = 'iPhone'"

>> t.where(t[:name].eq("iPhone")).project(t[:name], t[:id]).to_sql
=> "SELECT `products`.`name`, `products`.`id` FROM `products`  WHERE `products`.`name` = 'iPhone'"

On peut donc, en Ruby, générer sa requête SQL. Il est biensûr possible de créer des conditions bien plus complexes, de les chaîner, …

Chaînage de conditions

>> t.where(t[:name].eq('Test')).where(t[:price].lt(100)).class
=> Arel::SelectManager

>> t.where(t[:name].eq('Test')).where(t[:price].lt(100)).to_sql
=> "SELECT FROM `products`  WHERE `products`.`name` = 'Test' AND `products`.`price` < 100"

Utilisation de OR

>> t.where(t[:name].eq('Test').or(t[:price].lt(100))).class
=> Arel::SelectManager

>> t.where(t[:name].eq('Test').or(t[:price].lt(100))).to_sql
=> "SELECT FROM `products`  WHERE (`products`.`name` = 'Test' OR `products`.`price` < 100)"

Utilisation explicite de AND

>> t.where(t[:name].eq('Test').and(t[:price].lt(100))).class
=> Arel::SelectManager

>> t.where(t[:name].eq('Test').and(t[:price].lt(100))).to_sql
=> "SELECT FROM `products`  WHERE (`products`.`name` = 'Test' AND `products`.`price` < 100)"

Création d’une jointure

>> photos = Arel::Table.new(:photos)
=> #<Arel::Table:0x007fbff5cd8090 @name="photos", @engine=ActiveRecord::Base, @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>

>> t.join(photos).on(t[:id].eq(photos[:product_id])).class
=> Arel::SelectManager

>> t.join(photos).on(t[:id].eq(photos[:product_id])).to_sql
=> "SELECT FROM `products` INNER JOIN `photos` ON `products`.`id` = `photos`.`product_id`"

Création de groupes

>> t.group(:price).class
=> Arel::SelectManager

>> t.group(:price).to_sql
=> "SELECT FROM `products`  GROUP BY price"

Intervalles

>> t.take(5).skip(10).class
=> Arel::SelectManager

>> t.take(5).skip(10).to_sql
=> "SELECT  FROM `products`  LIMIT 5 OFFSET 10"

Vous pouvez tout chainer à loisir, créer des méthodes pour vos outils de recherche. ARel ne présente pas de réelle limitation et permet d’aller beaucoup plus loin qu’ActiveRecord qui limite volontairement son interface publique.

Nous allons d’ailleurs maintenant voir la liste de prédicats que nous offre ARel pour étoffer nos requêtes.

Prédicats

Vous pourrez retrouver l’ensemble des prédicats directement dans le code source sur GitHub mais je vais tout de même prendre le temps de vous les lister :

Égalité

>> t[:name].eq("iPhone").class
=> Arel::Nodes::Equality

>> t[:name].eq("iPhone").to_sql
=> "`products`.`name` = 'iPhone'"

>> t[:name].not_eq("iPhone").class
=> Arel::Nodes::NotEqual

>> t[:name].not_eq("iPhone").to_sql
=> "`products`.`name` != 'iPhone'"

>> t[:name].eq_any(["iPhone", "BlackBerry"]).class
=> Arel::Nodes::Grouping

>> t[:name].eq_any(["iPhone", "BlackBerry"]).to_sql
=> "(`products`.`name` = 'iPhone' OR `products`.`name` = 'BlackBerry')"

>> t[:name].not_eq_any(["iPhone", "BlackBerry"]).class
=> Arel::Nodes::Grouping

>> t[:name].not_eq_any(["iPhone", "BlackBerry"]).to_sql
=> "(`products`.`name` != 'iPhone' OR `products`.`name` != 'BlackBerry')"

>> t[:name].eq_all(["iPhone", "BlackBerry"]).class
=> Arel::Nodes::Grouping

>> t[:name].eq_all(["iPhone", "BlackBerry"]).to_sql
=> "(`products`.`name` = 'iPhone' AND `products`.`name` = 'BlackBerry')"

>> t[:name].not_eq_all(["iPhone", "BlackBerry"]).class
=> Arel::Nodes::Grouping

>> t[:name].not_eq_all(["iPhone", "BlackBerry"]).to_sql
=> "(`products`.`name` != 'iPhone' AND `products`.`name` != 'BlackBerry')"

Appartenance

>> t[:name].in(["iPhone", "BlackBerry"]).class
=> Arel::Nodes::In

>> t[:name].in(["iPhone", "BlackBerry"]).to_sql
=> "`products`.`name` IN ('iPhone', 'BlackBerry')"

>> t[:name].not_in(["iPhone", "BlackBerry"]).class
=> Arel::Nodes::NotIn

>> t[:name].not_in(["iPhone", "BlackBerry"]).to_sql
=> "`products`.`name` NOT IN ('iPhone', 'BlackBerry')"

>> t[:name].in_any(["iPhone", "BlackBerry"]).class
=> Arel::Nodes::Grouping

>> t[:name].in_any(["iPhone", "BlackBerry"]).to_sql
=> "(`products`.`name` IN ('iPhone') OR `products`.`name` IN ('BlackBerry'))"

>> t[:name].not_in_any(["iPhone", "BlackBerry"]).class
=> Arel::Nodes::Grouping

>> t[:name].not_in_any(["iPhone", "BlackBerry"]).to_sql
=> "(`products`.`name` NOT IN ('iPhone') OR `products`.`name` NOT IN ('BlackBerry'))"

>> t[:name].in_all(["iPhone", "BlackBerry"]).class
=> Arel::Nodes::Grouping

>> t[:name].in_all(["iPhone", "BlackBerry"]).to_sql
=> "(`products`.`name` IN ('iPhone') AND `products`.`name` IN ('BlackBerry'))"

>> t[:name].not_in_all(["iPhone", "BlackBerry"]).class
=> Arel::Nodes::Grouping

>> t[:name].not_in_all(["iPhone", "BlackBerry"]).to_sql
=> "(`products`.`name` NOT IN ('iPhone') AND `products`.`name` NOT IN ('BlackBerry'))"

Concordances

>> t[:name].matches("%Foo%").class
=> Arel::Nodes::Matches

>> t[:name].matches("%Foo%").to_sql
=> "`products`.`name` LIKE '%Foo%'"

>> t[:name].does_not_match("%Foo%").class
=> Arel::Nodes::DoesNotMatch

>> t[:name].does_not_match("%Foo%").to_sql
=> "`products`.`name` NOT LIKE '%Foo%'"

>> t[:name].matches_any(["%Foo%", "%Bar%"]).class
=> Arel::Nodes::Grouping

>> t[:name].matches_any(["%Foo%", "%Bar%"]).to_sql
=> "(`products`.`name` LIKE '%Foo%' OR `products`.`name` LIKE '%Bar%')"

>> t[:name].does_not_match_any(["%Foo%", "%Bar%"]).class
=> Arel::Nodes::Grouping

>> t[:name].does_not_match_any(["%Foo%", "%Bar%"]).to_sql
=> "(`products`.`name` NOT LIKE '%Foo%' OR `products`.`name` NOT LIKE '%Bar%')"

>> t[:name].matches_all(["%Foo%", "%Bar%"]).class
=> Arel::Nodes::Grouping

>> t[:name].matches_all(["%Foo%", "%Bar%"]).to_sql
=> "(`products`.`name` LIKE '%Foo%' AND `products`.`name` LIKE '%Bar%')"

>> t[:name].does_not_match_all(["%Foo%", "%Bar%"]).class
=> Arel::Nodes::Grouping

>> t[:name].does_not_match_all(["%Foo%", "%Bar%"]).to_sql
=> "(`products`.`name` NOT LIKE '%Foo%' AND `products`.`name` NOT LIKE '%Bar%')"

Comparaisons numériques

>> t[:price].gt(100).class
=> Arel::Nodes::GreaterThan

>> t[:price].gt(100).to_sql
=> "`products`.`price` > 100"

>> t[:price].lt(100).class
=> Arel::Nodes::LessThan

>> t[:price].lt(100).to_sql
=> "`products`.`price` < 100"

>> t[:price].gteq(100).class
=> Arel::Nodes::GreaterThanOrEqual

>> t[:price].gteq(100).to_sql
=> "`products`.`price` >= 100"

>> t[:price].lteq(100).class
=> Arel::Nodes::LessThanOrEqual

>> t[:price].lteq(100).to_sql
=> "`products`.`price` <= 100"

>> t[:price].gt_any([100, 120]).class
=> Arel::Nodes::Grouping

>> t[:price].gt_any([100, 120]).to_sql
=> "(`products`.`price` > 100 OR `products`.`price` > 120)"

>> t[:price].lt_any([100, 120]).class
=> Arel::Nodes::Grouping

>> t[:price].lt_any([100, 120]).to_sql
=> "(`products`.`price` < 100 OR `products`.`price` < 120)"

>> t[:price].gteq_any([100, 120]).class
=> Arel::Nodes::Grouping

>> t[:price].gteq_any([100, 120]).to_sql
=> "(`products`.`price` >= 100 OR `products`.`price` >= 120)"

>> t[:price].lteq_any([100, 120]).class
=> Arel::Nodes::Grouping

>> t[:price].lteq_any([100, 120]).to_sql
=> "(`products`.`price` <= 100 OR `products`.`price` <= 120)"

>> t[:price].gt_all([100, 120]).class
=> Arel::Nodes::Grouping

>> t[:price].gt_all([100, 120]).to_sql
=> "(`products`.`price` > 100 AND `products`.`price` > 120)"

>> t[:price].lt_all([100, 120]).class
=> Arel::Nodes::Grouping

>> t[:price].lt_all([100, 120]).to_sql
=> "(`products`.`price` < 100 AND `products`.`price` < 120)"

>> t[:price].gteq_all([100, 120]).class
=> Arel::Nodes::Grouping

>> t[:price].gteq_all([100, 120]).to_sql
=> "(`products`.`price` >= 100 AND `products`.`price` >= 120)"

>> t[:price].lteq_all([100, 120]).class
=> Arel::Nodes::Grouping

>> t[:price].lteq_all([100, 120]).to_sql
=> "(`products`.`price` <= 100 AND `products`.`price` <= 120)"

Il est donc facile d’entrevoir toutes les possibilités qu’offre ARel pour générer du SQL complexe sans avoir à l’écrire soit même.

On pourrait aussi imaginer étendre toutes les colonnes avec ces prédicats (via method_missing par exemple). On aurait ainsi accès à des “scopes” dans tous les modèles héritants de ActiveRecord::Base tels que name_matches, price_lte, category_ids_matches_all, …

Le mot de la fin

Pour conclure pensez à passer par ARel pour écrire vos requêtes complexes plûtot que de les écrire à la main. Votre code sera plus lisible et maintenable et en vous aurez en bonus une compatibilité assurée entre tous le SGBD !

Si vous souhaitez créer votre propre ORM, ARel vous fournira déjà tout le nécessaire pour la connexion aux différents SGBD disponibles mais aussi tous les outils de base pour générer des requêtes SQL complexes.

Vous en priver reviendrait à vous auto-flageller !

L’équipe Synbioz.

Libres d’être ensemble.

Articles connexes

12 06 2012

Sequel - Ruby et SQL

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 l…

Lire la suite

Commentaires (1) Flux RSS des commentaires

  • Pierre

    04/07/2012 à 13:43

    Excellent article !
    Je connaissais l'existence sous-jacente de ARel mais n'avait jamais eu le temps de me plonger plus en profondeur sur son usage. Je vais garder ça dans un bookmark pour le jour où j'en ai besoin.

    A noter également l'existence de l'excellente gem Squeel (https://github.com/ernie/squeel) et son ancêtre Rails < 3.1 Metawhere qui offre une DSL très agréable pour les requêtes compliquées.

Ajouter un commentaire

Notre expérience vous intéresse ? Inscrivez-vous à nos articles !

×

Newsletter

Rejoignez-nous !

Poursuivons la conversation

N° Vert
0 805 69 35 35

Nos dernières nouvelles

Nos derniers tweets

#hackademy Et vous, quels sujets aimeriez vous voir traités en vidéo ?

Pour échanger autour de nos vidéos, rendez vous directement sur notre chaine Youtube https://t.co/YHSF65VEqI

Hackademy : Découvrez en vidéo la mise en place et l'impact d'index composites avec #postgresql http://t.co/jdCuDzma66