dependentはどうやって動いているのか？

後述する仕様を実現するためにactiverecordのdependent周りのソースを読んだのでまとめました。

# 前提
railsのアソシエーションには`dependent`というオプションを設定することができます。
これによりレコードを削除した時に関連レコードに対して自動的に様々な動作をさせることができます。
例えば`has_many`の場合は下記を指定できます。

```ruby
# レコード削除時に関連するbooksも削除する(callbackあり)
has_many :books, dependent: :destroy
# レコード削除時に関連するbooksも削除する(callbackなし)
has_many :books, dependent: :delete_all
# レコード削除時に関連するbooksの外部キーをnilに更新する
has_many :books, dependent: :nullify
# レコード削除時に関連するbooksが存在する場合例外を発生させる
has_many :books, dependent: :restrict_with_exception
# レコード削除時に関連するbooksが存在する場合errorsを追加して削除を失敗させる
has_many :books, dependent: :restrict_with_error
```

詳細はrailsドキュメント参照してください。
https://railsguides.jp/association_basics.html#dependent

# やりたかったこと
(色々端折ってはいますが)下記の仕様を満たすモデルを作成しようとしていました。

* お店には本を追加・削除できる。ただし、必ず１冊以上は本がある状態をキープする。
* お店を削除した時は全ての本を削除する。

簡単に実装したものが下記。
ソースコメントに記載していますが、`before_destory`の中で①「`shop`を削除して`dependent: :destroy`で削除されようとしている」のか、②「`books`を単独で消そうとした」のかを判断する方法がぱっとわかりませんでした。

```ruby
# お店
class Shop < ApplicationRecord
  has_many :books, dependent: :destroy
end

# 本
class Books < ApplicationRecord
  belongs_to :shop

  before_destroy :before_destroy_action

  def before_destroy_action
    # 最後の１冊の場合は削除できない。
    # ただし、お店ごと削除する場合はすべて削除する。 <- これどう判定するの？？？？
    raise unless Books.not.where(id: id).exists?
  end
end
```

# 考えた解決案
## `dependent: :delete_all`を使う
`dependent: :destroy`の部分を`dependent: :delete_all`を変更することでcallbackが呼ばれなくなるので`before_destroy`は`books`を直接削除したときのことだけ考えれば良くなる。
これで動作は問題ないが、今後`books`に他のcallbackが追加された時や子テーブルが追加されて`dependent: :destory`としたい時などに支障がでるためこの方法はパス。

```ruby
# お店
class Shop < ApplicationRecord
  has_many :books, dependent: :delete_all
end

# 本
class Books < ApplicationRecord
  belongs_to :shop

  before_destroy :before_destroy_action

  def before_destroy_action
    # 最後の１冊の場合は削除できない。
    # shopを削除した場合はbefore_destroyは実行されないので考慮不要
    raise unless Books.not.where(id: id).exists?
  end
end
```

## `dependent: :destroy`で削除する場合にshopレコードに判定するためのフラグを持たせる
下記のように`shop`を削除する時にフラグを立てて`book`の`before_destroy`内でそのフラグで判定する。
この方法でも動作は問題ないが、そもそもこの微妙なフラグをわざわざ自前で定義する必要があるのか？railsのことだから判定する方法があるのではないか？と考えて保留にしました。

```ruby
# お店
class Shop < ApplicationRecord
  before_destroy: :before_destroy_action
  has_many :books, dependent: :destroy

  def before_destroy_action
    @destroying = true
  end

  def destroying?
    @destroying
  end
end

# 本
class Books < ApplicationRecord
  belongs_to :shop

  before_destroy :before_destroy_action

  def before_destroy_action
    # shopを削除した場合は何もしない
    return if shop.destroying?

    # 最後の１冊の場合は削除できない。
    raise unless Books.not.where(id: id).exists?
  end
end
```

## railsに組み込まれている機能を使って判定する
ということでrailsのソースを読んで使えそうな機能を探すことにしました。

### `destroy`について
最初に`destroy`のソースを見てみました。
`@destroyed`ってフラグを立てているじゃないか。これ使えないかな？
https://github.com/rails/rails/blob/93a6500baa6bbb331bb93ccdc14fdda5769f5ef9/activerecord/lib/active_record/persistence.rb#L172

結論を先に書きますが`dependent: :destroy`による削除が先に動いてしまい、関連データの`before_destroy`が呼ばれる時には`@destroyed`はtrueになっていません。

### `dependent`について
動作を確認して`destroy`より先に`dependent: :destroy`が動いていることはわかりましたが、そもそも`dependent`って何をしているんだろうということでソースをみてみました。

https://github.com/rails/rails/blob/fc35da76e93f8a5d5ace595b4819e19cc0512edd/activerecord/lib/active_record/associations/builder/association.rb#L32
↓
https://github.com/rails/rails/blob/fc35da76e93f8a5d5ace595b4819e19cc0512edd/activerecord/lib/active_record/associations/builder/association.rb#L76
↓
https://github.com/rails/rails/blob/fc35da76e93f8a5d5ace595b4819e19cc0512edd/activerecord/lib/active_record/associations/builder/association.rb#L129
↓
https://github.com/rails/rails/blob/47e3bbeb9057b37c244330cc4e745c8a8090e8c5/activerecord/lib/active_record/associations/has_many_association.rb#L13

上記の順番に追っていけばわかりますが、dependentで指定したオプションを判定して削除処理の`before_destroy`を追加しています。

ということで、dependentは`before_destroy`を追加しているだけなので`destroy`より前に動いちゃうんですね。

### じゃあどうする？
下記のソースを見てみると`dependent: :destroy`の時に`destroyed_by_association`に値を入れていることがわかります。
https://github.com/rails/rails/blob/47e3bbeb9057b37c244330cc4e745c8a8090e8c5/activerecord/lib/active_record/associations/has_many_association.rb#L27

これを判定に使えそうなので検証したところ下記のようにチェック可能でした。
自前フラグを追加するよりこちらの方がスマートな気がします。

```ruby
# お店
class Shop < ApplicationRecord
  has_many :books, dependent: :destroy
end

# 本
class Books < ApplicationRecord
  belongs_to :shop

  before_destroy :before_destroy_action

  def before_destroy_action
    # shopを削除した場合は何もしない
    return if destroyed_by_association.present?

    # 最後の１冊の場合は削除できない。
    raise unless Books.not.where(id: id).exists?
  end
end
```

# あとがき
railsのようなオープンソースのフレームワークはソースを簡単にみることができるので仕様を細かく知りたくなったら積極的に読んでみることをお勧めします。
自分でごちゃごちゃやらなくても大抵のことは他の人も困っていて、すでに実装されていたりします。
また、コードリーディングはコーディング力向上に繋がると思いますし、理解が深まればその言語を使うことがより楽しくなると思います。

# あとがきのあとがき
ソースを読んで`destroyed_by_association`が使えそうとわかったところで、あらためてググってみたら似たような記事をいくつか見つけました。下記は一例（qiitaにもあった)
https://qiita.com/mishiwata1015/items/ac7c33b5f116111d8568

私は`destroyed_by_association`というキーワードを見つけるまでは関連記事には辿り着けなかったけど、ググり力が高い人はさらっと見つけて時短できるんだろうな。ググり力もエンジニアには重要だなと感じました。