验证多态父级的存在

时间:2013-01-27 23:18:45

标签: ruby-on-rails polymorphism associations

我正在使用以下模型开发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

我要做的是验证用户是否始终拥有可验证的父级。这本身很好用,但在我的表单中,用户模型抱怨不存在authenticatable。

我正在使用以下控制器为新医生显示一个表格,该医生接受用户的嵌套属性:

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

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

这是我的创建方法:

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

在提交表单时,它表示用户的authenticable不能为空。但是,只要保存@physician,就应该分配authenticatable_id和authenticatable_type。如果我使用相同的表单来编辑医生及其用户,那么它可以正常工作,因为这样就分配了id和类型。

我在这里做错了什么?

5 个答案:

答案 0 :(得分:7)

我相信这是预期的:

https://github.com/rails/rails/issues/1629#issuecomment-11033182(最后两条评论)。

同时从rails api

中查看
  

一对一协会

     

将对象分配给has_one关联会自动保存该对象   对象和被替换的对象(如果有的话),以便   更新其外键 - 除非父对象未保存   (new_record?== true)。

     

如果其中任何一个保存失败(由于其中一个对象存在)   无效),引发了一个ActiveRecord :: RecordNotSaved异常   作业被取消。

     

如果您希望在没有的情况下将对象分配给has_one关联   保存它,使用build_association方法(下面记录)。该   被替换的对象仍将被保存以更新其外键。

     

将对象分配给belongs_to关联不会保存   object,因为外键字段属于父键。它不是   保存父母。

和这个

  

build_association(attributes = {})返回一个新对象   已使用属性实例化并链接的关联类型   通过外键到达此对象,但尚未保存。

您必须先创建一个Parent。然后将它的id分配给多态对象。

从我所看到的,你创建了一个对象Physician.new,它构建了User,但此时它还没有保存,因此它没有id,所以没有任何东西要分配给多态对象。因此,验证将始终失败,因为它在保存之前被调用。

换句话说:在你调用build_user的情况下,它会返回User.new NOT User.create。因此,authenticatable并没有分配authenticatable_id。

您有几种选择:

  • 首先保存关联用户。

    或者

  • 将验证移至after_save回调(可能但非常烦人且不好)

    或者

  • 更改您的应用结构 - 也许避免多态关联并切换到has_many通过?由于我不了解内部和业务要求,因此很难判断。但在我看来,这不是多态关联的良好候选者。你会有更多的模型,而不仅仅是可验证的用户吗?

恕我直言,多态关联的最佳候选者是电话,地址等等。地址可以属于用户,客户,公司,组织,Area51等,是家庭,航运或计费类别,即它可以 MORPH 以适应多种用途,因此它是一个很好的提取对象。但Authenticatable在我看来有点做作,并且在不需要的地方增加了复杂性。我没有看到任何其他需要可验证的对象。

如果您可以展示您的Authenticatable模型以及您的推理和迁移(?),我可以为您提供更多建议。现在我只是凭空捏出这个:-)但它似乎是重构的好选择。

答案 1 :(得分:3)

您可以将验证移至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

答案 2 :(得分:0)

在创建动作中,我必须手动分配:

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

我的问题有点不同(has_many和不同的验证),但我认为这应该有效。

答案 3 :(得分:0)

我能够通过覆盖嵌套的属性设置器来实现这一点。

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的最后评论是正确的 - 你的例子看起来不像多态性的好候选人。

答案 4 :(得分:-1)

我不确定这是否能解决您的问题,但在验证多态父级存在时,我会使用类似的东西。

以下是我在video模型中使用parent作为多态关联的一些代码。这进入了video.rb

  validates_presence_of :parent_id, :unless => Proc.new { |p|
      # if it's 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's 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
    }