35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Rails ActiveRecord Callbacks: Wanneer Ze Helpen en Wanneer Ze Je Bijten

Rails ActiveRecord Callbacks: Wanneer Ze Helpen en Wanneer Ze Je Bijten

TTB Software
rails
ActiveRecord callbacks besparen tijd tot ze onvindbare bugs veroorzaken. Leer welke callbacks veilig zijn, welke je beter kunt vermijden, en wat je in plaats daarvan kunt gebruiken in Rails 7+ en 8 applicaties met echte productievoorbeelden.

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.

#rails #activerecord #callbacks #best-practices #architectuur
T

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 Touch

Share this article

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