Associations
Associations
NoBrainer supports belongs_to
and has_many
associations.
NoBrainer is different from other ORMs: has_many
associations are not writable
because writable has_many
associations are leaky abstractions and provide hard to
understand semantics. Therefore there is no has_and_belongs_to
associations.
Remember that NoBrainer never saves a model instance under the covers.
In this section, the owner refers to the model where the association is declared, and the target refers to the other side of the association.
belongs_to Association
The belongs_to
syntax is the following: belongs_to :target, options
The following describes the different options belongs_to
accepts:
:class_name
: the target class name. Defaults toTarget
.:foreign_key
: the foreign key to use. Defaults to#{target_name}_#{primary_key}
.:foreign_key_as
: the alias for the foreign key. Defaults tonil
.:index
: when true, the foreign key field gets an index declared to speed to the correspondinghas_many
association. Defaults tonil
.:polymorphic
: when true, the model can belongs to any models, following the Ruby On Rails Polymorphic Associations convention.:primary_key
: the primary key to use on the target. Defaults to:id
.:required
: a shorthand for:validates => { :presence => true }
.:uniq
(or:unique
): a shorthand to specify a uniquness validation on the foreign key.:validates
: passes a validation totarget
, and nottarget_id
. Useful to provide a presence validation.
The following describes the behavior of belongs_to
associations:
owner.target
looks up the target instance by performingTarget.find(owner.foreign_key)
. The result is cached regardless if the target is found or not.owner.target=(value)
setsowner.foreign_key = value.primary_key
, and caches the value.owner.foreign_key=(value)
sets the foreign key and kills the target cache.
NoBrainer will always insert an after_validation
callback to check that if there
is a target set, then it must be persisted?
. If the target is not persisted,
NoBrainer will raise a NoBrainer::Error::AssociationNotPersisted
exception.
You can read more about how presence validations are handled on belongs_to associations in the validations section.
has_many Association
The has_many
syntax is the following: has_many :targets, options
The following describes the different options has_many
accepts:
:primary_key
: the primary key to use. Defaults to the owner’s primary key.:foreign_key
: the foreign key that the targets use. Defaults to#{owner_name}_#{primary_key}
.:class_name
: the targets class name. Defaults toTarget
.:dependent
: configure the destroy behavior further explained below. Defaults tonil
.:through
: See thehas_many through
association below.:scope
: A lambda that evaluates to a criteria which gets applied to the query. This lambda is evaluated in the context of theTarget
class, which means that using named scoped defined onTarget
is possible.
The dependent option tells what to do when destroying an owner that has many
targets with a before_destroy
callback. The different dependent values are:
nil
: do nothing:destroy
:destroy_all
the targets.:delete
:delete_all
the targets.:nullify
:update_all
the targets’ foreign keys tonil
.:restrict
: raises aNoBrainer::Error::ChildrenExist
if a target still exists.
When performing the dependent destroy logic, the targets criteria is run
unscoped
(without the any declared default_scope
).
The following describes the behavior of has_many
associations:
-
The
has_many
association is read only. NoBrainer makes no attempts whatsoever in collecting targets as they get created withTarget.create()
. This also mean that you cannot usepost.comments.build
. Rather, you should useComment.create(:post => post)
and have a presence validation on post. -
Loading targets through
instance.targets
will automatically set their matchingbelongs_to
associations toinstance
, with or without eager loading. -
instance.targets
returns the criteriaTarget.where(foreign_key => owner.primary_key)
, which is cached. This means that you will always get the same instance of criteria on a given instance, which will cache enumerated documents. When a custom:scope
is defined, the custom scope is evaluated in the context ofTarget
and added to the criteria. Note that usingunscoped
has no effect on the custom scope.
has_many
associations leverage the cache, illustrated with the following
example. You can read more about the caching behavior in the caching
section.
has_many through Association
The has_many
syntax is the following: has_many :targets, :through => :association
.
targets
must be a defined association on the through association
. You may
go through any associations. No other options are supported.
The implementation of has_many
through is essentially a thin wrapper around the
eager loading functionality, which implies that reading a has_many
through
association will not yield a criteria, but a plain unmodifiable array which gets cached.
The following show an example of using a has_many
through association:
has_and_belongs_to_many Association
NoBrainer will never support such association. Nevertheless, you may create your own join table as such:
has_one Association
The has_one
association is a has_many
with the following differences:
- The target name is assumed to be singular instead of plural.
- Reading the target of a
has_one
association returns a single document, unlike ahas_many
which returns an array of documents. Nevertheless, NoBrainer will emit warnings if your association has more than one element.
Note that the dependent
option behaves like the has_many
association one. In other words, all the targets matching the foreign key of the
owner will be subject to the destroy
behavior, not just the first one.
The has_one through
association follow the same rules as the has_many through
association.
accept_nested_attributes_for
This will never be implemented since has_many
associations are read only. This
sort of feature belongs in a separate gem anyway because it’s a crazy feature.
Reflection
You can retrieve the association declarations with Model.association_metadata
.
It returns a hash of the form {target_name => metadata_instance}
.
Association instances can be retrieved on a model instance with
model_instance.associations[metadata_instance]
.