Может ли принять accept_nested_attributes_for на основе составного ключа? - PullRequest
3 голосов
/ 31 марта 2011

Мои модели выглядят следующим образом:

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_id и template_id (в настоящее время она имеет первичный ключ id какхорошо, но это может быть удалено при необходимости).

Поскольку я использую accepts_nested_attributes_for, я могу создавать новые строки одновременно с созданием нового шаблона, который работает как должен.Однако, когда я пытаюсь обновить строку в существующем шаблоне, accepts_nested_attributes_for пытается создать новые объекты TemplateString, и затем база данных жалуется, что уникальное ограничение было нарушено (как и должно быть).

Есть лилюбой способ заставить accepts_nested_attributes_for использовать составной ключ при определении, должен ли он создать новую запись или загрузить существующую?

1 Ответ

2 голосов
/ 31 марта 2011

Я решил эту проблему так, чтобы обезьяна исправила 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).

...