はじめに:dependent オプションとは?
dependent オプションはRailsであるモデルが子のレコードを持っている場合、親レコードを削除するときに子レコードをどうするのかを決めるオプションです。実際の挙動はいくつかの選択肢の中から選ぶことができます。
オプションの種類
- 
:destroy親と一緒に子レコードも削除する。(無理心中パターン)
- 
:delete_all親と一緒に子レコードも削除する。ただし、直接DBのレコードを削除するので、子レコードのコールバック処理は実行されない。
- 
:nullify子レコードの外部キーをNULL更新する。(みなしごパターン)
- 
:restrict_with_exception子レコードがある場合はActiveRecord::DeleteRestrictionErrorが発生する。(引き留めパターン)
- 
:restrict_with_error子レコードがある場合は削除できず、親レコードにエラー情報が付加される。(引き留めパターン)
参考:ActiveRecord::Associations::ClassMethods
設定例
class Category < ActiveRecord::Base
  has_many :items, dependent: :restrict_with_exception
end
この記事ではこの中から restrict_with_error と restrict_with_exception をピックアップして、両者の違いを説明します。
restrict_with_exception と restrict_with_error の違いをサンプルコードで確認する
次のようなモデルを定義します。
# カテゴリ
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
挙動の違い確認するテストコード
Minitestでテストコードを書いてみました。
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 が返され、商品にエラーメッセージが付加されます。
refute item.destroy
assert_equal ["Cannot delete record because dependent orders exist"],
             item.errors.messages[:base]
同様に、商品があるとカテゴリは削除できません。
ただし、こちらは restrict_with_exceptionを付けているので、 ActiveRecord::DeleteRestrictionError が発生します。
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
サンプルコードはこちら
このコードはGitHubに置いてあります。気になる人は実際に動かしてみてください。
そもそもなんでこの記事を書こうと思ったのか
restrict_with_error と restrict_with_exception の意味を勘違いしていたからです。
話がちょっとそれますが、 Ruby には Exception クラスと StandardError クラスがあります。
StandardError は Exception のサブクラスです。
参考: rescue節で例外の型を指定しない場合に補足できるのはStandardErrorとそのサブクラスだけ
さらに、 ActiveRecord::DeleteRestrictionError は StandardError のサブクラスになっています。
僕は restrict_with_error と restrict_with_exception の違いを StandardError と Exception の違いだと思っていました。
なので、こうなると思っていたのです。(error と Error が同じという理由で)
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チュートリアル」の正式版を公開しました
- Minitestの技術書としては日本初!?「RSpecユーザのためのMinitestチュートリアル(ベータ版)」を公開しました!
みなさんよろしくお願いします。 m(_ _)m
RSpecユーザのためのMinitestチュートリアル
※「Everyday Rails - RSpecによるRailsテスト入門」を購入してもらうと追加コンテンツとしてダウンロードできます。


