Assigning a collection to has_many :through

written by damien on October 28th, 2007 @ 06:45 PM

This first article about Ruby on Rails is quite technical, it explains how to handle assignment of collections to a has_many :through relationship, as it is not the easiest kind of relationship to handle in Rails. I have used some references and made some improvements to the code so that it suits best my needs, but it may not be what you want, don't hesitate to post a comment to suggest some improvements or to ask for more :)

As has_many :through relationship is not handled using proxy collections like has_and_belongs_to_many relationships, we need to be a little tricky to make collection assignments. If you google a little to find a solution, you may find this subject on Ruby-Forum. This solution works well, but it will automatically save your data into the database. If like me you’re an “update_attributes addict”, you would like the affectation not to write into the database until the model is validated. So here is how I handle this assignment...

class Account < ActiveRecord::Base
  has_many :memberships, :dependent => :destroy
  has_many :users, :through => :memberships

  attr_reader :old_users
  after_save :save_users

  # “Reminds” the old users used
  # and replaces them by the new ones
  def users=(users)
    @old_users = Array.new
    self.users.each { |user| @old_users << user }

    self.users.clear
    users.each { |user| self.users << user }
  end

  private
  def save_users
    Membership.set_users_for_account self, self.users
    memberships.reset
    users.reset
  end
end
class Users < ActiveRecord::Base
  has_many :memberships, :dependent => :destroy
  has_many :accounts, :through => :memberships
end
class Membership < ActiveRecord::Base
  belongs_to :account
  belongs_to :user

  def Membership.set_users_for_account(account, users)
    old_users = account.old_users
    
    # at account creation we would add the users twice otherwise
    unless old_users.nil?
      delete_from account,  (old_users - users)
      add_to      account,  (users - old_users)
    end
  end
  
  private
  def Membership.delete_from(account, users)
    unless users.empty?
      delete_all ['account_id = ? and user_id in (?)',
                   account.id, users.collect { |u| u.id }]
    end
  end
  
  def Membership.add_to(account, users)
    unless users.empty?
      self.transaction do
        users.each do | user|
          create! :account => account, :user => user
        end
      end
    end
  end
end

This way, when you will make an update_attributes to your account giving a collection of users in parameters (and other account attributes eventually), the list of users associated to the account won’t be updated if the model is not valid.

I’m not the best Ruby on Rails programmer in the world, I would be glad to see your comments on how I could improve this code or even on how I could improve the way I handle collections assignment with has_many :through relationships.

This Article was written using Rails 1.1.6, this way of handling has_many :through relationship doesn't work with Rails 2.0 at creation of object

References

Comments are closed

Options:

Size

Colors