Ruby on Rails 3 Model (Many-to-Many Association)

Many-to-many Association

Scaffold

Vendors have 0 or many clients (or vice versa)

rails generate scaffold Vendor vendor_name:string
rails generate scaffold Client client_name:string

Create template code to build the association table defining the many-to-many relation

rails generate migration CreateClientsVendors

Association Table

Rails use lexical order to determine the name of the association table (if name1 < name2, the table will name as name1_name2)

Since "clients" is smaller than "vendors", the name of the association table will be "clients_vendors"

Edit the model to create the association table

db/migrate/20090930171701_create_clients_vendors.rb
class CreateClientsVendors < 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

    # Add table index
    add_index :clients_vendors, [:client_id, :vendor_id], :unique => true

  end

  def self.down
    remove_index :clients_vendors, :column => [:client_id, :vendor_id]
    drop_table :clients_vendors
  end
end

Create the table

rake db:migrate

Edit the Model

vendor.rb
class Vendor < ActiveRecord::Base
   # define many-to-many relation
   has_and_belongs_to_many :clients
end
cleint.rb
class Client < ActiveRecord::Base
   has_and_belongs_to_many :vendors
end

(Optional) To override the name of the association table

   has_and_belongs_to_many :clients, :join_table="vendors_link_clients"
   ...
   has_and_belongs_to_many :vendors, :join_table="vendors_link_clients"

Access Associated Objects

Inside a Model
class Vendor < ActiveRecord::Base
  ...
  def
    clients = self.clients
    clients.each do |client|
       client.client_name
       ...
    end
  end
  ...
end
Inside a Controller
vendor = Vendor.find(params[:id])

# Access the associated clients
client = vendor.clients

Adding Business Logic for the Vendor & Client Association

Add 2 Actions for

  • /vendors/1/register to register a client to a vendor
  • /vendors/1/clients to list all clients of a vendor
    vendors_controller.rb
      # GET /vendors/1/register?clientid=1
      # Register a client to a vendor
      def register
    
        @vendor = Vendor.find(params[:id])
        @client = Client.find(params[:clientid])
    
        # Register a client if it is not registered already
        unless @vendor.registered?(@client)
          # Add vendor to a client's vendor list
          @client.vendors << @vendor
          flash[:notice] = 'Client register with the vendor successfully'
        else
          flash[:error] = 'Client already registered'
        end
        # Redirect to the action "clients"
        # Redirect to /vendors/1/clients for vendor id 1
        redirect_to :action => :clients, :id => @vendor
      end
    
      # Display all clients of a vendor
      # GET /vendors/1/clients
      def clients
        @clients = Vendor.find(params[:id]).clients
      end
    
    

Add Model method

app/models/vendor.rb
class Vendor < ActiveRecord::Base
 has_and_belongs_to_many :clients

 def registered?(client)
    self.clients.include?(client)
 end

end

Add view to display vendor's client

app/views/vendors/clients.html.erb
<h1>Listing vendor's client</h1>

<table>
  <tr>
    <th>Vendor client name</th>
  </tr>

<% @clients.each do |client| %>
  <tr>
    <td><%= client.client_name %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New vendor', new_vendor_path %>

Functions

Add association

vendor = Vendor.find(params[:id])
client = Client.find(params[:clientid])

# Add vendor to a client
client.vendors << @vendor

Who are not on a vendor's client list

Client.find(:all) - vendor.clients

Remove a particular client from a vendor

vendor = Vendor.find(params[:id])
client = Client.find(params[:clientid])
vendor.clients.delete(client)

Security

Security risk:

vendor = Vendor.find(params[:id])
# Access any client (not just one belong to the vendor
client = Client.find(params[:clientid])

More secure: limit access to the client of a vendor only

vendor = Vendor.find(params[:id])
client = vendor.clients.find(params[:clientid])