Rails ActiveRecord Callbacks: Wanneer Ze Helpen en Wanneer Ze Je Bijten
ActiveRecord callbacks laten je inhaken op de levenscyclus van een object — before_save, after_create, around_destroy — en automatisch code uitvoeren. Het is een van de eerste Rails-features waar ontwikkelaars naar grijpen, en een van de eerste dingen die ervaren ontwikkelaars leren wantrouwen.
Het probleem zijn niet de callbacks zelf. Het is dat ze onzichtbare koppeling creëren tussen “een record opslaan” en “alles wat moet gebeuren als een record wordt opgeslagen.” Die koppeling stapelt zich op tot je testsuite 45 seconden nodig heeft om één gebruiker aan te maken, omdat after_create een e-mail triggert, een analytics-event, een webhook en drie cache-invalidaties.
Hier lees je hoe je callbacks gebruikt zonder een kaartenhuis te bouwen.
Callbacks Die (Meestal) Veilig Zijn
Sommige callbacks horen in models omdat ze over data-integriteit gaan — zorgen dat het record correct is voordat het in de database terechtkomt.
Data Normaliseren met before_validation
class User < ApplicationRecord
before_validation :normalize_email
private
def normalize_email
self.email = email&.strip&.downcase
end
end
Dit is een schoolvoorbeeld van een veilige callback. Het gaat over de data op dit specifieke record, het heeft geen neveneffecten, en het maakt validatie betrouwbaarder. Als je het zou verwijderen, krijg je inconsistente e-mailnotatie in je database.
Andere goede toepassingen voor before_validation:
- Witruimte verwijderen uit stringvelden
- Standaardwaarden instellen die afhangen van andere attributen
- Slugs genereren uit titels
Standaardwaarden met before_create
class Organization < ApplicationRecord
before_create :set_default_plan
private
def set_default_plan
self.plan ||= "free"
end
end
Database-defaults dekken eenvoudige gevallen, maar wanneer de standaardwaarde afhankelijk is van logica (bijvoorbeeld trial_ends_at instellen op de huidige tijd plus 14 dagen), houdt before_create het dicht bij het model.
Tellers Bijhouden met after_create / after_destroy
Rails heeft ingebouwde counter_cache:
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
# Rails regelt dit automatisch — geen callback nodig
end
Gebruik de ingebouwde counter_cache: true optie op belongs_to in plaats van je eigen after_create / after_destroy paar te schrijven. Het handelt race conditions en randgevallen af die je anders zelf zou moeten oplossen.
Callbacks Die Problemen Veroorzaken
E-mails Versturen vanuit after_create
Dit is de meest voorkomende callback-fout in Rails-applicaties:
# Doe dit niet
class Order < ApplicationRecord
after_create :send_confirmation_email
private
def send_confirmation_email
OrderMailer.confirmation(self).deliver_later
end
end
Het ziet er netjes uit. Het is ook een valkuil. Dit is waarom:
Probleem 1: Je testsuite verstuurt e-mails (of enqueuet jobs) elke keer dat het een order aanmaakt. Je eindigt met ActionMailer::Base.deliveries.clear door je tests heen, of je wraps factories in perform_enqueued_jobs blokken.
Probleem 2: Seed-scripts en datamigraties triggeren de callback. Draai Order.create!(...) in een migratie en je hebt net een bevestigingsmail verstuurd voor een drie jaar oude order.
Probleem 3: De callback draait zelfs wanneer je dat niet wilt. Orders importeren uit een CSV? Admin maakt een testorder? De e-mail gaat elke keer af, tenzij je skip_callbacks gaat gebruiken — wat het impliciete contract breekt dat callbacks altijd draaien.
Externe Services Aanroepen vanuit after_save
# Dit gaat pijn doen
class Payment < ApplicationRecord
after_save :sync_to_stripe
private
def sync_to_stripe
StripeService.update_payment(self)
end
end
Als de Stripe API down is, faalt het opslaan van een betaalrecord. Je gebruikers kunnen niet afrekenen omdat een third-party sync gekoppeld is aan je persistentielaag. De fout is onzichtbaar — de exception komt vanuit een callback, niet vanuit de controller-actie die het triggerde.
Cascaderende Callbacks
De ergste callback-bugs komen van ketens:
class User < ApplicationRecord
after_create :create_default_workspace
end
class Workspace < ApplicationRecord
after_create :create_default_channel
end
class Channel < ApplicationRecord
after_create :notify_workspace_members
end
Een gebruiker aanmaken stuurt nu een notificatie. Niemand die naar User.create! kijkt zou dat verwachten. En als de notificatieservice een fout gooit, wordt de hele gebruikersaanmaak teruggedraaid — inclusief de workspace en het channel — omdat het allemaal binnen één transactie gebeurde.
Wat Je In Plaats Daarvan Kunt Gebruiken
Service Objects voor Bedrijfslogica
Verplaats neveneffecten uit callbacks naar expliciete service objects:
class Orders::Create
def initialize(user:, cart:)
@user = user
@cart = cart
end
def call
order = Order.create!(
user: @user,
items: @cart.items,
total: @cart.total
)
OrderMailer.confirmation(order).deliver_later
Analytics.track("order_created", user: @user, order: order)
WebhookService.notify(:order_created, order)
order
end
end
Nu is elk neveneffect zichtbaar op de plek waar het wordt aangeroepen. Tests voor Order triggeren geen e-mails. Je kunt orders aanmaken in seeds en migraties zonder neveneffecten.
after_commit voor Cache-invalidatie
Als je toch een callback moet gebruiken voor neveneffecten, is after_commit veiliger dan after_save omdat het draait nadat de databasetransactie is gecommit:
class Product < ApplicationRecord
after_commit :invalidate_cache, on: [:create, :update, :destroy]
private
def invalidate_cache
Rails.cache.delete("products/#{id}")
Rails.cache.delete("products/index")
end
end
In Rails 7.1+ kun je after_commit ook combineren met jobs:
class Product < ApplicationRecord
after_create_commit :index_in_search
private
def index_in_search
SearchIndexJob.perform_later(self)
end
end
Jobs die vanuit after_commit worden geënqueued, draaien pas als het record daadwerkelijk is opgeslagen. Dit voorkomt de race condition waarbij Sidekiq de job oppakt voordat de transactie is gecommit.
ActiveSupport::Notifications voor Ontkoppelde Events
Voor complexe event-driven architecturen heeft Rails een ingebouwd pub/sub systeem:
# In je service object
ActiveSupport::Notifications.instrument("order.created", order: order)
# In een initializer
ActiveSupport::Notifications.subscribe("order.created") do |event|
OrderMailer.confirmation(event.payload[:order]).deliver_later
end
Dit ontkoppelt het event volledig van de handler. De code die de order aanmaakt hoeft niet te weten wat er daarna gebeurt.
Het Beslisframework
Gebruik dit om te bepalen waar logica thuishoort:
Gebruik een callback wanneer:
- De logica gaat over data-integriteit op dit specifieke record
- Het heeft geen externe neveneffecten (geen HTTP-calls, geen e-mails, geen job-enqueuing)
- Verwijderen zou de database in een inconsistente staat achterlaten
- Het moet elke keer draaien, zonder uitzonderingen
Gebruik een service object wanneer:
- De logica meerdere models of externe services omvat
- Tests het niet standaard zouden moeten triggeren
- Je scenario’s kunt bedenken waarin je het zou willen overslaan
- Het een bedrijfsproces vertegenwoordigt, geen databeperking
Gebruik after_commit wanneer:
- Je moet reageren op persistente wijzigingen (cache-invalidatie, zoekindexering)
- Het neveneffect alleen moet plaatsvinden als de transactie slaagt
- Het neveneffect lichtgewicht is en waarschijnlijk niet faalt
Omgaan met Bestaande Callback-spaghetti
Als je in een codebase werkt die al overal callbacks heeft:
Stap 1: Audit je callbacks. Draai dit in de Rails console:
ApplicationRecord.descendants.each do |model|
callbacks = model.__callbacks.flat_map { |type, chain| chain.map { |cb| "#{type}: #{cb.filter}" } }
puts "#{model.name}: #{callbacks.join(', ')}" if callbacks.any?
end
Stap 2: Categoriseer elke callback als “data-integriteit” of “neveneffect.”
Stap 3: Extraheer neveneffect-callbacks naar service objects, één tegelijk. Begin met degene die de meeste testpijn veroorzaken of de meest verwarrende bugs.
Callbacks en skip_callbacks: Een Code Smell
Als je dit schrijft:
User.skip_callback(:create, :after, :send_welcome_email)
user = User.create!(email: "test@example.com")
User.set_callback(:create, :after, :send_welcome_email)
Dan is dat een signaal dat de logica niet in een callback thuishoort. Je werkt om je eigen architectuur heen. Het skip_callback / set_callback patroon is bovendien niet thread-safe in productie — twee gelijktijdige requests kunnen elkaar verstoren.
Veelgestelde Vragen
Zijn Rails callbacks slecht?
Nee. Callbacks voor datanormalisatie en integriteitscontroles zijn prima. De problemen beginnen wanneer callbacks neveneffecten triggeren zoals e-mails, API-calls of job-enqueuing. De callback zelf is niet slecht — het is de koppeling tussen persistentie en bedrijfslogica die problemen veroorzaakt.
Moet ik after_save of after_commit gebruiken?
Geef de voorkeur aan after_commit voor alles wat interactie heeft met de buitenwereld (jobs, caches, externe services). after_save draait binnen de transactie, wat betekent dat het record mogelijk nog niet daadwerkelijk is opgeslagen wanneer je neveneffect draait. after_commit garandeert dat de data in de database staat.
Hoe test ik models met callbacks?
Als je callbacks dataintegriteit afhandelen (velden normaliseren, standaardwaarden instellen), test ze dan als onderdeel van je normale modeltests. Als je callbacks neveneffecten triggeren, is dat een teken om ze te extraheren naar service objects die je onafhankelijk kunt testen.
Kunnen callbacks N+1 queries veroorzaken?
Ja. Een callback zoals after_save :update_parent_stats die associaties laadt, triggert een query elke keer dat een record wordt opgeslagen. Als je records in een loop opslaat, krijg je N+1 gedrag van callbacks net als vanuit views. Gebruik after_commit met achtergrond-jobs voor zware berekeningen.
Wat over around_* callbacks?
around_save, around_create, etc. zijn zelden nodig en moeilijk te doorgronden. Ze wrappen de hele operatie en vereisen dat je expliciet yield aanroept om de keten voort te zetten. In zeven jaar Rails-consulting heb ik legitiem gebruik van around_* callbacks precies twee keer gezien — beide voor complexe audit logging. Als je denkt dat je er een nodig hebt, heb je waarschijnlijk een service object nodig.
About the Author
Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.
Get in TouchRelated Articles
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation