Les contextes de validation dans Rails
Les validateurs Rails sont expressifs et puissants, tout en étant faciles à utiliser et à étendre.
Une fonctionnalité intéressante est que les validateurs peuvent s’exécuter uniquement lorsqu’une condition est remplie (en utilisant if:
et unless:
), ou dans des contextes sélectionnés, en utilisant on:
.
Mais saviez-vous que vous pouvez également définir des contextes personnalisés ? Voici comment le faire, et pourquoi cela pourrait être utile.
Les contextes de validation de base
En plus des conditions if:
et unless:
, Rails permet de limiter des validations à certains contextes seulement :
on: :create
- Validations qui ne s’exécutent qu’à la création d’un modèle. Équivalent à :if: :new_record?
.on: :update
- Validations qui ne s’exécutent qu’à la mise à jour d’un objet existant. Équivalent à :if: :persisted?
.
Ces contextes peuvent être spécifiés dans les modèles comme ceci :
class User < ApplicationRecord
validates :email, presence: true, on: :create
validates :age, numericality: { greater_than: 18 }, on: :update
end
Dans cet exemple, la présence de l’email n’est vérifiée que lors de la création d’un nouvel utilisateur, tandis que la validation de l’âge ne s’applique que lors de la mise à jour d’un utilisateur.
Les validations qui ne précisent pas l’option :on
s’exécuteront toujours, quel que soit le contexte.
Les contextes de validation personnalisés
Au-delà des contextes intégrés, Rails permet de définir des contextes personnalisés pour des scénarios plus spécifiques :
class Article < ApplicationRecord
validates :title, presence: true, on: :publish
def publish
return false unless valid?(:publish)
# logique de publication
end
end
Ici, le contexte personnalisé :publish
est utilisé pour déclencher des validations qui doivent s’exécuter uniquement avant qu’un article ne soit publié.
Si les vérifications nécessitent d’interroger la base de données par exemple, il est judicieux de ne les effectuer que lorsque c’est nécessaire.
Vous pouvez même définir plusieurs contextes en passant un tableau de symboles : on: [:create, :publish]
.
Attention : le contexte de validation est passé aux modèles associés
Une fonctionnalité non documentée est que lorsque vous utilisez des associations has_one
ou has_many
, le contexte de validation parent est automatiquement transmis aux modèles associés.
Soyez conscient que ces modèles pourraient donc recevoir un contexte que vous ne vous attendiez pas à recevoir !
Dans le cas suivant :
class Author < ApplicationRecord
has_many :books
validates :name, presence: true, on: :publish
end
class Book < ApplicationRecord
belongs_to :author
validates :title, presence: true, on: :create
end
author = Author.new(books_attributes: [{}])
author.valid?(:publish)
Dans cette situation, l’appel valid?(:publish)
sur l’auteur déclencher les validations de tous les livres associés, mais uniquement celles qui sont associées au contexte :publish
, ou qui n’ont pas d’option :on
.
Ici, la validation sur le titre ne s’effectuera pas puisqu’elle est limitée au contexte :create
. Attention aussi, il est possible de déclencher des boucles de validation infinies !
Bonnes pratiques autour des validations Rails
Valider au niveau du modèle
Il est préférable de contrôler la validité au niveau des modèles plutôt que dans les contrôleurs (skinny controllers, fat models). Cela permet notamment de s’assurer que les modèles associés effectuent les vérifications requises.
Éviter ou limiter les appels à la base de données
Pour éviter les N+1, essayez d’éviter les validations qui doivent communiquer avec la base de données, comme la vérification de la présence d’un modèle associé.
Dans l’une de mes applications par exemple, j’ai remarqué que Rails interrogeait la base de données pour vérifier l’unicité de l’email chaque fois que l’utilisateur était mis à jour, même si l’email n’avait pas changé.
J’ai pu supprimer cette requête inutile en ajoutant on: :create
puisque cette application ne permettait pas aux utilisateurs de changer leur email.
Dans un autre modèle, la validation d’unicité s’exécutait chaque fois que l’enregistrement était sauvegardé, même si l’attribut n’avait pas été modifié. Je l’ai modifié pour qu’il ne se produise que lorsque c’est nécessaire, en utilisant les dirty attributes. C’est maintenant devenu une habitude de vérifier l’unicité d’un attribut uniquement si l’attribut a changé.
validates_uniqueness_of :name, if: :name_changed?
Limiter la visibilité des méthodes de validation
Les méthodes de validation spécifiques (qui sont appelées via validate
au lieu de validates
) ont rarement besoin de faire partie de l’API publique d’un modèle, et devraient donc être privées.
Il arrive néanmoins qu’on ait besoin de les appeler de l’extérieur, pour tester une logique métier importante, ou pour afficher conditionnellement un message par exemple.
Cela devrait être évité mais il arrive qu’on soit obligé de les appeler de l’extérieur, soit parce que le code ne peut pas être modifié, ou lorsque l’ajout d’un présentateur serait excessif.
Envisager l’utilisation de POROs et de Form Objects
Les formulaires qui créent ou mettent à jour plusieurs modèles, ou associés à une logique de validation complexe sont souvent de bons candidats pour l’utilisation du pattern Form Object.
N’oubliez pas que les POROs, y compris les objets de formulaire, peuvent inclure ActiveModel::Attributes
et ActiveModel::Validations
pour réutiliser l’API de validation de Rails.
C’est une autre technique pour extraire les validations spécifiques à un seul contexte des définitions de modèles, ce qui rend le code beaucoup plus facile à lire.
Ne pas s’appuyer uniquement sur les validations
La validation par le navigateur (attribut required
des balises input
) est facile à contourner et ne devrait être utilisée que pour empêcher les utilisateurs de perdre du temps en envoyant des formulaires invalides.
De plus, un modèle peut avoir besoin d’être importé à partir d’un fichier (CSV par exemple), il est dans ce cas fréquent d’utiliser insert_all
pour minimiser la pression sur la base de données.
Il est donc important que la cohérence soit appliquée au niveau de la base de données, en utilisant des colonnes non nulles, des clés étrangères et des index uniques.
La gemme Database Consistency peut être utilisée pour vérifier les divergences entre les validations du modèle et les contraintes de la base de données.
Enfin, je vous renvoie sur l’article de Thoughtbot qui compare validation, contraintes de base de données, ou les deux (en anglais) pour connaître les avantages et les inconvénients des deux options.