让工厂内的两个关联对象共享另一个关联对象。

20

我有这5个模型:Guardian(监护人),Student(学生),Relationship(关系),RelationshipType(关系类型)和School(学校)。它们之间有以下关联:

class Guardian < ActiveRecord::Base
  belongs_to :school
  has_many :relationships, :dependent => :destroy
  has_many :students, :through => :relationships
end

class Student < ActiveRecord::Base
  belongs_to :school
  has_many :relationships, :dependent => :destroy
  has_many :guardians, :through => :relationships
end

class Relationship < ActiveRecord::Base
  belongs_to :student
  belongs_to :guardian
  belongs_to :relationship_type
end

class School < ActiveRecord::Base
  has_many :guardians, :dependent => :destroy
  has_many :students, :dependent => :destroy
end

class RelationshipType < ActiveRecord::Base
  has_many :relationships
end

我想编写一个FactoryGirl来定义一个关系。每个关系必须有一个监护人和一个学生,这两个人必须属于同一所学校。监护人工厂与学校有一个关联,学生工厂也是如此。但我一直无法让它们在同一所学校中创建。我有以下代码:

FactoryGirl.define do

  factory :relationship do
    association :guardian
    association :student, :school => self.guardian.school
    relationship_type RelationshipType.first
  end

end

使用这个工厂构建关系时,会导致以下错误:

undefined method `school' for #<FactoryGirl::Declaration::Implicit:0x0000010098af98> (NoMethodError)

有没有什么方法可以实现我想要的,让监护人和学生属于同一所学校,而不必通过将已创建的监护人和学生传递给工厂(这不是它的目的)来实现?


我不确定这是否与错误有关,但是School类被写成了第二个Relation class声明(在我的修改之前)。 - PinnyM
6个回答

12

这个答案是谷歌搜索“factory girl shared association”的第一个结果,并且santuxus的答案真的对我有所帮助 :)

以下内容是关于最新版本Factory Girl的语法更新,以防其他人也会遇到这个问题:

FactoryGirl.define do
  factory :relationship do
    guardian
    relationship_type RelationshipType.first

    after(:build) do |relationship|
      relationship.student = FactoryGirl.create(:student, school: relationship.guardian.school) unless relationship.student.present?
    end
  end
end

unless 子句会防止 student 被替换,如果它已经通过 FactoryGirl.create(:relationship, student: foo) 传递到工厂中。


10

我认为这应该可以解决问题:

FactoryGirl.define do
  factory :relationship do 
    association :guardian
    relationship_type RelationshipType.first
    after_build do |relationship|
      relationship.student = Factory(:student, :school => relationship.guardian.school)
    end
  end
end

4
这种关联的写法有更简洁的方式。答案来自这个 Github 问题的回答。
FactoryGirl.define do
  factory :relationship do 
    association :guardian
    student { build(:student, school: relationship.guardian.school) }
    relationship_type RelationshipType.first
  end
end

根据问题看起来,那一行应该是 student { build(:student, school: guardian.school) } 而不是省略了 relationship - Leo Lei

3

在nitsas的解决方案的基础上,可以滥用@overrides来检查是否已经覆盖了监护人或学生协会,并使用来自监护人/学生的学校协会。这使您不仅可以覆盖学校,还可以单独覆盖监护人或学生。

不幸的是,这依赖于实例变量而不是公共API。未来的更新很可能会破坏您的工厂。

factory :relationship do
  guardian { create(:guardian, school: school) }
  student  { create(:student,  school: school) }

  transient do
    school do
      if @overrides.key?(:guardian)
        guardian.school
      elsif @overrides.key?(:student)
        student.school
      else
        create(:school)
      end
    end
  end
end

这对我来说似乎非常有效,最灵活,特别是在我的情况下,我已经向一个需要与现有关联匹配的模型添加了第二个关联。这几乎使我所有现有的测试“正常工作”,并保持了它们的表达能力,例如“如果这组测试真的关心监护人,那么就可以新建一个通用学校”或反之亦然。当然,正如所指出的那样,这不是一个好主意(嗯,它起作用了!)-但是是否有关于此或其他FactoryBot内部的文档,还是你只是阅读代码? - stephan.com

1
这并不是你正在寻找的答案,但似乎创建此关联的困难表明需要调整表格设计。
询问“如果用户更改学校会怎样?”,则需要更新学生和监护人的学校,否则模型将失去同步。
我提出,学生、监护人和学校之间都有一种关系。如果学生更换学校,则为新学校创建一个新的关系。作为一个很好的副作用,这使得学生曾经就读过哪些学校可以被记录下来。
“属于”关联将从“学生”和“监护人”中移除,并转移到“关系”中。
工厂可以改为如下形式:
factory :relationship do
  school
  student
  guardian
  relationship_type
end

这可以用以下方式使用:
# use the default relationship which creates the default associations
relationship = Factory.create :relationship
school = relationship.school
student = relationship.student
guardian = relationship.guardian

# create a relationship with a guardian that has two charges at the same school
school = Factory.create :school, name: 'Custom school'
guardian = Factory.create :guardian
relation1 = Factory.create :relationship, school: school, guardian: guardian
relation2 = Factory.create :relationship, school: school, guardian: guardian
student1 = relation1.student
student2 = relation2.student

0

在这种情况下,我会使用瞬态依赖属性:

FactoryGirl.define do
  factory :relationship do
    transient do
      school { create(:school) }
      # now you can even override the school if you want!
    end

    guardian { create(:guardian, school: school) }
    student { create(:student, school: school) }
    relationship_type RelationshipType.first
  end
end

使用方法:

relationship = FactoryGirl.create(:relationship)

relationship.guardian.school == relationship.student.school
# => true

如果你想的话,甚至可以覆盖学校:

awesome_school = FactoryGirl.create(:school)
awesome_relationship = FactoryGirl.create(:relationship, school: awesome_school)

awesome_relationship.guardian.school == awesome_school
# => true
awesome_relationship.student.school == awesome_school
# => true

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接