1 Le Cycle de Vie de l'Objet
Pendant le fonctionnement normal d'une application Rails, des objets peuvent être créés, mis à jour et détruits. Active Record fournit des points d'accroche dans ce cycle de vie de l'objet afin que vous puissiez contrôler votre application et ses données.
Les rappels vous permettent de déclencher une logique avant ou après une modification de l'état d'un objet.
class Baby < ApplicationRecord
after_create -> { puts "Félicitations !" }
end
irb> @baby = Baby.create
Félicitations !
Comme vous le verrez, il existe de nombreux événements du cycle de vie et vous pouvez choisir de vous accrocher à l'un d'entre eux, avant, après, voire pendant.
2 Aperçu des Rappels
Les rappels sont des méthodes qui sont appelées à certains moments du cycle de vie d'un objet. Grâce aux rappels, il est possible d'écrire du code qui s'exécutera chaque fois qu'un objet Active Record est créé, enregistré, mis à jour, supprimé, validé ou chargé depuis la base de données.
2.1 Enregistrement des Rappels
Pour utiliser les rappels disponibles, vous devez les enregistrer. Vous pouvez implémenter les rappels en tant que méthodes ordinaires et utiliser une méthode de classe de style macro pour les enregistrer en tant que rappels :
class User < ApplicationRecord
validates :login, :email, presence: true
before_validation :ensure_login_has_a_value
private
def ensure_login_has_a_value
if login.blank?
self.login = email unless email.blank?
end
end
end
Les méthodes de classe de style macro peuvent également recevoir un bloc. Utilisez ce style si le code à l'intérieur de votre bloc est si court qu'il tient sur une seule ligne :
class User < ApplicationRecord
validates :login, :email, presence: true
before_create do
self.name = login.capitalize if name.blank?
end
end
Vous pouvez également passer un procédure au rappel à déclencher.
class User < ApplicationRecord
before_create ->(user) { user.name = user.login.capitalize if user.name.blank? }
end
Enfin, vous pouvez définir votre propre objet de rappel personnalisé, que nous aborderons plus en détail plus tard ci-dessous.
class User < ApplicationRecord
before_create MaybeAddName
end
class MaybeAddName
def self.before_create(record)
if record.name.blank?
record.name = record.login.capitalize
end
end
end
Les rappels peuvent également être enregistrés pour ne se déclencher que lors de certains événements du cycle de vie, ce qui permet un contrôle complet sur le moment et le contexte dans lequel vos rappels sont déclenchés.
class User < ApplicationRecord
before_validation :normalize_name, on: :create
# :on prend également un tableau
after_validation :set_location, on: [ :create, :update ]
private
def normalize_name
self.name = name.downcase.titleize
end
def set_location
self.location = LocationService.query(self)
end
end
Il est considéré comme une bonne pratique de déclarer les méthodes de rappel comme privées. Si elles sont laissées publiques, elles peuvent être appelées depuis l'extérieur du modèle et violer le principe d'encapsulation des objets.
ATTENTION. Évitez les appels à update
, save
ou d'autres méthodes qui créent des effets secondaires sur l'objet à l'intérieur de votre rappel. Par exemple, n'appelez pas update(attribute: "valeur")
dans un rappel. Cela peut modifier l'état du modèle et entraîner des effets secondaires inattendus lors de la validation. À la place, vous pouvez assigner en toute sécurité des valeurs directement (par exemple, self.attribute = "valeur"
) dans before_create
/ before_update
ou dans des rappels antérieurs.
3 Rappels Disponibles
Voici une liste de tous les rappels disponibles dans Active Record, répertoriés dans le même ordre dans lequel ils seront appelés lors des opérations respectives :
3.1 Création d'un Objet
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit
/after_rollback
3.2 Mise à Jour d'un Objet
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
after_commit
/after_rollback
ATTENTION. after_save
s'exécute à la fois lors de la création et de la mise à jour, mais toujours après les rappels plus spécifiques after_create
et after_update
, quel que soit l'ordre dans lequel les appels de macro ont été exécutés.
3.3 Suppression d'un Objet
REMARQUE : Les rappels before_destroy
doivent être placés avant les associations dependent: :destroy
(ou utiliser l'option prepend: true
), pour s'assurer qu'ils s'exécutent avant la suppression des enregistrements par dependent: :destroy
.
ATTENTION. after_commit
offre des garanties très différentes de after_save
, after_update
et after_destroy
. Par exemple, si une exception se produit dans un after_save
, la transaction sera annulée et les données ne seront pas persistées. Tandis que tout ce qui se passe after_commit
peut garantir que la transaction est déjà terminée et que les données ont été persistées dans la base de données. Plus d'informations sur les rappels transactionnels ci-dessous.
3.4 after_initialize
et after_find
Chaque fois qu'un objet Active Record est instancié, le rappel after_initialize
est appelé, que ce soit en utilisant directement new
ou lorsqu'un enregistrement est chargé depuis la base de données. Cela peut être utile pour éviter de devoir remplacer directement la méthode initialize
de votre Active Record.
Lors du chargement d'un enregistrement depuis la base de données, le rappel after_find
est appelé. after_find
est appelé avant after_initialize
si les deux sont définis.
NOTE : Les rappels after_initialize
et after_find
n'ont pas de contreparties before_*
.
Ils peuvent être enregistrés de la même manière que les autres rappels Active Record.
class User < ApplicationRecord
after_initialize do |user|
puts "Vous avez initialisé un objet !"
end
after_find do |user|
puts "Vous avez trouvé un objet !"
end
end
irb> User.new
Vous avez initialisé un objet !
=> #<User id: nil>
irb> User.first
Vous avez trouvé un objet !
Vous avez initialisé un objet !
=> #<User id: 1>
3.5 after_touch
Le rappel after_touch
est appelé chaque fois qu'un objet Active Record est touché.
class User < ApplicationRecord
after_touch do |user|
puts "Vous avez touché un objet"
end
end
irb> u = User.create(name: 'Kuldeep')
=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">
irb> u.touch
Vous avez touché un objet
=> true
Il peut être utilisé avec belongs_to
:
class Book < ApplicationRecord
belongs_to :library, touch: true
after_touch do
puts 'Un livre a été touché'
end
end
class Library < ApplicationRecord
has_many :books
after_touch :log_when_books_or_library_touched
private
def log_when_books_or_library_touched
puts 'Un livre/une bibliothèque a été touché(e)'
end
end
irb> @book = Book.last
=> #<Book id: 1, library_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
irb> @book.touch # déclenche @book.library.touch
Un livre a été touché
Un livre/une bibliothèque a été touché(e)
=> true
4 Exécution des rappels
Les méthodes suivantes déclenchent des rappels :
create
create!
destroy
destroy!
destroy_all
destroy_by
save
save!
save(validate: false)
toggle!
touch
update_attribute
update
update!
valid?
De plus, le rappel after_find
est déclenché par les méthodes de recherche suivantes :
all
first
find
find_by
find_by_*
find_by_*!
find_by_sql
last
Le rappel after_initialize
est déclenché à chaque fois qu'un nouvel objet de la classe est initialisé.
NOTE : Les méthodes find_by_*
et find_by_*!
sont des finders dynamiques générés automatiquement pour chaque attribut. En savoir plus à leur sujet dans la section Finders dynamiques
5 Passer outre les rappels
Tout comme avec les validations, il est également possible de passer outre les rappels en utilisant les méthodes suivantes :
decrement!
decrement_counter
delete
delete_all
delete_by
increment!
increment_counter
insert
insert!
insert_all
insert_all!
touch_all
update_column
update_columns
update_all
update_counters
upsert
upsert_all
Ces méthodes doivent être utilisées avec prudence, car des règles métier importantes et de la logique d'application peuvent être conservées dans les rappels. Les contourner sans comprendre les implications potentielles peut conduire à des données invalides.
6 Arrêt de l'exécution
Lorsque vous commencez à enregistrer de nouveaux rappels pour vos modèles, ils sont mis en file d'attente pour l'exécution. Cette file d'attente comprend toutes les validations de votre modèle, les rappels enregistrés et l'opération de base de données à exécuter.
Toute la chaîne de rappels est enveloppée dans une transaction. Si un rappel génère une exception, la chaîne d'exécution est interrompue et un ROLLBACK est effectué. Pour arrêter intentionnellement une chaîne, utilisez :
throw :abort
ATTENTION. Toute exception qui n'est pas ActiveRecord::Rollback
ou ActiveRecord::RecordInvalid
sera relancée par Rails après l'arrêt de la chaîne de rappels. De plus, cela peut casser du code qui n'attend pas que les méthodes comme save
et update
(qui essaient normalement de renvoyer true
ou false
) génèrent une exception.
NOTE : Si une exception ActiveRecord::RecordNotDestroyed
est levée dans le rappel after_destroy
, before_destroy
ou around_destroy
, elle ne sera pas relancée et la méthode destroy
renverra false
.
7 Rappels relationnels
Les rappels fonctionnent à travers les relations entre les modèles et peuvent même être définis par eux. Supposons un exemple où un utilisateur a plusieurs articles. Les articles d'un utilisateur doivent être détruits si l'utilisateur est détruit. Ajoutons un rappel after_destroy
au modèle User
via sa relation avec le modèle Article
:
class User < ApplicationRecord
has_many :articles, dependent: :destroy
end
class Article < ApplicationRecord
after_destroy :log_destroy_action
def log_destroy_action
puts 'Article détruit'
end
end
irb> user = User.first
=> #<User id: 1>
irb> user.articles.create!
=> #<Article id: 1, user_id: 1>
irb> user.destroy
Article détruit
=> #<User id: 1>
8 Rappels conditionnels
Tout comme avec les validations, nous pouvons également conditionner l'appel d'une méthode de rappel en fonction de la satisfaction d'un prédicat donné. Nous pouvons le faire en utilisant les options :if
et :unless
, qui peuvent prendre un symbole, un Proc
ou un tableau.
Vous pouvez utiliser l'option :if
lorsque vous souhaitez spécifier dans quelles conditions le rappel doit être appelé. Si vous souhaitez spécifier les conditions dans lesquelles le rappel ne doit pas être appelé, vous pouvez utiliser l'option :unless
.
8.1 Utilisation de :if
et :unless
avec un Symbole
Vous pouvez associer les options :if
et :unless
à un symbole correspondant au nom d'une méthode prédicat qui sera appelée juste avant le rappel.
Lorsque vous utilisez l'option :if
, le rappel ne sera pas exécuté si la méthode prédicat renvoie false ; lorsque vous utilisez l'option :unless
, le rappel ne sera pas exécuté si la méthode prédicat renvoie true. C'est l'option la plus courante.
class Order < ApplicationRecord
before_save :normalize_card_number, if: :paid_with_card?
end
En utilisant cette forme d'enregistrement, il est également possible d'enregistrer plusieurs prédicats différents qui doivent être appelés pour vérifier si le rappel doit être exécuté. Nous aborderons cela ci-dessous.
8.2 Utilisation de :if
et :unless
avec un Proc
Il est possible d'associer :if
et :unless
à un objet Proc
. Cette option est particulièrement adaptée lors de l'écriture de méthodes de validation courtes, généralement d'une seule ligne :
class Order < ApplicationRecord
before_save :normalize_card_number,
if: Proc.new { |order| order.paid_with_card? }
end
Comme le proc
est évalué dans le contexte de l'objet, il est également possible d'écrire ceci :
class Order < ApplicationRecord
before_save :normalize_card_number, if: Proc.new { paid_with_card? }
end
8.3 Conditions de rappel multiples
Les options :if
et :unless
acceptent également un tableau de procs ou de noms de méthodes sous forme de symboles :
class Comment < ApplicationRecord
before_save :filter_content,
if: [:subject_to_parental_control?, :untrusted_author?]
end
Vous pouvez facilement inclure un proc dans la liste des conditions :
class Comment < ApplicationRecord
before_save :filter_content,
if: [:subject_to_parental_control?, Proc.new { untrusted_author? }]
end
8.4 Utilisation de :if
et :unless
ensemble
Les rappels peuvent mélanger à la fois :if
et :unless
dans la même déclaration :
class Comment < ApplicationRecord
before_save :filter_content,
if: Proc.new { forum.parental_control? },
unless: Proc.new { author.trusted? }
end
Le rappel ne s'exécute que lorsque toutes les conditions :if
sont évaluées à true
et aucune des conditions :unless
n'est évaluée à true
.
9 Classes de rappels
Parfois, les méthodes de rappel que vous écrirez seront suffisamment utiles pour être réutilisées par d'autres modèles. Active Record permet de créer des classes qui encapsulent les méthodes de rappel, afin qu'elles puissent être réutilisées.
Voici un exemple où nous créons une classe avec un rappel after_destroy
pour gérer la suppression des fichiers supprimés sur le système de fichiers. Ce comportement peut ne pas être unique à notre modèle PictureFile
et nous souhaitons peut-être le partager, il est donc judicieux de l'encapsuler dans une classe distincte. Cela facilitera les tests de ce comportement et sa modification.
class FileDestroyerCallback
def after_destroy(file)
if File.exist?(file.filepath)
File.delete(file.filepath)
end
end
end
Lorsqu'ils sont déclarés à l'intérieur d'une classe, comme ci-dessus, les méthodes de rappel recevront l'objet du modèle en tant que paramètre. Cela fonctionnera sur n'importe quel modèle qui utilise la classe de la manière suivante :
class PictureFile < ApplicationRecord
after_destroy FileDestroyerCallback.new
end
Notez que nous avons dû instancier un nouvel objet FileDestroyerCallback
, car nous avons déclaré notre rappel en tant que méthode d'instance. Cela est particulièrement utile si les rappels utilisent l'état de l'objet instancié. Cependant, il sera souvent plus logique de déclarer les rappels en tant que méthodes de classe :
class FileDestroyerCallback
def self.after_destroy(file)
if File.exist?(file.filepath)
File.delete(file.filepath)
end
end
end
Lorsque la méthode de rappel est déclarée de cette manière, il ne sera pas nécessaire d'instancier un nouvel objet FileDestroyerCallback
dans notre modèle.
class PictureFile < ApplicationRecord
after_destroy FileDestroyerCallback
end
Vous pouvez déclarer autant de rappels que vous le souhaitez à l'intérieur de vos classes de rappel.
10 Rappels de transaction
10.1 Gestion de la cohérence
Il existe deux rappels supplémentaires qui sont déclenchés à la fin d'une transaction de base de données : after_commit
et after_rollback
. Ces rappels sont très similaires au rappel after_save
, à la différence qu'ils ne s'exécutent qu'après que les modifications de la base de données ont été validées ou annulées. Ils sont particulièrement utiles lorsque vos modèles Active Record doivent interagir avec des systèmes externes qui ne font pas partie de la transaction de base de données.
Considérez, par exemple, l'exemple précédent où le modèle PictureFile
doit supprimer un fichier après la destruction de l'enregistrement correspondant. Si une exception est levée après l'appel du rappel after_destroy
et que la transaction est annulée, le fichier aura été supprimé et le modèle sera dans un état incohérent. Par exemple, supposons que picture_file_2
dans le code ci-dessous n'est pas valide et que la méthode save!
génère une erreur.
PictureFile.transaction do
picture_file_1.destroy
picture_file_2.save!
end
En utilisant le rappel after_commit
, nous pouvons prendre en compte ce cas.
class PictureFile < ApplicationRecord
after_commit :delete_picture_file_from_disk, on: :destroy
def delete_picture_file_from_disk
if File.exist?(filepath)
File.delete(filepath)
end
end
end
NOTE : L'option :on
spécifie quand un rappel sera déclenché. Si vous ne fournissez pas l'option :on
, le rappel sera déclenché pour chaque action.
10.2 Le contexte est important
Étant donné que l'utilisation du rappel after_commit
uniquement lors de la création, de la mise à jour ou de la suppression est courante, il existe des alias pour ces opérations :
class PictureFile < ApplicationRecord
after_destroy_commit :delete_picture_file_from_disk
def delete_picture_file_from_disk
if File.exist?(filepath)
File.delete(filepath)
end
end
end
AVERTISSEMENT. Lorsqu'une transaction est terminée, les rappels after_commit
ou after_rollback
sont appelés pour tous les modèles créés, mis à jour ou supprimés dans cette transaction. Cependant, si une exception est levée dans l'un de ces rappels, l'exception remontera et les méthodes after_commit
ou after_rollback
restantes ne seront pas exécutées. Par conséquent, si votre code de rappel peut générer une exception, vous devrez la capturer et la gérer dans le rappel afin de permettre l'exécution des autres rappels.
AVERTISSEMENT. Le code exécuté dans les rappels after_commit
ou after_rollback
n'est pas lui-même inclus dans une transaction.
AVERTISSEMENT. Utiliser à la fois after_create_commit
et after_update_commit
avec le même nom de méthode ne permettra que le dernier rappel défini d'être pris en compte, car ils sont tous deux des alias internes de after_commit
qui remplace les rappels précédemment définis avec le même nom de méthode.
class User < ApplicationRecord
after_create_commit :log_user_saved_to_db
after_update_commit :log_user_saved_to_db
private
def log_user_saved_to_db
puts 'L\'utilisateur a été enregistré dans la base de données'
end
end
irb> @user = User.create # ne rien afficher
irb> @user.save # mise à jour de @user
L'utilisateur a été enregistré dans la base de données
10.3 after_save_commit
Il y a aussi after_save_commit
, qui est un alias pour utiliser le rappel after_commit
à la fois pour la création et la mise à jour ensemble :
class User < ApplicationRecord
after_save_commit :log_user_saved_to_db
private
def log_user_saved_to_db
puts 'L\'utilisateur a été enregistré dans la base de données'
end
end
irb> @user = User.create # création d'un utilisateur
L'utilisateur a été enregistré dans la base de données
irb> @user.save # mise à jour de @user
L'utilisateur a été enregistré dans la base de données
10.4 Ordre des rappels transactionnels
Lors de la définition de plusieurs rappels transactionnels after_
(after_commit
, after_rollback
, etc), l'ordre sera inversé par rapport à leur définition.
class User < ActiveRecord::Base
after_commit { puts("Ceci est en fait appelé en second") }
after_commit { puts("Ceci est en fait appelé en premier") }
end
NOTE : Cela s'applique également à toutes les variations de after_*_commit
, telles que after_destroy_commit
.
Retour d'information
Vous êtes encouragé à contribuer à l'amélioration de la qualité de ce guide.
Veuillez contribuer si vous trouvez des fautes de frappe ou des erreurs factuelles. Pour commencer, vous pouvez lire notre contribution à la documentation section.
Vous pouvez également trouver du contenu incomplet ou des informations qui ne sont pas à jour. Veuillez ajouter toute documentation manquante pour la version principale. Assurez-vous de vérifier Edge Guides d'abord pour vérifier si les problèmes ont déjà été résolus ou non sur la branche principale. Consultez les Directives des guides Ruby on Rails pour le style et les conventions.
Si pour une raison quelconque vous repérez quelque chose à corriger mais ne pouvez pas le faire vous-même, veuillez ouvrir un problème.
Et enfin, toute discussion concernant la documentation de Ruby on Rails est la bienvenue sur le Forum officiel de Ruby on Rails.