tl;dr: Using has_many
associations with a :through
option can lead to lost or duplicate records. You should avoid them, or only use them to read records.
Consider this:
class User < ActiveRecord::Base
end
class Party < ActiveRecord::Base
has_many :invitations
has_many :users, through: :invitations, include: :user, order: 'users.name'
end
class Invitation < ActiveRecord::Base
belongs_to :party
belongs_to :user
after_create :send_invite
def send_invite
other_user_names = party.users.collect(&:name)
message = "You've been invited. Also coming: #{other_user_names.join(', ')}."
deliver_email(user.email, message)
end
end
When creating a party and lots of invitations, you want to send an e-mail to each user and tell them who else is coming.
Unfortunately, accessing party.users
may or may not give you the list of users that you expect:
- When accessing
users
, they are loaded from the database (probably because of theinclude
/order
combination), so you may be missingUser
records that have not yet been saved. - When creating invitation records via a nested form, you may even end up creating duplicate
Invitation
records for some reason (we did not entirely find out why, and we eventually did not care).
This happened on a Rails 3.2 application and is likely to happen on Rails 4 applications as well. Your best bet is to stay away from using them. Most times, it's good enough to actually loop associated records like this:
class Party < ActiveRecord::Base
has_many :invitations
def users
invitations.reject(&:marked_for_destruction?).map(&:user).flatten
end
end
That way you no longer can say party.users << some_user_object
to create invitation records for users, but you probably should not have done that anyway (just use party.invitation.create(user: some_user_object)
.