Подтвердить что ты не робот

Подтвердить наличие полиморфного родителя

Я разрабатываю приложение Rails 3.2 со следующими моделями:

class User < ActiveRecord::Base
  # Associations
  belongs_to :authenticatable, polymorphic: true

  # Validations
  validates :authenticatable, presence: true # this is the critical line
end

class Physician < ActiveRecord::Base
  attr_accessible :user_attributes

  # Associations
  has_one :user, as: :authenticatable
  accepts_nested_attributes_for :user
end

То, что я пытаюсь сделать, это проверить, имеет ли пользователь всегда аутентифицируемый родитель. Это отлично работает само по себе, но в моей форме пользовательская модель жалуется, что аутентифицируемого нет.

Я использую следующий контроллер, чтобы показать форму для нового врача, который принимает вложенные атрибуты для пользователя:

def new
  @physician = Physician.new
  @physician.build_user

  respond_to do |format|
    format.html # new.html.erb
    format.json { render json: @physician }
  end
end

И это мой метод create:

def create
  @physician = Physician.new(params[:physician])

  respond_to do |format|
    if @physician.save
      format.html { redirect_to @physician, notice: 'Physician was successfully created.' }
      format.json { render json: @physician, status: :created, location: @physician }
    else
      format.html { render action: "new" }
      format.json { render json: @physician.errors, status: :unprocessable_entity }
    end
  end
end

При отправке формы он говорит, что пользователь, аутентифицируемый, не должен быть пустым. Однако атрибуты authenticatable_id и authenticatable_type должны быть назначены, как только будет сохранен @physician. Он отлично работает, если я использую одну и ту же форму для редактирования врача и его пользователя, так как тогда назначаются идентификатор и тип.

Что я здесь делаю неправильно?

4b9b3361

Ответ 1

Я считаю, что это ожидается:

https://github.com/rails/rails/issues/1629#issuecomment-11033182 (последние два комментария).

Также проверьте это из rails api:

Индивидуальные ассоциации

Назначение объекта ассоциации has_one автоматически сохраняет это объект и заменяемый объект (если таковой имеется), чтобы обновлять свои внешние ключи - кроме случаев, когда родительский объект несохранен (new_record? == true).

Если любой из этих сбой неудачен (из-за того, что один из объектов недействителен), возникает исключение ActiveRecord:: RecordNotSaved и назначение отменено.

Если вы хотите назначить объект ассоциации has_one без сохраняя его, используйте метод build_association (описанный ниже). объект, который будет заменен, будет сохранен для обновления его внешнего ключа.

Присвоение объекта ассоциации belongs_to не сохраняет объект, так как поле внешнего ключа принадлежит родительскому элементу. Это не сохраните родительский элемент.

и этот

build_association (attributes = {}) Возвращает новый объект связанный тип, который был создан с атрибутами и связан к этому объекту через внешний ключ, но еще не сохранен.

Сначала вы должны создать родителя. Затем присвойте его id полиморфному объекту.

Из того, что я вижу, вы создаете объект Physician.new, который создает пользователя, но на данный момент он еще не сохранен, поэтому он не имеет идентификатора, поэтому назначить полиморфный объект нечего. Поэтому проверка всегда будет терпеть неудачу, поскольку она вызывается перед сохранением.

Другими словами: в вашем случае, когда вы вызываете build_user, он возвращает User.new NOT User.create. Поэтому аутентифицируемый не имеет назначенного authenticatable_id.

У вас есть несколько вариантов:

  • Сначала сохраните связанного пользователя.

    ИЛИ

  • Перенесите подтверждение на обратный вызов after_save (возможно, но очень раздражающий и плохой)

    ИЛИ

  • Измените структуру своего приложения - возможно, избегайте полиморфной ассоциации и переключайтесь на has_many через? Мне трудно судить, так как я не знаю внутренних дел и требований бизнеса. Но мне кажется, что это не хороший кандидат на полиморфную ассоциацию. У вас будет больше моделей, чем просто Пользователь, который будет аутентифицироваться?

ИМХО Лучшие кандидаты на полиморфные ассоциации - это такие вещи, как телефоны, адреса и т.д. Адрес может принадлежать Пользователю, Клиенту, Компании, Организации, Area51 и т.д., быть домашней, судоходной или Биллинговой категорией, т.е. может MORPH, чтобы приспособить несколько применений, поэтому это хороший объект для извлечения. Но Authenticatable кажется мне немного изобретенным и добавляет сложности, когда в этом нет необходимости. Я не вижу никаких других объектов, подлежащих аутентификации.

Если бы вы могли представить свою модель Authenticatable и ваши аргументы и, возможно, миграции (?), я мог бы вам посоветовать больше. Прямо сейчас я просто вытягиваю это из воздуха:-) Но он кажется хорошим кандидатом на рефакторинг.

Ответ 2

Вы можете просто перенести проверку на обратный вызов before_save, и он будет работать нормально:

class User < ActiveRecord::Base
  # Associations
  belongs_to :authenticatable, polymorphic: true

  # Validations
  before_save :check_authenticatable

  def check_authenticatable
    unless authenticatable
      errors[:customizable] << "can't be blank"
      false
    end
  end
end

Ответ 3

В действии create мне пришлось назначить его вручную:

@physician = Physician.new(params[:physician])
@physician.user.authenticatable = @physician

Моя проблема немного отличается (has_many и с другой проверкой), но я думаю, что это должно сработать.

Ответ 4

Мне удалось заставить это работать, переопределив вложенный набор атрибутов.

class Physician
  has_one :user, as: :authenticatable
  accepts_nested_attributes_for :user

  def user_attributes=(attribute_set)
    super(attribute_set.merge(authenticatable: self))
  end
end

Чтобы сушить его, я переместил полиморфный код в проблему:

module Authenticatable
  extend ActiveSupport::Concern

  included do
    has_one :user, as: :authenticatable
    accepts_nested_attributes_for :user

    def user_attributes=(attribute_set)
      super(attribute_set.merge(authenticatable: self))
    end
  end
end

class Physician
  include Authenticatable
  ...
end

Для ассоциаций has_many то же самое можно сделать с помощью map:

class Physician
  has_many :users, as: :authenticatable
  accepts_nested_attributes_for :users

  def users_attributes=(attribute_sets)
    super(
      attribute_sets.map do |attribute_set|
        attribute_set.merge(authenticatable: self)
      end
    )
  end
end

class User
  belongs_to :authenticatable, polymorphic: true
  validates :authenticatable, presence: true
end

Все, что сказал, я думаю, что последний комментарий konung правильный - ваш пример не похож на хорошего кандидата на полиморфизм.

Ответ 5

Я не уверен, что это решает вашу проблему, но я использую что-то вроде этого, проверяя, что существует полиморфный родитель.

Вот код, который я использовал в модели video с parent как полиморфная ассоциация. Это произошло в video.rb.

  validates_presence_of :parent_id, :unless => Proc.new { |p|
      # if it a new record and parent is nil and addressable_type is set
      # then try to find the parent object in the ObjectSpace
      # if the parent object exists, then we're valid;
      # if not, let validates_presence_of do it thing
      # Based on http://www.rebeccamiller-webster.com/2011/09/validate-polymorphic/
      if (new_record? && !parent && parent_type)
        parent = nil
        ObjectSpace.each_object(parent_type.constantize) do |o|
          parent = o if o.videos.include?(p) unless parent
        end
      end
      parent
    }