Module: Funes::Associations
Overview
Declares references to other models, accessible at interpretation time.
Events are immutable facts serialized as plain JSON in the props column, so an event cannot
store another model directly. The refers_to macro stores only the referenced record's id as a
regular event attribute (and therefore in props), while exposing a reader that lazily loads the
record by id and a writer that accepts the record itself.
The reader returns the record's current state (a live lookup via find_by), not a snapshot of
how it looked when the event was recorded. A reference whose record no longer exists reads as
nil rather than raising. The loaded record is memoized per reference and reloaded whenever the
foreign key attribute changes, matching Active Record's staleness handling.
Example
class Deposit::Created < Funes::Event
refers_to :customer
end
event = Deposit::Created.new(customer: some_customer)
event.customer_id # => some_customer.id (this is what gets serialized)
event.customer # => some_customer
# Inside an interpretation block, the reference is read straight off the event:
interpretation_for Deposit::Created do |state, event, _at|
state.customer = event.customer
state
end
Options
-
class_name- The name of the referenced class, as a string (or symbol) — passing the class itself raisesArgumentError, as in Active Record. Defaults to the camelized reference name (e.g.refers_to :customerinfers"Customer"). Resolved lazily, so it plays well with autoloading and namespaced constants. -
foreign_key- The attribute that stores the id. Defaults to"#{name}_id". -
required- Whentrue, adds a presence validation on the foreign key so an event without the reference is invalid and will not be persisted. Defaults tofalse(ActiveRecord-style).class Loan::Granted < Funes::Event refers_to :borrower, class_name: "User", required: true refers_to :account, foreign_key: :account_uuid end
Requirements
The including class must provide ActiveModel::Attributes, which backs the foreign key
attribute. Passing required: true additionally needs ActiveModel::Validations.
Event provides both.
Use on materialization models
A projection's state is an instance of its materialization model, so a materialization model
that declares refers_to offers interpretation blocks the same API events do — the reference
reads and writes identically on both sides of the block:
class CustomerSnapshot
include ActiveModel::Model
include ActiveModel::Attributes
include Funes::Associations
refers_to :customer, class_name: "Examples::Customer"
end
interpretation_for Deposit::Created do |state, event, _at|
state.customer = event.customer
state
end
Only the id travels in attributes, so the reference survives the rebuild that materialize!
performs. This works for both virtual materialization models and those persisted through
persist_materialization_model_with.
For an ActiveRecord-backed materialization model, declare a regular belongs_to association
instead. That is the idiomatic tool there, and it brings preloading and inverse_of with it.
refers_to can work on such a model, but only when the foreign key is already a column in the
database — and even then belongs_to remains the better choice. Without that column the failure
is late and misleading: interpretation blocks read and write the reference correctly and the
foreign key is populated, then materialize! feeds attributes into an upsert and raises
ActiveModel::UnknownAttributeError: unknown attribute 'customer_id' for CustomerSnapshot.
naming the attribute without mentioning the missing column or the refers_to call that created
it, so the error surfaces far from its cause.
Namespaced models
class_name must be fully qualified, e.g. class_name: "Examples::Customer". It is
resolved lazily, on the first load by id — assigning a record caches it directly, so a wrong
class_name surfaces only when a freshly built object reads the reference, not when it is
assigned.
Defined Under Namespace
Modules: ClassMethods