定义了两个模型之间的一对一关系,但似乎仍然能够建立一对多

Defined one to one relationship between 2 models but still seem to be able to build one to many

给定两个具有一对一关系的 Rails 模型。一个 WorkItem 有一个 ChemicalSafetyFeature,一个 ChemicalSafetyFeature 属于一个 WorkItem

class WorkItem < ActiveRecord::Base
  has_one :chemical_safety_feature
end 

class ChemicalSafetyFeature < ActiveRecord::Base    
  belongs_to :work_item
end 

我继续 rails 控制台并创建一个 WorkItem。我得到一个 ID = 1

的 WorkItem 对象
WorkItem.create()

然后我像这样创建一个 ChemicalSafetyFeature 对象并对其进行测试

ChemicalSafetyFeature.create(work_item: WorkItem.first)

ChemicalSafetyFeature.workItem == WorkItem.first
WorkItem.first.chemical_safety_feature == ChemicalSafetyFeature.first

但如果我创建另一个 ChemicalSafetyFeature 并将其 link 到第一个工作项,这让我感到惊讶:

ChemicalSafetyFeature.create(work_item = WorkItem.first)

即使 ChemicalSafetyFeature.first.work_item 和 ChemicalSafetyFeature.find(2).work_item 指向第一个 WorkItem,但第一个 WorkItem 仅指向第一个 ChemicalSafetyFeature 对象。 我的期望是,当我尝试创建与第一个 WorkItem 关联的第二个 ChemicalSafetyFeature 对象时,它应该会引发错误。似乎我仍然可以创建两个 ChemicalSafetyFeature 对象,它们都 link 到第一个 WorkItem,这意味着第一个 WorkItem 有 2 个 ChenicalSafetyFeature 项目

has_one 关系不强制具有 has_one 关系的模型仅属于一个具有 belongs_to 关系的对象。它不强制只有一个 chemical_safety_features 记录带有 work_item_id.

您可以通过唯一性验证强制执行此操作:

class ChemicalSafetyFeature < ActiveRecord::Base
  validates :work_item_id, uniqueness: true
  belongs_to :work_item
end

此验证的工作方式是在保存 ChemicalSafetyFeature 实例之前查询数据库,以检查是否已经有包含该 work_item_id 的记录。如果找到记录,则保存失败。由于竞争条件,仍然有可能违反此约束。

假设数据库中没有冲突记录,两个进程使用 work_item_id = 5 查询数据库中的记录,但两个进程都没有找到任何记录。然后两个进程都会保存一条work_item_id = 5违反约束的记录。

解决方法是在数据库中添加唯一约束。这将确保您不会有两个具有相同 work_item_id 值的记录。

使用此 add_index 调用创建迁移:

add_index :chemical_safety_features, :work_item_id, unique: true

有了这个索引,数据库将强制执行约束,应用程序代码将无法违反约束。

您还应该使用 add_foreign_key 迁移将 work_item_id 定义为数据库中的外键。这将防止向该字段插入垃圾值。它还将防止孤立记录 - 如果在 chemical_safety_features table.

中引用了该记录,您将无法从 work_items 中删除该记录

最后,如果您在属于 ChemicalSafetyFeatureWorkItem 实例上调用 #destroy,您应该告诉 rails 该怎么做。查看 has_onehas_many 关系的 dependent 选项。

例如,如果您指定

class WorkItem < ActiveRecord::Base
  has_one :chemical_safety_feature, dependent: :destroy
end 

然后在 WorkItem 上调用 #destroy 将在相关的 ChemicalSafetyFeature 上调用 destroydependent 选项的其他值为 :delete:nullify:restrict_with_exception:restrict_with_error

最好始终在所有 has_onehas_many 关系上指定 dependent 选项。