【问题标题】:Rails with FactoryGirl, parent-child association. Omit creating one more record in child modelRails 与 FactoryGirl,父子关联。省略在子模型中再创建一条记录
【发布时间】:2017-06-06 14:54:15
【问题描述】:

我有两个模型。 父模型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 表中创建一条新记录时,它会在keywords 表中创建一条新记录,该记录与tags 表中的同一父记录相关联。

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

irb(main):023:0> FactoryGirl.create :keyword
   (0.1ms)  BEGIN
  Keyword Exists (0.7ms)  SELECT  1 AS one FROM "keywords" WHERE "keywords"."name" = $1 LIMIT $2  [["name", "Keyword1"], ["LIMIT", 1]]
  Tag Exists (0.3ms)  SELECT  1 AS one FROM "tags" WHERE "tags"."name" = $1 LIMIT $2  [["name", "Tag1"], ["LIMIT", 1]]
  SQL (0.5ms)  INSERT INTO "tags" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) 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 ($1, $2, $3, $4) 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" = $1 LIMIT $2  [["name", "Keyword2"], ["LIMIT", 1]]
  SQL (0.4ms)  INSERT INTO "keywords" ("tag_id", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) 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 表中创建了一条记录,然后在keywords 表中创建了一条记录。

【问题讨论】:

    标签: ruby-on-rails ruby rails-activerecord factory-bot


    【解决方案1】:

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

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

    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" = $1 LIMIT $2  [["name", "Keyword1"], ["LIMIT", 1]]
      Tag Exists (0.3ms)  SELECT  1 AS one FROM "tags" WHERE "tags"."name" = $1 LIMIT $2  [["name", "Tag1"], ["LIMIT", 1]]
      SQL (0.5ms)  INSERT INTO "tags" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) 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 ($1, $2, $3, $4) 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" = $1 LIMIT $2  [["name", "Keyword2"], ["LIMIT", 1]]
      SQL (0.4ms)  INSERT INTO "keywords" ("tag_id", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) 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 确实为您提供了足够的选项来解决许多情况,但诀窍是了解它如何设置关联并尽量避免设置隐式循环关联。

    【讨论】:

      【解决方案2】:

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

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

      【讨论】:

      • 是的,我可以在tag工厂的帮助下以这种方式创建一个新的keyword。但是可能可以直接使用keyword工厂,也许应该更改这些工厂的源代码,以便传递一些额外的参数。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多