dependent: :restrict_with_error と :restrict_with_exception の違い

  • 68
    Like
  • 3
    Comment

はじめに: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_errorrestrict_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

モデル図で表すとこんな感じです。
diagram.jpg

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

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に置いてあります。気になる人は実際に動かしてみてください。

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

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

restrict_with_errorrestrict_with_exception の意味を勘違いしていたからです。

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

参考: rescue節で例外の型を指定しない場合に補足できるのはStandardErrorとそのサブクラスだけ

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

僕は restrict_with_errorrestrict_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_errorrestrict_with_exception 、 StandardError と Exception の違いを正しく理解し、バグのないRubyプログラムを書きましょう!!

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

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

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

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

RSpecユーザのためのMinitestチュートリアル
※「Everyday Rails - RSpecによるRailsテスト入門」を購入してもらうと追加コンテンツとしてダウンロードできます。
20150630033146.jpg