Rails 与FactoryGirl,亲子协会。省略在子模型中再创建一条记录

Rails with FactoryGirl, parent-child association. Omit creating one more record in child model

我有两个模型。 父模型 Tag:

class Tag < ApplicationRecord
  has_many :keywords, inverse_of: :tag, dependent: :destroy
  accepts_nested_attributes_for :keywords

  validates :keywords, presence: true
end

如您所见,tag 应该至少有一个 keyword

童模Keyword:

class Keyword < ApplicationRecord
  belongs_to :tag, inverse_of: :keywords

  validates :tag, presence: true
end

这里是FactoryGirl个工厂的代码 tag 工厂:

FactoryGirl.define do
  factory :tag do
    sequence(:name) { |n| "Tag#{n}" }
    after(:build) do |tag_object|
      tag_object.keywords << build(:keyword, tag: tag_object)
    end
  end
end

keyword工厂:

FactoryGirl.define do
  factory :keyword do
    tag
    sequence(:name) { |n| "Keyword#{n}" }
  end
end

当我使用 keyword 工厂在 keywords table 中创建一条新记录时,它会在 keywords table 中创建一条与tags table.

中的相同父记录

如何省略在 keywords table 中再创建一条记录并保持工厂有效?

irb(main):023:0> FactoryGirl.create :keyword
   (0.1ms)  BEGIN
  Keyword Exists (0.7ms)  SELECT  1 AS one FROM "keywords" WHERE "keywords"."name" =  LIMIT   [["name", "Keyword1"], ["LIMIT", 1]]
  Tag Exists (0.3ms)  SELECT  1 AS one FROM "tags" WHERE "tags"."name" =  LIMIT   [["name", "Tag1"], ["LIMIT", 1]]
  SQL (0.5ms)  INSERT INTO "tags" ("name", "created_at", "updated_at") VALUES (, , ) RETURNING "id"  [["name", "Tag1"], ["created_at", 2017-01-21 19:20:14 UTC], ["updated_at", 2017-01-21 19:20:14 UTC]]
  SQL (0.6ms)  INSERT INTO "keywords" ("tag_id", "name", "created_at", "updated_at") VALUES (, , , ) RETURNING "id"  [["tag_id", 36], ["name", "Keyword1"], ["created_at", 2017-01-21 19:20:14 UTC], ["updated_at", 2017-01-21 19:20:14 UTC]]
   (10.4ms)  COMMIT
   (0.1ms)  BEGIN
  Keyword Exists (0.4ms)  SELECT  1 AS one FROM "keywords" WHERE "keywords"."name" =  LIMIT   [["name", "Keyword2"], ["LIMIT", 1]]
  SQL (0.4ms)  INSERT INTO "keywords" ("tag_id", "name", "created_at", "updated_at") VALUES (, , , ) RETURNING "id"  [["tag_id", 36], ["name", "Keyword2"], ["created_at", 2017-01-21 19:20:14 UTC], ["updated_at", 2017-01-21 19:20:14 UTC]]
   (4.4ms)  COMMIT
=> #<Keyword id: 63, tag_id: 36, name: "Keyword2", created_at: "2017-01-21 19:20:14", updated_at: "2017-01-21 19:20:14">
irb(main):024:0> 

您可以看到它在 tags 中创建了一条记录,在 keywords 中创建了一条记录 table,然后在 keywords 中又创建了一条记录 table.

关键字和标签不能相互独立存在。您的标签工厂每次调用时都会创建一个关键字,因此您应该调用标签工厂。试试这个:

tag = FactoryGirl.create(:tag)
keyword = tag.keywords.first

FactoryGirl 在构建过程中为模型创建所有声明的关联。这意味着 FactoryGirl.build :keyword 将执行 FactoryGirl.create :tag,因此它将具有 Keyword#tag_id 的 ID,以帮助通过 Keyword 模型的验证。

这与您看到的数据库 activity 一致。

irb(main):023:0> FactoryGirl.create :keyword
### keywordA = Keyword.new
### call create(:tag) because of association 
### tag1 = Tag.new
### call build(:keyword) in after(:build)
###.keywordB.new(tag: tag1) # which prevents trying to make a new tag!
### tag1.save # which saves the keywordB
   (0.1ms)  BEGIN
  Keyword Exists (0.7ms)  SELECT  1 AS one FROM "keywords" WHERE "keywords"."name" =  LIMIT   [["name", "Keyword1"], ["LIMIT", 1]]
  Tag Exists (0.3ms)  SELECT  1 AS one FROM "tags" WHERE "tags"."name" =  LIMIT   [["name", "Tag1"], ["LIMIT", 1]]
  SQL (0.5ms)  INSERT INTO "tags" ("name", "created_at", "updated_at") VALUES (, , ) RETURNING "id"  [["name", "Tag1"], ["created_at", 2017-01-21 19:20:14 UTC], ["updated_at", 2017-01-21 19:20:14 UTC]]
  SQL (0.6ms)  INSERT INTO "keywords" ("tag_id", "name", "created_at", "updated_at") VALUES (, , , ) RETURNING "id"  [["tag_id", 36], ["name", "Keyword1"], ["created_at", 2017-01-21 19:20:14 UTC], ["updated_at", 2017-01-21 19:20:14 UTC]]
   (10.4ms)  COMMIT
### keywordA.tag = tag1
### keywordA.save
   (0.1ms)  BEGIN
  Keyword Exists (0.4ms)  SELECT  1 AS one FROM "keywords" WHERE "keywords"."name" =  LIMIT   [["name", "Keyword2"], ["LIMIT", 1]]
  SQL (0.4ms)  INSERT INTO "keywords" ("tag_id", "name", "created_at", "updated_at") VALUES (, , , ) RETURNING "id"  [["tag_id", 36], ["name", "Keyword2"], ["created_at", 2017-01-21 19:20:14 UTC], ["updated_at", 2017-01-21 19:20:14 UTC]]
   (4.4ms)  COMMIT
### Since keywordA gets saved after keywordB,
### keywordB gets a 1 from the sequence and
### keywordA gets a 2 from the sequence
=> #<Keyword id: 63, tag_id: 36, name: "Keyword2", created_at: "2017-01-21 19:20:14", updated_at: "2017-01-21 19:20:14">
irb(main):024:0> 

这只是发生的事情的要点。就个人而言,我无法想象在没有基于数据库架构的 tag 的情况下需要一个 keyword,所以我会调用 create(:tag) 并获得第一个 keyword,如前所述。但模式足够简单,因此以下内容应该在我们想要测试的 100% 情况下成立:

FactoryGirl.define do
  factory :tag do
    sequence(:name) { |n| "Tag#{n}" }

    after(:build) do |this|
      this.keywords << build(:keyword) if this.keywords.empty?
    end
  end
end

FactoryGirl.define do
  factory :keyword do
    sequence(:name) { |n| "Keyword#{n}" }

    after(:build) do |this|
      this.tag ||= build(:tag)
    end
  end
end

build(:tag)          # unsaved
build(:tag).keyword  # also unsaved
create(:tag)         # saved
create(:tag).keyword # also saved

build(:keyword)      # unsaved
build(:keyword).tag  # also unsaved
create(:keyword)     # saved
create(:keyword).tag # also saved

# And it still lets you be specific
create(:tag, keywords: [create(:keyword, name: "More of a phrase")])
create(:keyword, tag: create(:tag, name: "Pop Me!"))

还有几个选项需要考虑:

# Fake the association
FactoryGirl.define do
  factory :keyword do
    sequence(:name) { |n| "Keyword#{n}" }
    tag_id 1 # Danger!
             # Will make it pass validation but you
             # will forget and #tag will not be found
             # or not what you expect
  end
end

# use a trait
FactoryGirl.define do
  factory :keyword do
    sequence(:name) { |n| "Keyword#{n}" }
    trait :with_tag do
      tag
    end
  end
end

# make a new factory
FactoryGirl.define do
  # do not need parent if inside the "factory :tag do"
  factory :tag_with_keyword, parent: :tag do 
    sequence(:name) { |n| "Tag#{n}" }
    keyword
  end
end
# but now we are back to it creating the keyword on build(:tag)

FactoryGirl 确实为您提供了足够的选择来解决许多情况,但诀窍是了解它如何设置关联并尽量避免设置隐式循环关联。