ActiveFedora Relationships
In a recent sprint on ActiveFedora Aggregations I was working on relationships in ActiveFedora and felt like I should catalogue my understanding of how they work.
Entry Point
Let’s start with the has_many
method, as used below:
class Collection < ActiveFedora::Base
end
class MyObject < ActiveFedora::Base
has_many :collections, :class_name => "Collection"
end
That has_many method is defined in the associations module which is included into ActiveFedora::Base here: lib/active_fedora/associations.rb#L142-L144
The relevant code looks like this:
def has_many(name, options={})
Builder::HasMany.build(self, name, options)
end
As you can see, when you call has_many on the ActiveFedora class it delegates down to the HasMany builder.
Builder
Builders are responsible for setting up a Reflection (a registry of the metadata for an association) and defining readers/writers on the class it was called on. The class for HasMany is defined here: lib/active_fedora/associations/builder/has_many.rb
The relevant code looks like this:
def self.build(model, name, options)
reflection = new(model, name, options).build
define_accessors(model, reflection)
define_callbacks(model, reflection)
reflection
end
def build
reflection = super
configure_dependency
reflection
end
When the super call is inlined it looks like this:
def self.build(model, name, options)
reflection = new(model, name, options).build
define_accessors(model, reflection)
define_callbacks(model, reflection)
reflection
end
def build
configure_dependency if options[:dependent] # see https://github.com/rails/rails/commit/9da52a5e55cc665a539afb45783f84d9f3607282
reflection = model.create_reflection(self.class.macro, name, options, model)
configure_dependency
reflection
end
A reflection is created from the calling model (MyObject
) which allows you to
obtain all relevant information about the association by calling
MyObject.reflections
After that it defines accessors and callbacks. Let’s look at the accessors created.
Accessors
Accessors are defined for has_many in a function that looks like this:
def self.define_readers(mixin, name)
super
mixin.redefine_method("#{name.to_s.singularize}_ids") do
association(name).ids_reader
end
end
If you inline the super call:
def self.define_readers(mixin, name)
mixin.send(:define_method, name) do |*params|
association(name).reader(*params)
end
mixin.redefine_method("#{name.to_s.singularize}_ids") do
association(name).ids_reader
end
end
This makes it so if you do has_many :plums
it will define an instance method
called plums
on the object which then calls the association’s reader
method.
The reader method:
# Implements the reader method, e.g. foo.items for Foo.has_many :items
# @param opts [Boolean, Hash] if true, force a reload
# @option opts [Symbol] :response_format can be ':solr' to return a solr result.
def reader(opts = false)
if opts.kind_of?(Hash)
if opts.delete(:response_format) == :solr
return load_from_solr(opts)
end
raise ArgumentError, "Hash parameter must include :response_format=>:solr (#{opts.inspect})"
else
force_reload = opts
end
reload if force_reload || stale_target?
@proxy ||= CollectionProxy.new(self)
end
Effectively what happens is when you call #plums
you get back a
CollectionProxy object, which is defined here:
lib/active_fedora/associations/collection_proxy.rb
On this object are methods you might expect - #find
, #first
, #last
, etc.
If you wanted to change what methods are available on a set of related objects,
that’s where you’d change things.
And that’s it
That’s all there is to it. A builder gets called when you do things like
has_many
or belongs_to
, that builder stores the metadata about the
association, it defines a reader which delegates down to an association object
(such as
lib/active_fedora/associations/has_many_association.rb),
and the reader often defines a proxy object to handle actions on the associated
items as a whole.
For me the biggest trouble I had was tracing the path. To do so, just start at the associations module and work your way down.