Railsで論理削除を考慮するunique制約のかけ方

使用バージョン

  • Rails 4.2.0
  • Ruby 2.2.0
  • MySQL 5.6
  • 論理削除のgem: paranoia 2.1.0

目的

例えばこういうuserモデルがあります

# 20150119070756_create_users.rb
class CreateUsers < ActiveRecord::Migration  
  def change
    create_table :users do |t|
      t.string :name, :limit => 20

      t.datetime :deleted_at # <- 論理削除
      t.timestamps null: false
    end
  end
end  

注意したいのはdeleted_atコラムは削除した日付です。デフォルトだと、

  • 非削除レコードはdeleted_at IS NULL
  • 削除済みレコードはdeleted_at IS NOT NULL

問題: もしnameunique制約をかけると、削除済みレコードはそのままデータベースに残るので同じnameのレコードを追加することができません。
目的: 非削除レコードの中でnameをunique制約したい。

ネットで調べるとハマる人が少なくないようです。 今回はデータベースレベルとアプリケーションレベルの両方設定します。

作業内容

まずdeleted_atはNULLだとunique複合キーを設定するのも意味ないのでNULLを避けます。

設定ファイルparanoia.rbを作る

# config/initializers/paranoia.rb
Paranoia.default_sentinel_value = DateTime.new(0)  

以上の1行だけで、

  • 非削除レコードはdeleted_at = '0000-01-01 00:00:00'
  • 削除済みレコードはdeleted_at != '0000-01-01 00:00:00'

になります。これで、unique複合キーを設定することができます。

データベースレベル:migration変更

# 20150119070756_create_users.rb
class CreateUsers < ActiveRecord::Migration  
  def change
    create_table :users do |t|
      # 省略
    end

    add_index :users, [:name,:deleted_at], unique: true
  end
end  

アプリケーションレベル: validation追加

# app/models/user.rb
class User < ActiveRecord::Base  
  acts_as_paranoid
  validates :name, uniqueness: { scope: :deleted_at }        

完了!

参考