ruby-on-rails - accepts_nested_attributes_for 可以基于复合键更新吗?

标签 ruby-on-rails has-many nested-attributes

我的模型如下所示:

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 在确定是否应该创建新记录或加载现有记录时使用复合键?

最佳答案

我解决这个问题的方法是猴子补丁 accepts_nested_attributes_for 接受一个 :key 选项,然后 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)。

关于ruby-on-rails - accepts_nested_attributes_for 可以基于复合键更新吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5493053/

相关文章:

grails - Grails/GORM是否可以在不执行N + 1查询的情况下过滤关联结果?

ruby-on-rails - 如何在 Rails 3 中路由多级嵌套资源?

ruby-on-rails - Rails 嵌套表单属性不保存

javascript - 实现图形来调用和显示rails数据库中的数据。

ruby-on-rails - 仅显示 act_as_taggable_on 标签云中最流行的标签

ruby-on-rails - rails/Papertrail : Changeset with association changes

ruby-on-rails - Rails 4 嵌套属性更新

ruby-on-rails - 链接到域提供商网站的 Heroku 应用程序

javascript - Rails '.js.erb' 不呈现部分

ruby-on-rails - Rails - 检查 has_many 关联中是否存在记录