Assigning a collection to has_many :through
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
- Many-to-many Dance-off! by Josh Susser
- Why aren't join models proxy collections? by Josh Susser
- Collection assignment to a has_many :through on Ruby-Forum