FactoryBot（旧FactoryGirl）で関連データを同時に生成する方法いろいろ


リレーションの関係にある複数のモデルを連動して追加したいときの方法いろいろ.
リレーション関係なくても、インスタンス生成と同時に特定の処理を実行したいときにも使える.

## 呼び出し側でcreateにブロック渡す

rspecなどのテストコード側でカスタムに関連データを追加する方法.
`shop has_many staffs`のリレーションができている前提で、以下のように`build`や`create`にブロックを渡せば、ブロック内で生成されたインスタンスを自由に修正できる.

```ruby
# 
# 生成されたインスタンスの内容をブロック内で自由に修正できる
# 
shop = FactoryBot.build(:shop) do |s|
  s.name = "あいうえお"
end

# 
# 永続化せずにインスタンス生成
# shopにひもづくstaffをブロック内で追加している
# 
shop = FactoryBot.build(:shop) do |s|
  s.staffs.build(FactoryBot.attributes_for(:staff)) 
end

# 
# 永続化されたインスタンス生成
# shopにひもづくstaffをブロック内で追加している
# 
shop = FactoryBot.create(:shop) do |s|
  s.staffs.create(FactoryBot.attributes_for(:staff)) 
end
```

## ファクトリ内でファクトリ呼び出し

ファクトリ内で別のファクトリ呼び出すと連鎖的にインスタンス生成することができる.
モデルが互いに関連していた場合はリレーションもはられた状態になる.
以下の例は、`staff belongs_to shop`のリレーションができている前提で動作する.

```ruby
FactoryBot.define do
  factory :staff, class: Staff do
    name "Isaac Newton"
    role  "physicist"

    shop # ファクトリよびだし
  end

  factory :shop, class: Shop do
    name "テストショップ"
  end
end
```

```ruby
staff = FactoryBot.create :staff
  # staffおよびshopがinsertされる
staff.shop
  # 連鎖生成されたshopをアソシエーションたどって参照できる
```

おそらく、↑ような別のファクトリを呼び出す記法は以下の省略形？になる.

```ruby
FactoryBot.define do
  factory :staff, class: Staff do
    name "Isaac Newton"
    role  "physicist"
    
    shop { FactoryBot.create :shop }
  end
end
```

↑の書き方だと、ブロックを使えるので自由度の高い生成ができるが、こういったカスタマイズしたい場合は次に紹介のassociationを使うのが推奨らしい.

また、trait使えばアソシエーション先を作成するかどうか切り替えることもできる

```ruby
FactoryBot.define do
  factory :staff, class: Staff do
    name "Isaac Newton"
    role  "physicist"
    
    trait :with_shop do
      shop
    end
  end

  factory :shop, class: Shop do
    name "テストショップ"
  end
end
```

```ruby
# 
# アソシエーション先を作らない
# 
user = FactoryBot.build :staff
user.shop
  # nil

# 
# アソシエーション先作る
# 
user = FactoryBot.build :staff, :with_shop
user.shop
  # => #<Shop id: 1, name: "テストショップ">
```

## Association

`staff belongs_to shop`の関連がある前提で以下のファクトリが動作する.
`association`メソッドにアソシエーション名とファクトリ名を指定する.
任意で生成時の属性を指定することもできる.

```ruby
factory :staff, class: Staff do
  name "Isaac Newton"
  association :shop, factory: :shop, name: "マダガスカルショップ"
end

factory :shop, class: Shop do
  name "テストショップ"
end
```

```ruby
staff = FactoryBot.create :staff
staff.shop.name # マダガスカルショップ
```

ただし、`association`はhas_manyリレーションでは使えない.
has_manyで関連データをつっこむときは次に紹介のcallbackを使う.

これも先ほどと同様trait使えばassociation実行切り替えもできる.

```ruby
FactoryBot.define do
  factory :staff, class: Staff do
    name "Isaac Newton"
    role  "physicist"
    
    trait :with_shop do
      association :shop, factory: :shop, name: "マダガスカルショップ"
    end
  end

  factory :shop, class: Shop do
    name "テストショップ"
  end
end
```

## Callback

callbackを使えば、生成したインスタンスが`create`, `build`されたイベントの直後に自由にインスタンスを修正することができる.
以下は、`shop has_many staffs`の関連ができている前提で動作.

```ruby
factory :shop, class: Shop do
  name "テストショップ"
  after(:create) do |shop|
    shop.staffs << FactoryBot.create(:staff, name: "織田信長")
    shop.staffs << FactoryBot.create(:staff, name: "葛飾北斎")
  end
end
```

↑の例では`build`時は関連が作成されない.
なのでcallback作る際は`after create`ではなく、`after build`にしておいたほうがよさそう.
`after build`コールバックは`FactoryBot.create`した後も実行される.

```ruby
factory :shop, class: Shop do
  name "テストショップ"
  after(:build) do |shop|
    shop.staffs << FactoryBot.build(:staff, name: "織田信長")
    shop.staffs << FactoryBot.build(:staff, name: "葛飾北斎")
  end
end

shop = FactoryBot.build :shop
shop.persisted? # false
shop.users.first.persisted? # false
```

こちらももちろんtrait使えばコールバック実行切り替えできる.
以下の例ではデフォルトでデフォルトのtraitを実行して、カスタムのtraitが指定された場合はデフォルトのデータを上書きする.

```ruby
factory :shop, class: Shop do
  name "テストショップ"
  
  with_default_staffs
  
  trait :with_default_staffs do
    after(:build) do |shop|
      shop.staffs << FactoryBot.build(:staff, name: "one")
      shop.staffs << FactoryBot.build(:staff, name: "two")
    end
  end
  
  trait :with_japanese_staffs do
    after(:build) do |shop|
      shop.staffs = []
      shop.staffs << FactoryBot.build(:staff, name: "織田信長")
      shop.staffs << FactoryBot.build(:staff, name: "葛飾北斎")
    end
  end
  
  trait :with_english_staffs do
    after(:build) do |shop|
      shop.staffs = []
      shop.staffs << FactoryBot.build(:staff, name: "Isaac Newton")
      shop.staffs << FactoryBot.build(:staff, name: "Robert Hooke")
    end
  end
end
```

```ruby
FactoryBot.create :shop
  # アソシエーション先としてdefault staffsが作成される
FactoryBot.create :shop, :with_japanese_staffs
  # アソシエーション先としてjapanese staffsが作成される
FactoryBot.create :shop, :with_english_staffs
  # アソシエーション先としてenglish staffsが作成される
```


# References

* https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md
* http://arjanvandergaag.nl/blog/factory_girl_tips.html
* https://robots.thoughtbot.com/aint-no-calla-back-girl