简体   繁体   中英

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

I have two models. Parent model Tag :

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

  validates :keywords, presence: true
end

As you can see tag should have at least one keyword .

Child model Keyword :

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

  validates :tag, presence: true
end

Here is the code of FactoryGirl factories tag factory:

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 factory:

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

When I create a new record in keywords table with keyword factory it creates one more record in keywords table which is associated with the same parent record in tags table.

How to omit creating one more record in keywords table and keep factories valid?

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> 

You can see that it created a record in tags , a record in keywords table, and after that one more record in keywords table.

FactoryGirl creates all the stated associations for the model during the build process. Which means a FactoryGirl.build :keyword will do a FactoryGirl.create :tag so it will have an id for Keyword#tag_id to help pass validations on the Keyword model.

This is consistent with the database activity you are seeing.

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> 

This is just the gist of what happens. Personally, I cannot imagine wanting a keyword without it's tag based on the database's schema so I would just call create(:tag) and get the first keyword as mentioned before. But the schema is simple enough so the following should hold up in 100% of the situations we would want to test:

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!"))

A few more options to consider:

# 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 does give you enough options to solve many situations but the trick is understanding how it sets up the associations and try to stay away from setting up implicit circular ones.

Keywords and tags cannot exist independent of each other. Your tag factory creates a keyword every time it is called, so you should be calling the tag factory. Try this:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM