Ruby on Rails 3 Model Association

Rails Mdoel Association

Many-to-1 association

class Item < ActiveRecord::Base
  # Associate an item to exactly one order
  belongs_to :order
end
  • item table contains a column order_id referencing the orders table

1-to-Many association

class Order < ActiveRecord::Base
  has_many :items, :dependent => :destroy
end
  • :dependent enable the cascade delete

Create and save an associated object

@item = @order.items.create(params[:item])

Initialize an associated object (without saving to DB)

@item = @order.items.build(...)

Delete parent and all children (Cascade delete)

@order.destroy
Association Type Example Code
belongs_to An item belongs to an order

A profile belongs to an User

1-to-1 association
class Item < ActiveRecord::Base
  # An item belongs to an order
  belongs_to :order
end

"items" table has an order_id column referencing the orders

Assign an object to a belongs_to association does not save the object

has_one User has one profile

1-to-1 association
class User < ActiveRecord::Base
  has_one :profile
end

"profiles" table has an user_id column referencing the users

Assign a new object to a has_one association will save the object

has_many Order has many items

1-to-many association
class Order < ActiveRecord::Base
  has_many :items
end

"items" table has an order_id referencing the orders

has_and_belongs_to_many Vendors has many clients and vice versa

Many-to-many relationshop through an association table
class Client < ActiveRecord::Base
  has_and_belongs_to_many :vendors
end

class Vendor < ActiveRecord::Base
  has_and_belongs_to_many :clients
end

The mapping is defined in an association table that need to be created manually

DB migration
class CreateClientsVendorsJoinTable < ActiveRecord::Migration
  def self.up
    # Create the association table
    create_table :clients_vendors, :id => false do |t|
      t.integer :client_id, :null => false
      t.integer :vendor_id, :null => false
    end
  end
end
has_one :through A user has a profile and a profile has a profile_view.



Hence a user has a profile_view through profile

1-to-1 association
class User < ActiveRecord::Base
  # An account has one profile
  has_one :profile

  # An account has one profile_view through profile
  has_one :profile_view, :through => :profile
end

class Profile < ActiveRecord::Base
  belongs_to :user

  # A profile has one profile_view
  has_one :profile_view
end

class ProfileView < ActiveRecord::Base
  belongs_to :profile
end
has_many :through Vendor has many orders

Client has many orders



A vendor is associated with a client through an order

Many-to-many association
class Vendor < ActiveRecord::Base
  has_many :orders
  has_many :clients, :through => :orders
end

class Order < ActiveRecord::Base
  belongs_to :vendor
  belongs_to :client
end

class Client < ActiveRecord::Base
  has_many :orders
  has_many :clients, :through => :orders
end

The orders table has an client_id and vendor_id reference the clients and vendors tables

@vendor.clients = @clients will add/remove rows in the order table

  • belongs_to and has_one are both 1-to-1 association. The major difference is which table have the foreign key.

1-to-1 modeling

class User < ActiveRecord::Base
  has_one :profile
end

class Profile < ActiveRecord::Base
  belongs_to :user
end

Migration code creating the association

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string  :name
      t.timestamps
    end

    create_table :profiles do |t|
      t.string  :profile_name
      t.references :user
      t.timestamps
    end
  end

  def self.down
    drop_table :users
    drop_table :profiles
  end
end

Self-Join

Self-Joining table

Model
# Joining with itself by the manager_id
# To find the manager and the reporting staff of an employee
class Employee < ActiveRecord::Base
  has_many :members, :class_name => "Employee", :foreign_key => "manager_id"
  belongs_to :manager, :class_name => "Employee"
end
# Access the manager
emp.manager

# Acess the team members
emp.members

Rails Model Lazy & Eager Loading

  • Rails use lazy loading by default in dealing with association
  • Associated objects are loaded when first access. Accessing 10 associated objects results in 10 SQL calls
  • Eager loading may be more efficient sometimes to load all objects at once with 1 SQL call

To use eager loading for the specific association

# Eager load clients also
@vendors = Vendor.all :include => [:clients]
Vendor.find(:all, :include => [ :clients, :contacts ])

Ro use eager loading for multiple level of table association

:include => [ :tbla, :tblb, { :tblb1 => { :tblb1a => :tblb1a1 } } ]

Apply a where clause in eager loading

Group.includes([:members, :contacts]).where(['members.banned = ?', false]).all

Reload Association Object

Associated objects of a model object is cached until it is refresh

my_order.items     # Retrieve items form database
                   # Further items access from my_order will be retrieved from a cache
my_order.items(true)  # Refresh items from database again

Scoping

To access model in the same module, no modifier is needed

class Item < ActiveRecord::Base
  # Associate an item to exactly one order
  belongs_to :order
end

To access model in different modules

module WebApp
  module Business
    class Order < ActiveRecord::Base
       has_one :item,
        :class_name => "WebApp::Common::Item"
    end
  end

  module Common
    class Item < ActiveRecord::Base
       belongs_to :order,
        :class_name => "WebApp::Business::Order"
    end
  end
end

Rails Polymorphic Associations

With polymorphic associations, rows in a table can be associated with different tables

  • For example, a single documents table is used to store documents for both vendors & clients table
  • The document table can have one row associate with the vendor table and another with the client table
    class Document < ActiveRecord::Base
      belongs_to :content, :polymorphic => true
    end
    
    class Vendor < ActiveRecord::Base
      has_many :documents, :as => :content
    end
    
    class Client < ActiveRecord::Base
      has_many :documents, :as => :content
    end
    

To access the document

@vendor.documents
@client.documents

doc = @vendor.documents[0]

To access the object associated with the document

doc.content                  # return vendor or client

"Document" Model

class CreateDocuments < ActiveRecord::Migration
  def self.up
    create_table :documents do |t|
      t.text :doc_content
      t.references :content, :polymorphic => true
      t.timestamps
    end
  end
  ...
end

t.references :content creates the following 2 columns in the documents table

   t.integer :content_id
   t.string  :content_type

content_type identify the table associated with

content_id reference the id in the associated table

Adding Custom Methods to association

Custom methods

class Group < ActiveRecord::Base
  has_many :members do
    def find_by_something(something)
      ...
    end
  end
end

Group.first.members.find_by_something(...)

Have the custom methods shareable amount different associations

module FindBySomething
  def find_by_something(name)
    ...
  end
end

class Group < ActiveRecord::Base
  has_many :members,  :extend => FindBySomething
  has_many :premiums, :extend => [FindBySomething, ...]
end

Association Options

Limit the association with a where condition

class Group < ActiveRecord::Base
  has_many :confirmed_members, :class_name => 'Member', :conditions => ['confirmedd = ?', true]
  • class_name defines the name of the model class

Use select to retrieve specific columns

  belongs_to :member, :select => "name, profile, group_id"
  • Include any foreign key columns needed to build the association used by your model objects

Eager loading all confirmed members

Group.find(:all, :include => :confirmed_members, :order => 'id DESC', :limit => 50)

belongs_to option

Options Description
class_name Model class name
conditions conditions
select select fields
foreign_key DB column name for the foreign key
primary_key Primar key column name
dependent Define cascade delete
include Specify eager loading
polymorphic polymorphic association
readonly read only
validate whether to validate the object
autosave whether to auto save the object
touch Treat the object is updated

has_and_belongs_to_many

Options Description
class_name Model class name
join_table The join table
foreign_key DB column name for the foreign key
association_foreign_key Receiver side foreign key
conditions conditions
order Order by
uniq Return unique
finder_sql Set Finder SQL
counter_sql Set counter SQL
delete_sql Set delete SQL
insert_sql Set insert SQL
extend Extend association
include Eager loading
group Group by
having Having
limit Limit
offset offset in SQL
select select fields
readonly read only
validate whether to validate the object
autosave whether to auto save the object

has_many

Options Description
class_name Model class name
conditions conditions
foreign_key DB column name for the foreign key
order Order by
primary_key Primar key column name
dependent Define cascade delete
finder_sql Set Finder SQL
counter_sql Set counter SQL
extend Extend association
include Eager loading
group Group by
having Having
limit Limit
offset offset in SQL
select select fields
readonly read only
validate whether to validate the object
autosave whether to auto save the object
as Specify polymorphic
through Specify a join model
uniq Remove duplicate
source Override source association name
source_type Type of source association
inverse_of Specify the name of the belongs_to association

has_one

Options Description
class_name Model class name
conditions conditions
foreign_key DB column name for the foreign key
order Order by
primary_key Primar key column name
dependent Define cascade delete
include Eager loading
select select fields
readonly read only
validate whether to validate the object
autosave whether to auto save the object
as Specify polymorphic
through Specify a join model
source Override source association name
source_type Type of source association
inverse_of Specify the name of the belongs_to association