Posted at

ActiveRecordのdestroyでバリデーションをやりたい

More than 5 years have passed since last update.

どうも、ばばです。


destroyするときにバリデーションを走らせたい

たとえば、『destroy出来る条件』みたいなのがあって、その条件をパスしないと消せない、的な感じにしたいとする。

あり得るとしたら、『TwitterとGitHubの両方の認証手段がある(Qiitaみたいにね)んだけど、両方消しちゃったらログインする方法が無くなっちゃうから、両方とも消すことは出来ない』みたいな事がしたかったとする。

方法としては、



  1. destroyアクションの中で弾く

  2. バリデーションを書く

の2つの方法がぱっと思いつくと思う。ただ、バリデーションの方はちょっと特殊なことをやらなきゃいけないし、アクションの中で書くのは『えーそれモデルの仕事でしょー』感がハンパない。負けた感ある。

というわけで、今回はバリデーション側でなんとかスマートに解決させることは出来ないか、っていうのをやってみようと思う。


before_destroyを活用する

before_destroyについては、ActiveRecord::Callbacksに詳しく書いてある。

たとえば、こんな使い方をする。


user.rb

class User < ActiveRecord::Base

before_destroy { |record| Post.destroy_all(user_id: record.id) }
end

いや、dependentオプションでやれよ、っていうのは気にしないで。

また、こいつ、メソッド名を渡すことも可能だ。


user.rb

class User < ActiveRecord::Base

before_destroy :delete_all_his_posts

private

def destroy_all_his_posts
self.posts.delete_all
end
end


これを利用してどうにかしてみよう。

たとえば、saveメソッドは、truefalseを返してくる。trueなら保存成功、falseなら保存失敗だ。

しかし、destroyは基本的に自分自身を返してくる。これに『もし何かに引っかかってエラーになったら、falseを返す』ようにしたら、saveと同じような挙動にすることが出来るのではないか。


login_provider.rb

class LoginProvider < ActiveRecord::Base

belongs_to :user

before_destroy :user_should_have_at_least_one_login_provider

private

def user_should_have_at_least_one_login_provider
# もしユーザーのLoginProviderが1つしか無いのに、消そうとしていたら
if self.user.login_providers.length == 1
errors.add :base, '少なくとも1つ、ログイン用の認証が必要です'
return false
end
end
end


すると、

if @login_provider.destroy

redirect_to user_login_providers_path, notice: '削除しました'
else
redirect_to user_login_providers_path, notice: '削除できませんでした'
end

こんなことが出来るようになる。

もちろん、Viewで@login_provider.errorsを表示すれば、ちゃんと『少なくとも1つ、ログイン用の認証が必要です』と表示される。

ちょっと強引だけど、こんなことをしなきゃいけない時点でだいぶレール踏み抜いてる感があるので、まぁ仕方ないかなぁ。

だいたいそんな感じ( ˘ω˘)