2011-03-30 64 views
2

我的模型看起来像下面这样:可以基于复合键accepts_nested_attributes_for更新?

class Template < ActiveRecord::Base 
    has_many :template_strings 

    accepts_nested_attributes_for :template_strings 
end 

class TemplateString < ActiveRecord::Base 
    belongs_to :template 
end 

TemplateString模型由一个复合键确定,在language_idtemplate_id(它目前拥有id主键为好,但如果需要,可以删除)。

因为我使用accepts_nested_attributes_for,我可以同时创建新的字符串我创建一个新的模板,它的作品,因为它应该。然而,当我尝试在现有模板更新字符串,accepts_nested_attributes_for尝试创建新TemplateString对象,然后将数据库抱怨说,独特的违反约束(因为它应该)。

有没有什么办法让accepts_nested_attributes_for使用确定是否应该创建一个新的记录,或者加载现有一个当复合键?

回答

2

我解决这个问题的办法是猴子补丁accepts_nested_attributes_for采取在:关键选项,然后assign_nested_attributes_for_collection_associationassign_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)。