Rails多态关联:getter和setter方法

时间:2016-02-18 22:14:44

标签: ruby-on-rails ruby getter-setter polymorphic-associations

我当前的项目允许Doctors有许多Patients我可以执行以下操作:

dennis = Patient.create
frank = Doctor.create
dennis.update(doctor: frank)
dennis.doctor #=> frank
frank.patients #=> [dennis, ...]

但现在我想增加一个班级医院,也可以有很多病人。我不想在has_many课程中添加另一个Hostpital,因为Patient'所有权'可能会再次改变,最终我的病人模型会被外键字段所困扰,除了其中一个外,其他所有字段都是空的。多态关联似乎正是我所寻找的:Patient可以拥有'通过DoctorHospital

class Patient < ActiveRecord::Base
  belongs_to :owner, polymorphic: true
end

class Doctor < ActiveRecord::Base
  has_many :patients, as: :owner
end

class Hospital < ActiveRecord::Base
  has_many :patients, as: :owner
end

这允许我们执行以下操作:

dennis = Patient.create
frank = Doctor.create
dennis.update(owner: frank)
dennis.owner #=> frank
frank.patients #=> [dennis, ...]

但是,我们无法致电dennis.doctor来返回frank。我了解所有者可能并不总是Doctor类的实例,但我当前的大部分代码都使用#doctor#doctor=方法。所以我想我可以定义它们:

class Patient < ActiveRecord::Base
  belongs_to :owner, polymorphic: true

  def patient=(patient)
    self.owner_id = patient.id
    self.owner_type = "Patient"
  end

  def patient
    return nil unless self.owner
    self.owner.class == Patient ? self.owner : nil
  end

end

这似乎工作正常,但这种关联仍然没有反映在我的数据库中。我有一些引用patients.doctor的自定义SQL查询。现在抛出Mysql2::Error: Unknown column 'patients.doctor'多态关联。

我有更好的方法来实现这个吗?此时,返回我的所有代码和SQL查询以将.doctor更改为.owner将非常耗时。

TL; DR 尝试从has_many切换到多态关联,但我想保留方便的getter和setter方法(以及我的数据库中的关联)有很多关系。

感谢任何帮助!

1 个答案:

答案 0 :(得分:0)

警告:我不强烈推荐这个,因为它非常hacky并且依赖于绕过几个Rails行为,这些行为容易出错并且永远不会出现前瞻性。例如。如果您使用它们,您还必须覆盖build_doctorcreate_doctorcreate_doctor!方法,我在下面没有这样做。我同意@doctor_of_ogz,如果你可以避免多态关联,你应该。我不清楚最好的解决方案是什么,但你可能想要更多地考虑多个外键列,因为它可能值得额外的nils。

但如果你不想走那条路,你想要的应该是:

class Patient < ActiveRecord::Base
  belongs_to :owner, polymorphic: true
  belongs_to :doctor, ->{joins(:patients).where(patients: {owner_type: "Doctor"})}, foreign_key: "owner_id"
  belongs_to :hospital, ->{joins(:patients).where(patients: {owner_type: "Hospital"})}, foreign_key: "owner_id"

  def doctor(*args)
    owner_type == "Doctor" ? super : nil
  end

  def doctor=(doctor)
    super
    self.owner = doctor
  end

  def hospital(*args)
    owner_type == "Hospital" ? super : nil
  end

  def hospital=(hospital)
    super
    self.owner = hospital
  end
end

说明:

关联在resource_type上添加范围,以允许您为连接和预加载等正确定义关联。但是如果你打算使用实例方法joins,你需要显式地将patient.doctor添加到范围中(不用试试,你会看到会发生什么)。

我仍然在doctorhospital实例方法上使用方法覆盖,因为如果您调用patient.hospital,它将返回Hospital它会找到一个具有Patient以及Hospital的{​​{1}}与当前id的{​​{1}}匹配的位置,即使patient。 Rails正在owner_id模型上生成查询而不考虑我们从中调用它的实例。这是它开始感到hacky并且不推荐的地方,但是覆盖只是通过像你已经完成的那样开启patient.owner_type == "Doctor"来解决这个问题,但现在我们可以使用Hospital来获取你Rails的缓存行为和Rails的关联getter方法中内置的任何其他褶边。同样,setter方法self.owner_type没有设置super,所以我再次覆盖它与现有方法类似,但使用doctor=。请注意,如果提供了错误的参数,我会调用super first (例如,您将owner_type传递给super,这将引发Hospital),在哪种情况下它不会改变doctor=。然后,我选择ActiveRecord::AssociationTypeMismatch而不是简单地说self.owner_type 设置self.owner_type = "Doctor",并且知道填充self.owner = doctor的缓存关联

我写了一些关于这个解决方案的更多细节here