Я решил эту проблему так, чтобы обезьяна исправила accepts_nested_attributes_for
, чтобы получить параметр: key, а затем assign_nested_attributes_for_collection_association
и assign_nested_attributes_for_one_to_one_association
, чтобы проверить существующую запись на основе этих ключевых атрибутов, прежде чем продолжить работу в обычном режиме. если не найден.
module ActiveRecord
module NestedAttributes
class << self
def included_with_key_option(base)
included_without_key_option(base)
base.class_inheritable_accessor :nested_attributes_keys, :instance_writer => false
base.nested_attributes_keys = {}
end
alias_method_chain :included, :key_option
end
module ClassMethods
# Override accepts_nested_attributes_for to allow for :key to be specified
def accepts_nested_attributes_for_with_key_option(*attr_names)
options = attr_names.extract_options!
options.assert_valid_keys(:allow_destroy, :reject_if, :key)
attr_names.each do |association_name|
if reflection = reflect_on_association(association_name)
self.nested_attributes_keys[association_name.to_sym] = [options[:key]].flatten.reject(&:nil?)
else
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
end
end
# Now that we've set up a class variable based on key, remove it from the options and call
# the overriden method to continue setup
options.delete(:key)
attr_names << options
accepts_nested_attributes_for_without_key_option(*attr_names)
end
alias_method_chain :accepts_nested_attributes_for, :key_option
end
private
# Override to check keys if given
def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
attributes = attributes.stringify_keys
if !(keys = self.class.nested_attributes_keys[association_name]).empty?
if existing_record = find_record_by_keys(association_name, attributes, keys)
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
return
end
end
if attributes['id'].blank?
unless reject_new_record?(association_name, attributes)
send("build_#{association_name}", attributes.except(*UNASSIGNABLE_KEYS))
end
elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
end
end
# Override to check keys if given
def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
end
if attributes_collection.is_a? Hash
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
end
attributes_collection.each do |attributes|
attributes = attributes.stringify_keys
if !(keys = self.class.nested_attributes_keys[association_name]).empty?
if existing_record = find_record_by_keys(association_name, attributes, keys)
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
return
end
end
if attributes['id'].blank?
unless reject_new_record?(association_name, attributes)
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s }
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
end
end
end
# Find a record that matches the keys
def find_record_by_keys(association_name, attributes, keys)
[send(association_name)].flatten.detect do |record|
keys.inject(true) do |result, key|
# Guess at the foreign key name and fill it if it's not given
attributes[key.to_s] = self.id if attributes[key.to_s].blank? and key = self.class.name.underscore + "_id"
break unless (record.send(key).to_s == attributes[key.to_s].to_s)
true
end
end
end
end
end
Возможно, не самое чистое решение, но оно работает (обратите внимание, что переопределения основаны на Rails 2.3).