ActiveFedora in depth
What are your goals?
What is ActiveFedora?
- Persistence
- Querying
- Relationships
- Defining and Managing Attributes
- Callbacks
- Validation
- Serializing (to JSON)
- Deserializing (from nested attributes)
Where did it come from?
Whole cloth
https://github.com/projecthydra/active_fedora/commit/29d0c09eef32dcda4120a5bb82c23596d33363dc
@blame flyingzumwalt
zoia -- flyingzumwalt reminds jcoyne of a bug in Rubydora
Early Contributors (in order of apperance)
- Matt Zumwalt
- Rick Johnson
- John Scofield
- Bess Sadler
- Molly Pickral
- Naomi Dushay
So, jcoyne, what's your involvement?
- Got on board Summer 2011. Version 2.3.3
- Coming from a Rails background, I found ActiveFedora confusing
- Goal: Harmonize ActiveFedora with ActiveRecord as much as possible
- Make it easier to learn. Simplify the interaction.
How did we do?
- It is a lot more like ActiveRecord
- Much more functionality has been added
- It is more complicated now
Best improvements (according to jcoyne)
- Autosave Associations
- Easy to describe indexing
- Associations can be extended (e.g. indirect_container)
- Anything else?
Worst parts (according to jcoyne)
- RDF doesn't map 1:1 to RDBMS, so things like has_and_belongs_to_many are confusing
- Massive API, unclear how certain options (e.g.
inverse
) work - Anything else?
What does the code look like?
/lib/active_fedora/base.rb#L27-L54Compare to ActiveRecord
/lib/active_record/base.rb#L269-L316How does ActiveRecord differ from ActiveFedora?
- Fedora is schemaless. We have to encode the properties using ActiveTriples or OM.
- ActiveFedora persists/queries two stores: Solr and Fedora. (Fedora doesn't have good query capabilities)
- ActiveFedora speaks LDP instead of SQL.
- Fedora is much slower than SQL.
- ActiveFedora doesn’t support transactions (yet).
Multiple inheritance architecture
What happens when we call save() on an ActiveFedora object?
ActiveFedora::Base.ancestors
=> [ActiveFedora::Base, ActiveFedora::LoadableFromJson, ActiveFedora::Versionable, ActiveFedora::Attributes::PrimaryKey, ActiveFedora::Attributes::Serializers, ActiveFedora::Attributes, ActiveModel::ForbiddenAttributesProtection, ActiveModel::Dirty, ActiveFedora::AttributeMethods::Dirty, ActiveFedora::AttributeMethods::Write, ActiveFedora::AttributeMethods::Read, #, ActiveFedora::AttributeMethods, ActiveModel::AttributeMethods, ActiveTriples::Reflection, ActiveFedora::Base::GeneratedPropertyMethods, ActiveTriples::Properties, ActiveFedora::FedoraAttributes, ActiveFedora::InheritableAccessors, ActiveFedora::AttachedFiles, ActiveFedora::Serialization, ActiveModel::Serializers::JSON, ActiveModel::Serialization, ActiveFedora::Reflection, ActiveFedora::NestedAttributes, ActiveFedora::AutosaveAssociation, ActiveFedora::Associations, ActiveFedora::Validations, ActiveModel::Validations::HelperMethods, ActiveModel::Validations, ActiveModel::Validations::Callbacks, ActiveSupport::Callbacks, ActiveFedora::Callbacks, ActiveModel::Conversion, ActiveFedora::Scoping::Named, ActiveFedora::Scoping::Default, ActiveFedora::Scoping, ActiveFedora::Indexing, ActiveFedora::Persistence, ActiveFedora::Core, Object, PP::ObjectMixin, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]
Multiple inheritance architecture
What happens when we call save() on an ActiveFedora object?
Querying
Querying means to define a scope (an ActiveFedora::Relation
)
The most common scope is all
> Book.all
=> []
> Book.all.class
=> ActiveFedora::Relation
ActiveFedora::Relation
/lib/active_fedora/relation.rb
Has a @klass
and @values
Hash (:where, :order, :limit, :offset
)
These are the constraints of a Relationship
Value accessors are here: /lib/active_fedora/relation/query_methods.rb#L4-L47
ActiveFedora::Relation
In our book example all the values are empty
> Book.all.where_values
=> []
> Book.all.order_values
=> []
> Book.all.limit_value
=> nil
> Book.all.offset_value
=> nil
How does it load the records?
- Delegates to
to_a
lib/active_fedora/relation.rb line 53 - calls
find_each
lib/active_fedora/relation/finder_methods.rb line 122 - calls
find_in_batches
lib/active_fedora/relation/finder_methods.rb line 150 - which queries solr and then
load_from_fedora
lib/active_fedora/relation/finder_methods.rb line 188
You can refine the scope
By usingwhere()
> Book.where(title: 'foo').where_values
=> ["title_tesim:foo"]
and chain scopes:
> Book.where(title: 'foo').
where(author: 'Betty').where_values
=> ["title_tesim:foo", "author_tesim:Betty"]
How do associations work?
# lib/active_fedora/associations.rb
def belongs_to(name, options = {})
raise "You must specify a property name for #{name}" if !options[:property]
Builder::BelongsTo.build(self, name, options)
end
Builder creates a Reflection
Book.create_reflection(:has_and_belongs_to_many, 'pages', { property: 'hasPages'}, Book)
# Adds a reflection:
Book.reflections
#=> { 'pages' => <#AssociationReflection
# @options={ :property => :has_pages }
# @macro=:has_and_belongs_to_many ...> }
Builder creates the accessors
def "#{name}_id"
association(name).id_reader
end
def "#{name}_id="(id)
association(name).id_writer(id)
end
Builder creates the accessors
def "#{name}"
association(name).reader
end
def "#{name}="(value)
association(name).writer(value)
end
The association method
def association(name)
reflect = self.class.reflect_on_association(name)
reflect.association_class.new(self, reflect)
end
The SingularAssociation
- id_reader
- id_writer
- reader
- writer
The CollectionAssociation
- ids_reader
- ids_writer
- reader (returns a proxy)
- writer
class Book < ActiveFedora::Base
belongs_to :library, property: :has_constituent
end
class Library < ActiveFedora::Base
has_many :books
end
CollectionProxy
library = Library.new
library.books.build
library.books.build
library.books.class
=> ActiveFedora::Associations::CollectionProxy
library.books
=> [#<Book pid: nil, library_id: nil>, #<Book pid: nil, library_id: nil>]
CollectionProxy is a Relation
Challenges with Associations
- Differences between
has_and_belongs_to_many
in ActiveRecord and ActiveFedora - Necessitating has_many is inverse of
belongs_to
orhas_and_belongs_to_many
- Also, polymorphic associations:
class_name: 'ActiveFedora::Base'
- Bidirectional assocations
inverse_of
on HABTM
Attribute tracking
ActiveFedora keeps track of attributes in memory, like ActiveRecord. This enables dirty tracking on a per-attribute basis.
Challenge: Track attributes regardless of whether they are persisted on the RDF resource representing the object, or in a NonRDFSource subnode (f.k.a. Datastream)