devise_invitableのインスタンスメソッド版invite!は、原則として保存済みのUserに対して使う
TL;DR(最初に結論)
- devise_invitableにはクラスメソッド版の
invite!
とインスタンスメソッド版のinvite!
がある - インスタンスメソッド版の
invite!
は新規Userではなく、保存済みのUserに対して呼び出す - 新規Userに対してインスタンスメソッド版の
invite!
を呼び出すと常に未検証のまま保存されるため、不正な状態でデータが保存される恐れがある
対象バージョン
本記事の内容は以下の環境で動作確認しました。
- devise_invitable 2.0.1
- devise 4.6.2
- Rails 5.2.3
はじめに
DeviseにはUser作成時に招待メールを送ってパスワードの設定を後回しにできる、devise_invitableというgemがあります。
標準的な使い方であれば、「Devise::InvitationsController
に全部おまかせ」で終わるのですが、実装の要件によってはプログラム上で招待メールを送るタイミングをコントロールしたい場合があります。
# Userの作成とメール送信を同時に行う
User.invite!(email: '[email protected]')
User.exists?(email: '[email protected]')
#=> true
ただし、この方法だとバリデーションが実行されません。
# 入力必須のnameを未入力の状態でinvite!する
User.invite!(email: '[email protected]', name: '')
# 名前が未入力のまま保存されてしまう(招待メールも送信されてしまう)
User.exists?(email: '[email protected]')
#=> true
これを防止するにはvalidate_on_invite
オプションを使います。
class User < ApplicationRecord
devise :invitable, :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, validate_on_invite: true
validates :name, presence: true
end
こうすると、検証失敗時にUserの保存と招待メールの送信が中止されます。
user = User.invite!(email: '[email protected]', name: '')
# 検証エラーがあると保存されない(招待メールも送信されない)
user.persisted?
#=> false
User.exists?(email: '[email protected]')
#=> false
問題:インスタンスメソッド版のinvite!は検証エラーの有無に関係なく処理が進んでしまう
さて、ここからが本題です。
devise_invitabaleにはクラスメソッド版のinvite!
とインスタンスメソッド版のinvite!
があります。
上で使ったのはクラスメソッド版です。
インスタンスメソッド版を使うと先ほどのコードは次のように書き直すことができるように思えます。
ですが、クラスメソッド版とは異なり、バリデーションは実行されません。
そのため、Userの保存と招待メールの送信が実行されてしまいます。
user = User.new(email: '[email protected]', name: '')
# 名前が未入力の状態でインスタンスメソッド版のinvite!を使う
user.invite!
# バリデーションが実行されず、Userが保存されてしまう(招待メールも送信される)
user.persisted?
#=> true
User.exists?(email: '[email protected]')
#=> true
また、上の実行結果を見てもわかるとおり、検証エラーが起きる状態でinvite!
を呼びだしても例外が発生することもありません。
!
が付いているので、save!
メソッドのように検証エラー時に例外が発生することを期待する人がいるかもしれませんが、ActiveRecordのsave!
と同じ感覚でinvite!
を呼び出すことはできません。
(そもそも!
なしのinvite
メソッドは、インスタンスメソッド版にもクラスメソッド版にもありません)
対処方法:インスタンスメソッド版のinvite!は保存済みのUserに対して使う
devise_invitableのREADMEを見ると、インスタンスメソッド版のinvite!
は次のように説明されています。
Sending an invitation after user creation(User作成後に招待メールを送信する)
You can send an invitation to an existing user if your workflow creates them separately:
(他の方法でUserを作成するワークフローになっている場合は、既存のUserに対して招待メールを送信することもできます)
user = User.find(42)
user.invite!(current_user) # invited_by属性を指定するcurrent_userはオプションです
ご覧のとおり、インスタンスメソッド版のinvite!
メソッドは、保存済みのUserに対して呼び出されることを想定していることがわかります。
実装を確認する
ちなみに、インスタンスメソッド版のinvite!
メソッドの実装は次のようになっています。
def invite!(invited_by = nil, options = {})
# ...
run_callbacks :invitation_created do
# ...
if save(validate: false)
self.invited_by.decrement_invitation_limit! if !was_invited and self.invited_by.present?
deliver_invitation(options) unless skip_invitation
end
end
end
コードを見るとわかりますが、save(validate: false)
でレコードを保存しています。
このため、新規Userの場合でも保存処理自体は実行されますが、validate: false
オプションが付いているため、validate_on_invite
のありなしに関わらず、常に未検証のまま保存されます。
もちろん、「未検証のまま保存される」というのは既存のレコードに対しても(=update時も)同じです。
ですので、updateのタイミングで動いてほしい重要なバリデーションがある場合は、自前で検証のステップを入れた方が良いかもしれません。
user = User.find(42)
# 既存のUserに対して、検証エラーがないことを確認してからinvite!を実行する
if user.valid?
user.invite!(current_user)
end
ですが、厳密にいうと、上のような書き方をしてもまだinvite!
に失敗する可能性はあります。
なぜなら、valid?
がtrueを返してもレコードの保存に失敗するケースがあるからです。
詳しい内容は以下の記事にまとめてあるので、こちらをご覧ください。
valid?がtrueを返しても、after_save、after_create、after_updateによって保存が失敗する可能性を考慮する - Qiita
まとめ
というわけで、devise_invitableを利用する場合は、「Devise::InvitationsController
に全部おまかせ」で終わらせるのが理想的ですが、それが無理な場合は極力クラスメソッド版のinvite!
を利用するようにしましょう。
もし、インスタンスメソッド版のinvite!
を使いたくなったときは、原則として保存済みのUserに対して使うようにしてください。
Author And Source
この問題について(devise_invitableのインスタンスメソッド版invite!は、原則として保存済みのUserに対して使う), 我々は、より多くの情報をここで見つけました https://qiita.com/jnchito/items/689a7069f02ce14d1a3f著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .