RUBY ON RAILS · 13 MIN READ ·

Rails Zeitwerk Autoloading: NameErrors oplossen, Eager Loading en de Classic Loader Migratie

Rails Zeitwerk autoloading uitgelegd: fix NameErrors, begrijp eager loading valkuilen, migreer van Classic loader, en configureer inflections. Volledige gids.

Rails Zeitwerk Autoloading: NameErrors oplossen, Eager Loading en de Classic Loader Migratie

Het berichtje kwam op een donderdagochtend om twee uur. Niet van een klant, maar van een Sentry-alert op mijn telefoon, veertig seconden na een geslaagde GitHub Actions-deploy: NameError: uninitialized constant Api::V2::PaymentGateway::StripeAdapter.

Die constante bestond. Ik had hem twee weken eerder geschreven. De volledige testsuite op CI was groen. Staging had nooit geklaagd. We hadden de rollback in vier minuten gedaan. Nog twee uur later snapte ik eindelijk waarom het mis was gegaan.

Ontwikkelomgevingen laden constanten lazy, bij eerste gebruik. Productie laadt alles in één keer bij het opstarten. Het StripeAdapter-bestand had een subtiele naamgevingsfout die Zeitwerk tolereerde onder lazy loading — omdat iets anders het bestand toevallig eerder inlaadde — maar die hij afkeurde bij eager loading. We hadden een productie-NameError opgeleverd die geen enkele test had gevangen, omdat ook onze CI-omgeving met eager loading uitgeschakeld draaide.

Na negentien jaar Rails heb ik deze fout zelf gemaakt, bij klanten zien gebeuren, en Zeitwerks regels vaker uitgelegd dan ik zou willen. Dit is de gids die ik wilde hebben voor die donderdagnacht.

Hoe Rails Zeitwerk Autoloading Werkt

Zeitwerk verving Rails’ Classic autoloader in Rails 6.0 en werd de enige ondersteunde optie vanaf Rails 7.0. Classic autoloading gebruikte $LOAD_PATH-lookups en const_missing-hooks — flexibel op manieren die subtiele bugs veroorzaakten in multithreaded omgevingen, en fundamenteel incompatibel met thread-safe eager loading.

Zeitwerk is strikter en voorspelbaarder. De kernregel: het bestandspad relatief aan een autoload-root moet exact overeenkomen met de constante die het definieert.

app/models/user.rb verwacht dat het bestand User definieert. app/models/admin/billing_report.rb verwacht Admin::BillingReport. Één bestand, één constante, het pad dicteert de naam.

Ontwikkelingsmodus laadt lazy: constanten worden ingeladen bij eerste gebruik. Productimodus laadt eager: alle bestanden onder autoload-roots worden bij het opstarten ingeladen. Dat verschil is waar de meeste Zeitwerk-bugs zich verstoppen. Een constante die in development werkt omdat hij toevallig eerder ingeladen wordt via een andere route — een keten die Zeitwerk niet kan garanderen — werkt stil in development maar faalt in productie.

De NameErrors Die Je Echt Gaat Tegenkomen

Bestandsnaam en constante komen niet overeen

# app/services/stripe_payment_processor.rb
class StripePaymentProcessorService  # FOUT
  def charge(amount:, currency:)
    # ...
  end
end

Zeitwerk verwacht StripePaymentProcessor in dit bestand omdat het stripe_payment_processor.rb heet. Wanneer code elders StripePaymentProcessorService aanroept, zoekt Zeitwerk naar stripe_payment_processor_service.rb, dat niet bestaat, en gooit een NameError.

Oplossing: hernoem het bestand naar stripe_payment_processor_service.rb, of hernoem de klasse naar StripePaymentProcessor. Kies wat overeenkomt met hoe de rest van je codebase ernaar verwijst. Voer daarna de checker uit:

bin/rails zeitwerk:check

Dit scant alle autoload-paden en rapporteert elk bestand waar de gedefinieerde constante niet overeenkomt met wat Zeitwerk op basis van de bestandsnaam verwacht. Voeg dit toe aan je CI-pipeline. Het duurt minder dan twee seconden.

Naamruimtestructuur komt niet overeen met mapstructuur

# app/models/billing_invoice.rb  ← verkeerde locatie
class Billing::Invoice            # Zeitwerk verwacht dit in app/models/billing/invoice.rb
end

Zeitwerk leest het pad om de verwachte constante te bepalen. billing_invoice.rb in de root van app/models/ vertelt Zeitwerk dat BillingInvoice verwacht wordt. Onder lazy loading gaat dit mis zodra eager loading aanstaat. Zeitwerk verwerkt billing_invoice.rb, verwacht BillingInvoice, vindt Billing::Invoice en gooit een Zeitwerk::Error.

De oplossing is om je modulehiërarchie te spiegelen in de mapstructuur:

app/models/
  billing.rb              # definieert module Billing (of laat weg — Zeitwerk maakt de module aan)
  billing/
    invoice.rb            # definieert Billing::Invoice
    line_item.rb          # definieert Billing::LineItem

Zeitwerk kan namespace-modules automatisch aanmaken — je hebt geen billing.rb nodig tenzij de module methoden of includes nodig heeft.

require_dependency bestaat niet meer

Classic autoloading vertrouwde op require_dependency voor het beheren van laadvolgorde. Zeitwerk gebruikt het niet. Elk require_dependency-aanroep die na een Classic-naar-Zeitwerk-migratie overblijft, is op z’n best dood gewicht en op z’n slechtst gevaarlijk:

# FOUT — doe dit niet met Zeitwerk
require_dependency "billing/invoice"

class OrdersController < ApplicationController
  def create
    order = Billing::Invoice.create!(order_params)
    # ...
  end
end

Verwijder elke require_dependency-aanroep. Zoek ze op:

grep -r "require_dependency" app/ lib/

Als het verwijderen iets breekt, is de onderliggende oorzaak een naamgevings- of structuurprobleem — geen laadvolgorde om te patchen. Zeitwerk handelt de laadvolgorde correct af wanneer je bestandsnamen overeenkomen met je constanten.

Eager Loading: Het Productiekloof-Probleem

Het operationeel gevaarlijkste aspect van Rails Zeitwerk autoloading is het standaard configuratieverschil tussen omgevingen:

# config/environments/development.rb
config.eager_load = false  # Rails-standaard in development

# config/environments/production.rb
config.eager_load = true   # Rails-standaard in productie

Met eager_load = false worden constanten op aanvraag ingeladen. Je testsuite kan elk codepad doorlopen zonder ooit een Zeitwerk-schending te triggeren, omdat de tests toevallig constanten in een volgorde laden waarbij alle namen correct resolven.

Reproduceer het productiegedrag lokaal door dit in een Rails-console uit te voeren:

Rails.application.eager_load!

Elke Zeitwerk::Error die daardoor wordt gegenereerd, is een productie-NameError die wacht om te gebeuren. Los het op voor het live gaat.

De betrouwbaardere plek om dit te vangen is CI. Voeg een aparte stap toe aan je workflow:

# .github/workflows/test.yml
- name: Verifieer Zeitwerk eager loading
  run: bin/rails runner "Rails.application.eager_load!"
  env:
    RAILS_ENV: production
    SECRET_KEY_BASE: "ci-placeholder-not-used"
    DATABASE_URL: $

Deze stap duurt minder dan vijf seconden en heeft voor mij al meerdere keren een productie-NameError voorkomen. De GitHub Actions CI/CD-gids behandelt de volledige workflow-configuratie als je deze nog niet hebt staan.

Aangepaste Autoload-Paden

Rails voegt standaard app/*-submappen toe aan Zeitwerks autoload-roots. Om je eigen toe te voegen:

# config/application.rb
module MyApp
  class Application < Rails::Application
    config.autoload_paths << Rails.root.join("lib/my_app")
  end
end

Met deze configuratie moet lib/my_app/data_importer.rb de klasse DataImporter definiëren. Een bestand op lib/my_app/importers/csv_importer.rb moet Importers::CsvImporter definiëren.

De meest voorkomende val is lib zelf als autoload-root toevoegen:

# DOE DIT NIET
config.autoload_paths << Rails.root.join("lib")

Met lib als root overschaduwt een bestand genaamd lib/json.rb de standaard Ruby-bibliotheek json gem. Elk bestand in lib wordt potentieel gevaarlijk. Gebruik een naamruimte-submap: lib/my_app/, lib/domain/, lib/services/.

Om te inspecteren wat Zeitwerk tijdens runtime geregistreerd heeft:

# In een Rails-console
Rails.autoloaders.main.dirs          # alle autoload-roots
Rails.autoloaders.each(&:log!)       # activeer uitgebreide laad-output

Inflections: Zeitwerk Je Afkortingen Leren

Zeitwerk vertaalt bestandsnamen naar constantennamen via ActiveSupports inflector. Gangbare Engelse patronen werken automatisch; domein-afkortingen en onregelmatige namen niet:

# Zonder configuratie:
# app/services/html_parser.rb    → Zeitwerk verwacht HtmlParser
# app/models/ttb_client.rb       → Zeitwerk verwacht TtbClient
# app/models/oauth_token.rb      → Zeitwerk verwacht OauthToken

Stel dit in via de inflections-initializer, die zowel Zeitwerk als ActiveSupport respecteert:

# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym "HTML"
  inflect.acronym "TTB"
  inflect.acronym "OAuth"
  inflect.acronym "API"
  inflect.acronym "JSON"
end

Met deze declaraties mapt Zeitwerk html_parser.rb naar HTMLParser, ttb_client.rb naar TTBClient, enzovoort. De inflect.acronym-aanpak verdient de voorkeur boven Zeitwerk-specifieke inflections omdat het consistent doorwerkt in classify, humanize, route-helpers en URL-generatie.

Voor eenmalige uitzonderingen die je niet met een acronymregel kunt oplossen, kun je Zeitwerk direct configureren:

# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
  autoloader.inflector.inflect(
    "mijn_vreemd_bestandsnaam" => "MijnVreemdConstante"
  )
end

Gebruik dit spaarzaam. Meer dan twee of drie van zulke uitzonderingen wijzen gewoonlijk op een naamgevingsprobleem in je conventies.

Migreren van Classic naar Zeitwerk Autoloading

Als je op Rails 6.x zit met config.autoloader = :classic en moet migreren naar Zeitwerk voor een Rails 7+-upgrade, is het proces recht-toe-recht-aan maar methodisch. De Rails upgrade incrementele strategie-post behandelt de bredere upgradeaanpak.

Stap 1: Voer de checker uit en los elke schending op.

bin/rails zeitwerk:check

Hernoem voor elk gerapporteerd bestand het bestand zodat het overeenkomt met de constante (aanbevolen), of hernoem de constante. Doe nu niets anders dan de naamgeving repareren.

Stap 2: Verwijder alle require_dependency-aanroepen.

grep -rn "require_dependency" app/ lib/ | sort

Verwijder elke aanroep. Commit deze verwijderingen apart van de naamgevingsfixes zodat de diff leesbaar blijft.

Stap 3: Voeg eager-loading-verificatie toe aan CI.

Verifieer vóór het overschakelen dat je huidige Classic-codebase de eager-loading-check doorstaat. Dit geeft je een schone uitgangsituatie.

Stap 4: Schakel de autoloader over.

In Rails 6.x-apps doe je dit expliciet:

# config/application.rb
config.autoloader = :zeitwerk

In Rails 7.0+ is Zeitwerk de enige optie — config.load_defaults 7.0 stelt het automatisch in. Er is geen config.autoloader-instelling meer nodig.

Stap 5: Voer zeitwerk:check nogmaals uit en test met eager_load = true.

Na het overschakelen de checker opnieuw uitvoeren — Zeitwerks regels wijken subtiel af van wat Classic tolereerde. Test daarna op staging met config.eager_load = true voordat je naar productie gaat.

De migratie duurt zelden meer dan een paar uur op een goed gestructureerde codebase. Op een codebase met vijftien jaar require_dependency door controllers verspreid en drie verschillende conventies voor domeinlogica, plan je een dag. De RSpec-testpatronen post heeft de testinfrastructuur om regressies te vangen terwijl je de migratie uitvoert.

Zeitwerk-compliance Testen in Je Spec Suite

Voeg een aparte spec toe die de volledige eager loader uitvoert:

# spec/autoloading_spec.rb
require "rails_helper"

RSpec.describe "Zeitwerk autoloader", type: :integration do
  it "laadt alle bestanden in zonder fout" do
    expect { Rails.application.eager_load! }.not_to raise_error
  end
end

Één spec, één bewering, klaar in minder dan een seconde. Zet hem in spec/autoloading_spec.rb in plaats van hem te begraven in een rails helper — hij is belangrijk genoeg om zichtbaar te zijn en moet bij elke CI-build draaien. De meest effectieve bescherming tegen productie-NameErrors die ik ooit aan een Rails-project heb toegevoegd.

Veelgestelde Vragen

Waarom werkt mijn constante in development maar geeft hij een NameError in productie?

Omdat config.eager_load standaard false is in development en true in productie. Met eager loading uit laadt Rails constanten lazy bij eerste gebruik. Als je testsuite of ontwikkelsessie toevallig de juiste laadvolgorde triggert, blijft een verkeerd benoemd bestand onopgemerkt. Eager loading legt de mismatch bloot omdat Zeitwerk elk bestand onvoorwaardelijk bij het opstarten laadt. Reproduceer het lokaal door Rails.application.eager_load! in de development-console uit te voeren voordat je iets oplevvert.

Wat is het verschil tussen autoload_paths en eager_load_paths in Rails?

autoload_paths vertelt Zeitwerk welke mappen hij moet bewaken en lazy vanuit laden in development. eager_load_paths vertelt Rails welke mappen eager geladen moeten worden in productie. In Rails 7+ wordt het toevoegen aan config.autoload_paths automatisch ook in eager_load_paths verwerkt — je hoeft ze niet allebei in te stellen. De enige situatie waarin je eager_load_paths direct zou instellen, is wanneer je een map eager wil laden maar niet beschikbaar wil stellen voor lazy autoloading in development, wat zeldzaam is.

Hoe ga ik om met constanten die dynamisch of binnen blocks gedefinieerd worden?

Zeitwerk kan constanten die binnen blocks, module_eval of via const_set gedefinieerd worden niet autoloaden. Gebruik voor deze gevallen een expliciete require bovenaan het bestand dat de constante nodig heeft. Dynamisch constanten aanmaken is sowieso de moeite waard om te elimineren — het is precies het soort ondoorzichtige laadregel-afhankelijkheid die Classic autoloadings flexibiliteit vroeger maskeerde.

Hoe debug ik een Zeitwerk::Error in productie als ik hem lokaal niet kan reproduceren?

Voer eerst bin/rails zeitwerk:check uit — dit identificeert het probleembestand en de verwachte constante. Vergelijk dat met de werkelijke constante bovenaan dat bestand. Als zeitwerk:check geen schendingen rapporteert, schakel Zeitwerk-logging tijdelijk in: Rails.autoloaders.each(&:log!) in een initializer, deploy naar staging, en bekijk de logs voor welke bestanden worden geladen en in welke volgorde. De log-output is uitgebreid maar de mismatch verschijnt altijd binnen een paar seconden na het opstarten.

Bezig met een Rails-upgrade die vastloopt op Zeitwerk-fouten? TTB Software heeft deze migratie uitgevoerd op codebases uit elk tijdperk van Rails. We weten waar de problemen begraven liggen. Negentien jaar ervaring betekent dat de migratie die er uitziet als een week werk, meestal een dag duurt.

#rails-zeitwerk-autoloading #zeitwerk-nameerror-rails #rails-classic-to-zeitwerk-migration #zeitwerk-eager-loading #rails-autoloading-paths #zeitwerk-inflections

Related Articles

Laatste sectie. Bel dan alsjeblieft.

Het is een telefoongesprek. Erger dan dat kan het niet worden.

Geen discovery-deck. Geen 45-minuten "kwalificatiegesprek." 30 minuten, jouw probleem, mijn mening. Als we een fit zijn weet je dat in minuut 12.

Directe lijn — Roger neemt zelf op
+31 6 5123 6132
Ma–vr, 09:00–18:00 CET · Nu beschikbaar

OF
info@ttb.software