一から勉強させてください( ̄ω ̄;)

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです。

Rails4 + devise + paranoiaで論理削除を考慮したユーザーバリデーションを実現する

Railsでユーザー登録、認証機能を実装する際、deviseを使用することはわりと一般的かと思います。

またユーザーの論理削除を実現するためのgemとしてはparanoiaが有名かと思います。(Rails3時代によく使われていたacts_as_paranoidをよりシンプルにしたやつ)

どちらも素晴らしいgemなのですがこれらを同時に使用した際、以下のような問題が発生しました。

  1. deviseのvalidatableを使用してUserモデルのvalidationを実装 (email, password関連のvalidationが追加される)。
  2. paranoiaacts_as_paranoidメソッドを呼んで、ユーザーの論理削除をできるようにする。
  3. email: hoge@hoge.comのユーザーhogeemail: fuga@fuga.comのユーザーfugaを用意する。
  4. fuga.destroyでfugaを論理削除。
  5. hoge.update_attributes(email: 'fuga@fuga.com')hogeのemailを更新しようとするとユニーク制約でvalidationエラー発生。

こんな感じで削除済のユーザーと同じemailを登録しようとするとユニーク制約にひっかかってしまって困りました。

今回はこれを通常のユーザーのemailユニーク制約は残しつつ、削除済みユーザーはその対象外となるよう修正していきたいと思います。

とりあえずvalidatableを外す

まずはdeviseのvalidatableを外して、必要なコードを本家からコピーしてくる。

base.class_eval do
  validates_presence_of   :email, if: :email_required?
  #validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
  validates_format_of     :email, with: email_regexp, allow_blank: true, if: :email_changed?

  validates_presence_of     :password, if: :password_required?
  validates_confirmation_of :password, if: :password_required?
  validates_length_of       :password, within: password_length, allow_blank: true
end

必要そうなのはこの辺り。ただvalidates_uniqueness_ofはこのままだとダメなので一旦外す。

DBのindexを修正する

devise導入時のmigrationで

class DeviseCreateUsers < ActiveRecord::Migration
 中略
  add_index :users, :email, unique: true
end

のようにしていると思うので、その前提で。

これだとdeleted_atに関係なくユニーク制約がついてしまっているので、これを修正するためのmigrationファイルを作成する。

class UpdateIndexUsersOnEmail < ActiveRecord::Migration
  def up
    remove_index :users, :email
    execute 'CREATE UNIQUE INDEX unique_index_on_users_email ON users(email) WHERE deleted_at IS NULL'
  end

  def down
    execute 'DROP INDEX unique_index_on_users_email'
    add_index :users, :email, unique: true
  end
end

こんな感じでdeleted_atnullの場合のみユニークとする。これでDB側の修正はOKのはず。

paranoia_uniqueness_validatorを使う

次にModel側の修正。 paranoia_uniqueness_validatorというgemを使用する。これはacts_as_paranoid

class User < ActiveRecord::Base
  validates_as_paranoid
  validates_uniqueness_of_without_deleted :email
end

のようにしていた機能がparanoiaではparanoia_uniqueness_validatorというgemに切り離されたので、これを別途追加する必要があるといった感じ。

こちらの記事とか参考にさせていただきました。

んで、paranoia_uniqueness_validatorでは以下のようにvalidationを追加する。

class User < ActiveRecord::Base
  validates :email, uniqueness_without_deleted: true, allow_blank: true, if: :email_changed?
end

これで期待通りの挙動になるよう修正できた、、はず。

まとめ

gemは便利だけど、思い通りにいかない時は自力で頑張る必要があるということですね。。世知辛い。 もしなにかおかしな所がございましたら、コメント等頂けると嬉しいです!!

参考