dependent: :restrict_with_error と :restrict_with_exception の違い


## はじめに：dependent オプションとは？

dependent オプションはRailsであるモデルが子のレコードを持っている場合、親レコードを削除するときに子レコードをどうするのかを決めるオプションです。実際の挙動はいくつかの選択肢の中から選ぶことができます。

### オプションの種類

- `:destroy` 親と一緒に子レコードも削除する。（無理心中パターン）
- `:delete_all` 親と一緒に子レコードも削除する。ただし、直接DBのレコードを削除するので、子レコードのコールバック処理は実行されない。
- `:nullify` 子レコードの外部キーを `NULL` 更新する。（みなしごパターン）
- `:restrict_with_exception` 子レコードがある場合は `ActiveRecord::DeleteRestrictionError` が発生する。（引き留めパターン）
- `:restrict_with_error` 子レコードがある場合は削除できず、親レコードにエラー情報が付加される。（引き留めパターン）

参考：[ActiveRecord::Associations::ClassMethods](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html)

### 設定例

```ruby
class Category < ActiveRecord::Base
  has_many :items, dependent: :restrict_with_exception
end
```

この記事ではこの中から `restrict_with_error` と `restrict_with_exception` をピックアップして、両者の違いを説明します。

## restrict_with_exception と restrict_with_error の違いをサンプルコードで確認する

次のようなモデルを定義します。

```ruby
# カテゴリ
class Category < ActiveRecord::Base
  # :restrict_with_exception を指定
  has_many :items, dependent: :restrict_with_exception
end

# 商品
class Item < ActiveRecord::Base
  belongs_to :category
  # :restrict_with_error を指定
  has_many :orders, dependent: :restrict_with_error
end

# 注文
class Order < ActiveRecord::Base
  belongs_to :item
end
```

モデル図で表すとこんな感じです。
![diagram.jpg](https://qiita-image-store.s3.amazonaws.com/0/7465/d6d68428-deda-1ff2-f8a7-139d0eb124ab.jpeg "diagram.jpg")

### 挙動の違い確認するテストコード

Minitestでテストコードを書いてみました。

```ruby
require 'test_helper'

class CategoryTest < ActiveSupport::TestCase
  test 'restrict options' do
    category = Category.create!(name: '本')
    item = category.items.create!(name: 'Ruby入門')
    order = item.orders.create!(customer: 'Alice')

    # 子レコードがあると削除できない
    refute item.destroy
    assert_equal ["Cannot delete record because dependent orders exist"],
                 item.errors.messages[:base]
    assert_raises(ActiveRecord::DeleteRestrictionError) do
      category.destroy
    end

    # 子レコードがなければ削除可能
    assert order.destroy
    assert_difference 'Item.count', -1 do
      item.reload.destroy
    end
    assert_difference 'Category.count', -1 do
      category.reload.destroy
    end
  end
end
```

注文があると商品は削除できません。
`restrict_with_error` を付けているので `destroy` を呼ぶと `false` が返され、商品にエラーメッセージが付加されます。

```ruby
refute item.destroy
assert_equal ["Cannot delete record because dependent orders exist"],
             item.errors.messages[:base]
```

同様に、商品があるとカテゴリは削除できません。
ただし、こちらは `restrict_with_exception`を付けているので、 `ActiveRecord::DeleteRestrictionError` が発生します。

```ruby
assert_raises(ActiveRecord::DeleteRestrictionError) do
  category.destroy
end
```

先に子レコードを削除しておくと、親レコードを削除することができます。

```ruby
assert order.destroy
assert_difference 'Item.count', -1 do
  item.reload.destroy
end
assert_difference 'Category.count', -1 do
  category.reload.destroy
end
```

### サンプルコードはこちら

このコードはGitHubに置いてあります。気になる人は実際に動かしてみてください。

https://github.com/JunichiIto/restrict-options-sandbox

## そもそもなんでこの記事を書こうと思ったのか

`restrict_with_error` と `restrict_with_exception` の意味を勘違いしていたからです。

話がちょっとそれますが、 Ruby には Exception クラスと StandardError クラスがあります。
StandardError は Exception のサブクラスです。

参考: [rescue節で例外の型を指定しない場合に補足できるのはStandardErrorとそのサブクラスだけ](http://qiita.com/jnchito/items/a6046733dd5683ff35b7)

さらに、 ActiveRecord::DeleteRestrictionError は StandardError のサブクラスになっています。


僕は `restrict_with_error` と `restrict_with_exception` の違いを StandardError と Exception の違いだと思っていました。

なので、こうなると思っていたのです。（error と Error が同じという理由で）

```ruby
class Item < ActiveRecord::Base
  belongs_to :category
  has_many :orders, dependent: :restrict_with_error
end

# 実際はActiveRecord::DeleteRestrictionErrorは発生しない
assert_raises(ActiveRecord::DeleteRestrictionError) do
  item.destroy
end
```

が、実際は `restrict_with_error` の error は ActiveRecord の error （つまりバリデーションエラー）のことを意味しているようでした。

`restrict_with_exception` を付けると DeleteRestrictionError が発生し、 `restrict_with_error` を付けるとActiveRecordに error が付加されるって、なんかややこしくないですか？
そうでもないですか、そうですか。。。

## まとめ

というわけで、 `restrict_with_error` と `restrict_with_exception` 、 StandardError と Exception の違いを正しく理解し、バグのないRubyプログラムを書きましょう！！

### PR:「RSpecユーザのためのMinitestチュートリアル」を出版しました

先日、「RSpecユーザのためのMinitestチュートリアル」という電子書籍を出版しました。
MinitestはRSpecほど高機能ではありませんが、rails newするだけですぐにテストコードが書けるので、今回のような簡単なプログラムをテストするのにはとても便利です。
RSpecしか使ったことがない、という方はMinitestも使えるようになるとどこかで役に立つかもしれませんよ？

書籍の詳しい内容や購入方法については以下のブログ記事をご覧ください。

- [追加コンテンツが盛りだくさん！「RSpecユーザのためのMinitestチュートリアル」の正式版を公開しました](http://blog.jnito.com/entry/2015/07/29/081843)
- [Minitestの技術書としては日本初！？「RSpecユーザのためのMinitestチュートリアル（ベータ版）」を公開しました！](http://blog.jnito.com/entry/2015/06/30/070631)

みなさんよろしくお願いします。 m(_ _)m

[RSpecユーザのためのMinitestチュートリアル](https://leanpub.com/everydayrailsrspec-jp)
※「Everyday Rails - RSpecによるRailsテスト入門」を購入してもらうと追加コンテンツとしてダウンロードできます。
[![20150630033146.jpg](https://qiita-image-store.s3.amazonaws.com/0/7465/00fb2826-da37-bd65-f741-e1318a5b61ce.jpeg "20150630033146.jpg")](https://leanpub.com/everydayrailsrspec-jp)