Rails の has_one の関連に対して create が複数回実行された時の挙動

## 例に使うモデル

Rails Guides から拝借しています。
http://guides.rubyonrails.org/association_basics.html

```rb
class Supplier < ApplicationRecord
  has_one :account
end

class Account < ApplicatoinRecord
  belongs_to :supplier
end
```

```rb
class CreateSuppliers < ActiveRecord::Migration[5.0]
  def change
    create_table :suppliers do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :accounts do |t|
      t.belongs_to :supplier, index: true
      t.string :account_number
      t.timestamps
    end
  end
end
```

## create を 2 度実行する

### Supplier を作成

```
[2] pry(main)> Supplier.create(name: 'dan')
   (0.1ms)  BEGIN
  SQL (27.8ms)  INSERT INTO "suppliers" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "dan"], ["created_at", 2016-08-24 14:22:00 UTC], ["updated_at", 2016-08-24 14:22:00 UTC]]
   (19.8ms)  COMMIT
=> #<Supplier:0x007ffb93d3f428
 id: 1,
 name: "dan",
 created_at: Wed, 24 Aug 2016 14:22:00 UTC +00:00,
 updated_at: Wed, 24 Aug 2016 14:22:00 UTC +00:00>
[3] pry(main)> s = Supplier.first
  Supplier Load (1.0ms)  SELECT  "suppliers".* FROM "suppliers" ORDER BY "suppliers"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<Supplier:0x007ffb93d2e7b8
 id: 1,
 name: "dan",
 created_at: Wed, 24 Aug 2016 14:22:00 UTC +00:00,
 updated_at: Wed, 24 Aug 2016 14:22:00 UTC +00:00>
```

### Account を 1 件作成する

```
[7] pry(main)> s.create_account(account_number: 1)
   (0.1ms)  BEGIN
  SQL (4.7ms)  INSERT INTO "accounts" ("supplier_id", "account_number", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["supplier_id", 1], ["account_number", "1"], ["created_at", 2016-08-24 14:22:46 UTC], ["updated_at", 2016-08-24 14:22:46 UTC]]
   (1.7ms)  COMMIT
  Account Load (2.7ms)  SELECT  "accounts".* FROM "accounts" WHERE "accounts"."supplier_id" = $1 LIMIT $2  [["supplier_id", 1], ["LIMIT", 1]]
=> #<Account:0x007ffb98299168
 id: 1,
 supplier_id: 1,
 account_number: "1",
 created_at: Wed, 24 Aug 2016 14:22:46 UTC +00:00,
 updated_at: Wed, 24 Aug 2016 14:22:46 UTC +00:00>

[8] pry(main)> Account.count
   (0.6ms)  SELECT COUNT(*) FROM "accounts"
=> 1

[9] pry(main)> Account.first
  Account Load (0.2ms)  SELECT  "accounts".* FROM "accounts" ORDER BY "accounts"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<Account:0x007ffb977eaa78
 id: 1,
 supplier_id: 1,
 account_number: "1",
 created_at: Wed, 24 Aug 2016 14:22:46 UTC +00:00,
 updated_at: Wed, 24 Aug 2016 14:22:46 UTC +00:00>
```

普通に 1 件 INSERT されているのが分かります。

### Account をさらに 1 件作成する

```
[10] pry(main)> s.create_account(account_number: 2)
   (0.1ms)  BEGIN
  SQL (0.3ms)  INSERT INTO "accounts" ("supplier_id", "account_number", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["supplier_id", 1], ["account_number", "2"], ["created_at", 2016-08-24 14:23:10 UTC], ["updated_at", 2016-08-24 14:23:10 UTC]]
   (1.1ms)  COMMIT
   (0.1ms)  BEGIN
  SQL (0.7ms)  UPDATE "accounts" SET "supplier_id" = $1, "updated_at" = $2 WHERE "accounts"."id" = $3  [["supplier_id", nil], ["updated_at", 2016-08-24 14:23:10 UTC], ["id", 1]]
   (0.3ms)  COMMIT
=> #<Account:0x007ffb9832dcf0
 id: 2,
 supplier_id: 1,
 account_number: "2",
 created_at: Wed, 24 Aug 2016 14:23:10 UTC +00:00,
 updated_at: Wed, 24 Aug 2016 14:23:10 UTC +00:00>

[11] pry(main)> Account.count
   (0.3ms)  SELECT COUNT(*) FROM "accounts"
=> 2

[12] pry(main)> Account.first
  Account Load (0.3ms)  SELECT  "accounts".* FROM "accounts" ORDER BY "accounts"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<Account:0x007ffb975331e0
 id: 1,
 supplier_id: nil,
 account_number: "1",
 created_at: Wed, 24 Aug 2016 14:22:46 UTC +00:00,
 updated_at: Wed, 24 Aug 2016 14:23:10 UTC +00:00>
```

`INSERT` の後に `UPDATE` が実行されているのが分かります。

#### 1 件目の Account が関連から外されている

`UPDATE` 文を見て分かる通り `supplier_id` に `nil` が指定されています。
`has_one` の関連は単純に関連するキーの first が取られることもあり、これまでの関連を外しておく必要があるようです。

```
[14] pry(main)> s.reload.account
  Supplier Load (0.4ms)  SELECT  "suppliers".* FROM "suppliers" WHERE "suppliers"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
  Account Load (0.2ms)  SELECT  "accounts".* FROM "accounts" WHERE "accounts"."supplier_id" = $1 LIMIT $2  [["supplier_id", 1], ["LIMIT", 1]]
=> #<Account:0x007ffb9a048e20
 id: 2,
 supplier_id: 1,
 account_number: "2",
 created_at: Wed, 24 Aug 2016 14:23:10 UTC +00:00,
 updated_at: Wed, 24 Aug 2016 14:23:10 UTC +00:00>
```

### Rails のコードを見てみる

has_one の create メソッドは以下が呼ばれます。

https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/singular_association.rb

```rb:activerecord/lib/active_record/associations/singular_association.rb
module ActiveRecord
  module Associations
    class SingularAssociation < Association
      def create(attributes = {}, &block)
        _create_record(attributes, &block)
      end

      private

        def _create_record(attributes, raise_error = false)
          record = build_record(attributes)
          yield(record) if block_given?
          saved = record.save
          set_new_record(record)
          raise RecordInvalid.new(record) if !saved && raise_error
          record
        end
```

どうやら `record.save` の後の `set_new_record` が今回見るべき箇所のようです。

https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/has_one_association.rb

```rb
module ActiveRecord
  module Associations
    class HasOneAssociation < SingularAssociation
      def replace(record, save = true)
        raise_on_type_mismatch!(record) if record

        # 1. 作成済の has_one 関連を読み込み
        load_target

        return target unless target || record

        # 2. 新しい関連と、異るか
        assigning_another_record = target != record

        if assigning_another_record || record.changed?
          save &&= owner.persisted?

          transaction_if(save) do
            # 3. dependent オプションに応じて、すでに存在していた関連を削除
            remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record

            # 4. 新しい関連のオーナー（今回であれば supplier）の設定と inverse_of の指定があれば参照するインスタンスも入れ替え
            if record
              set_owner_attributes(record)
              set_inverse_instance(record)

              # 5. 新しい関連の保存ができなければ、新しい関連のオーナーを null に戻し、元の関連にオーナーを再設定し、例外を返す
              if save && !record.save
                nullify_owner_attributes(record)
                set_owner_attributes(target) if target
                raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
              end
            end
          end
        end

        self.target = record
      end

      private

        def set_new_record(record)
          replace(record, false)
        end
```

概要はコメントに書いた感じです。

`remove_target!` の詳細を見てみます。`dependent` オプションの指定がなければ、以前の関連のオーナーに null を入れています。どうやらここが先ほどの `UPDATE`の正体のようですね :bulb: :bulb: 

```rb
def remove_target!(method)
  case method
  when :delete
    target.delete
  when :destroy
    target.destroy
  else
    nullify_owner_attributes(target)

    if target.persisted? && owner.persisted? && !target.save
      set_owner_attributes(target)
      raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " +
                            "The record failed to save after its foreign key was set to nil."
    end
  end
end

def nullify_owner_attributes(record)
  record[reflection.foreign_key] = nil
end
```

## 関連の外部キーに null: false が指定されている場合

もし Account のデータとして `supplier_id` が存在しないことがありえないとします。それを表現するには `NULL` を許可しないようにすることになります。

```rb
class AddNotNullConstraintToAccount < ActiveRecord::Migration[5.0]
  def change
    change_column_null :accounts, :supplier_id, false
  end
end
```

### Account をさらに 1 件作ってみる

```
[3] pry(main)> s.create_account(account_number: 3)
   (0.1ms)  BEGIN
  SQL (0.3ms)  INSERT INTO "accounts" ("supplier_id", "account_number", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["supplier_id", 1], ["account_number", "3"], ["created_at", 2016-08-24 15:10:04 UTC], ["updated_at", 2016-08-24 15:10:04 UTC]]
   (6.1ms)  COMMIT
   (0.1ms)  BEGIN
  SQL (7.6ms)  UPDATE "accounts" SET "supplier_id" = $1, "updated_at" = $2 WHERE "accounts"."id" = $3  [["supplier_id", nil], ["updated_at", 2016-08-24 15:10:04 UTC], ["id", 3]]
   (0.3ms)  ROLLBACK
ActiveRecord::StatementInvalid: PG::NotNullViolation: ERROR:  null value in column "supplier_id" violates not-null constraint
DETAIL:  Failing row contains (3, null, 3, 2016-08-24 15:09:49.784332, 2016-08-24 15:10:04.571745).
: UPDATE "accounts" SET "supplier_id" = $1, "updated_at" = $2 WHERE "accounts"."id" = $3
from /Users/sansan_dan/.rbenv/versions/2.2.5/lib/ruby/gems/2.2.0/gems/activerecord-5.0.0/lib/active_record/connection_adapters/postgresql_adapter.rb:598:in `async_exec'
```

このように `supplier_id` に nil を入れようとしても DB の制約により不可能なので失敗してしまいます。

ここで注意したいのは、SQL を見てもわかるように ROLLBACK されているのは `UPDATE` だけです。つまり `Account` は作られてしまっています。

複数の Account が作られている以上は正しく has_one の関連を引くことはできません。

## まとめ

create を複数回実行する事を意図して実施することはあまり無いと思います。

今回私が遭遇した環境も同様で、本来は create は一度しか実行されないはずでしたが、外部の挙動が複数回呼び出しを可能にしてしまっていました。

`has_one` の挙動を知るいいきっかけになりました。