Skip to content

Instantly share code, notes, and snippets.

@blrobin2
Created January 6, 2021 15:16
Show Gist options
  • Save blrobin2/8738a41c34eb654200b44ab75a02cfdd to your computer and use it in GitHub Desktop.
Save blrobin2/8738a41c34eb654200b44ab75a02cfdd to your computer and use it in GitHub Desktop.
A Rails model mix-in for handling many-to-many updates and nested attributes
module AutosaveAssociatedManyToMany
extend ActiveSupport::Concern
# When using a combination of autosave and accepts nested_attributes_for for a many-to-many association
# By default, Rails always creates new records
# If you have unique indexes on the association keys on your bridge table, you'll just get errors
# By defining a matcher, you provide a way to determine whether a "new" record is actually an existing record
# By defining an updater, you provide a way to update values on a many-to-many (changing an association to a new field
# or updating any additional fields on the bridge table)
#
# Best utilized by calling within autosave_associated_records_for_{association}, an autosave hook that Rails
# implements, where {association} is the same name as the association you wish to handle autosave
def handle_many_to_many_autosave(association, matcher, updater = ->(_record, _record_update) {})
raise NotImplementedError "association #{association} not defined for #{self.name}" unless self.respond_to?(association)
unless matcher.respond_to?(:call) && matcher.arity == 2
raise ArgumentError "matcher must respond to :call and accept old and new record arguments"
end
unless updater.respond_to?(:call) && updater.arity == 2
raise ArgumentError "updater must respond to :call and accept old and update record arguments"
end
records = self.send(association)
existing = records.reject { |record| record.id.nil? }
incoming = records.select { |record| record.id.nil? }
# Remove ones that are in existing but not new
existing.select { |record| incoming.none? { |new_record| matcher.call(record, new_record) } }.map(&:destroy)
# Add ones that are in new but not existing
to_add = incoming.select { |new_record| existing.none? { |record| matcher.call(record, new_record) } }
# Update ones that are in both and if the count has been updated
existing.each do |record|
update = incoming.find { |new_record| matcher.call(record, new_record) }
next unless update
updater.call(record, update)
record.save!
end
self.send(association) << to_add
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment