Created
January 6, 2021 15:16
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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