ActiveRecord Associations

Associations in Rails declaratively express relationships between model classes.  Although they appear as macro-like statements in model classes, they are actually methods called on the classes where they are used.  It can be unclear to beginning Rails developers what type of objects are returned by association with these methods.  The objects returned tend to behave like regular Ruby objects and arrays, so it can be easy to think of them actually as arrays or direct model classes; however, it is important to know that these objects are actually associations.

has_many :somethings # would actually be an instance of HasManyAssociation

The superclass of all associations is AssociationProxy, which holds the basic structure and functionality of all association proxies.  The proxy object starts out by undef_method most instance methods and relies on sending most method calls to the target of the proxy via method_missing.

object.somethings.respond_to? :proxy_owner # => true proves that somethings is a proxy.

Warning about Associations
Don’t name associations the same as ActiveRecord::Base instance method names.  When an association is created, a method with that name is added to the model, which would override the inherited method (BAD).

One-to-Many Relationships
Consider the following…

class User < ActiveRecord::Base
has_many :timesheets
has_many :expense_reports
end

class Timesheet < ActiveRecord::Base
belongs_to :user # makes it possible to reference the user to which this belongs
end

class ExpenseReport < ActiveRecord::Base
belongs_to :user  # makes it possible to reference the user to which this belongs
end

# When the code is evaluated, Ruby/Rails uses some metaprogramming techniques to add code to your models dynamically add code to your models.  Proxy objects are created to let you manipulate the relationship easily.

dan = User.create :login => ‘demmons’, :password => ‘xajrwgsdf3′, :password_confirmation => ‘xajrwgsdf3′, :email => ’someone@somewhere.com’

dan.timesheets # => ActiveRecord::StatementInvalid: … no such column: timesheets.user_id…
Remember – you have to add a foreign key column to the timesheets and expense_reports tables.
dan = User.find(1) # => <User:0×35dc85a … >
dan.timesheets << Timesheet.new # => [<Timesheet:0x1234 .. @new_record=true, @attributes={}>]
dan.timesheets # => [<Timesheet:0x1234 .. @new_record=true, @attributes={}>]

Adding Associated Objects to a Collection

Adding an object to a has_many collection saves the object automatically unless the parent object is not yet stored in the collection.

dan.timesheets.reload # re-fetches the attributes of an object from the database…

The << method set the user_id attribute on the timesheet automatically for you when the item was added.
The << method takes one or more association objects, and adds each object to the collection.

AssociationCollection Methods

HasManyAssociation < AssociationCollection
HasAndBelongsToManyAssociation < AssociationCollection

<<(*records)
create(attributes = {})

Both methods will ad either a single object or many, depending on the input. << is transactional, and create is not.
The << method will trigger the :before_add and :after_add callbacks, but the create method does not.  The create method returns the new instance, while the << method returns the association for easy chaining.  If any records fail to save, the << method returns false.

clear # removes all records from the association by clearing the fk.  If the association is configured with the :dependent option set to :delete_all, then clear calls destroy on each one.  The clear method is transactional.

delete(*records) and delete_all # remove associations transactionally.
# Note: delete_all will first load all of the objects in the collection from the database before each SQL UPDATE
# The :dependent option on the association defaults to :nullify.  The fk will be updated to null, but won’t be deleted.
# If the association is configured with :dependent => :delete_all or :destroy, the records will be deleted.

destroy_all # begins a transaction & calls destroy on each element, invoking individual DELETE sql

length # returns size of the collection by loading it and calling size on the array.
# length.zero? is more efficient than calling .emtpy on the array directly.

replace(other_array) # first deletes objects in the current collection that are not in other_array, and then concat each item in the other_array

size # returns the size of the array if it has been loaded (otherwise select count(*) query is used).
# use length instead if you will need to access the elements of the collection later anyway.

sum(column, *options) # Computes a summed value on the attribute using SQL.  Requires :group option.

uniq # Iterates over the collection and creates a Set with the unique values.  id is used for equality.

~ by bandwagonblog on January 31, 2009.

Leave a Reply