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:
users
, they are loaded from the database (probably because of the include
/order
combination), so you may be missing User
records that have not yet been saved.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)
.