はじめに
最近読んだ @yuba さんの 論理削除と一意性制約を両立させる方法・DB製品別 という記事で、部分インデックス というインデックスについて知ることができました。そこで今回、Rails + PostgreSQL の環境でこの部分インデックスを活用するには具体的にどうすればいいかをまとめました。
TL;DR
add_index の where
オプションを使う。
add_index :users, :email, unique: true, where: 'deleted_at IS NULL'
問題
論理削除の機能を使った User モデルがあるとします。deleted_at
列に (削除された際の) 日付が設定された場合に論理削除されていると見なします。ここでは paranoia という Gem を使用していることを想定します。ただし、この記事では Gem を使用するかどうかやその種類は重要ではありません。
次のマイグレーションでは users
テーブルの email
列に対して UNIQUE インデックスを作成しています。
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :email, null: false
t.string :name, null: false
t.datetime :deleted_at, index: true
t.timestamps
end
add_index :users, :email, unique: true
end
end
class User < ApplicationRecord
# 論理削除を使用する。
acts_as_paranoid
end
UNIQUE インデックス (一意性制約) により、同じメールアドレスを持つ User レコードを作成できません。
maho = User.create(name: '西住 まほ', email: 'nishizumi@example.com')
#=> #<User:0x00007fd7b6dd72b0
# id: 1,
# email: "nishizumi@example.com",
# name: "西住 まほ",
# deleted_at: nil,
# created_at: Tue, 24 Oct 2017 16:05:46 JST +09:00,
# updated_at: Tue, 24 Oct 2017 16:05:46 JST +09:00>
User.create(name: '西住 みほ', email: 'nishizumi@example.com')
# ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_users_on_email"
既に同じメールアドレスを持つレコードを (物理削除の挙動を論理削除にオーバーライドされた) destroy
メソッドで削除しても結果は同じです。ここでの削除はあくまで論理削除であり、同じメールアドレスのレコードは依然としてデータベースに存在するためです。どうしても作成したい場合は、論理削除されたレコードをさらに物理削除するしかありません。
maho = User.find_by(name: '西住 まほ', email: 'nishizumi@example.com')
maho.destroy
#=> #<User:0x00007fd7b6dd72b0
# id: 1,
# email: "nishizumi@example.com",
# name: "西住 まほ",
# deleted_at: Tue, 24 Oct 2017 16:07:17 JST +09:00,
# created_at: Tue, 24 Oct 2017 16:05:46 JST +09:00,
# updated_at: Tue, 24 Oct 2017 16:07:17 JST +09:00>
User.count
#=> 0
# 論理削除されたレコードの中に email: nishizumi@example.com のものが存在するため
# 作成に失敗する。
User.create(name: '西住 みほ', email: 'nishizumi@example.com')
# ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_users_on_email"
そこで 論理削除されていないレコードのみに一意性制約を適用 したいです。
解決方法
マイグレーションの add_index
の引数を次のように更新します。
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :email, null: false
t.string :name, null: false
t.datetime :deleted_at, index: true
t.timestamps
end
# where オプションを追加した。
add_index :users, :email, unique: true, where: 'deleted_at IS NULL'
end
end
こうすることで deleted_at
列が NULL
のレコード、すなわち論理削除されていないレコードにのみ UNIQUE インデックス (一意性制約) が適用されます。
maho = User.create(name: '西住 まほ', email: 'nishizumi@example.com')
#=> #<User:0x00007f87a5931de8
# id: 1,
# email: "nishizumi@example.com",
# name: "西住 まほ",
# deleted_at: nil,
# created_at: Tue, 24 Oct 2017 16:31:33 JST +09:00,
# updated_at: Tue, 24 Oct 2017 16:31:33 JST +09:00>
User.create(name: '西住 みほ', email: 'nishizumi@example.com')
# ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_users_on_email"
maho.destroy
#=> #<User:0x00007f87a5931de8
# id: 1,
# email: "nishizumi@example.com",
# name: "西住 まほ",
# deleted_at: Tue, 24 Oct 2017 16:31:48 JST +09:00,
# created_at: Tue, 24 Oct 2017 16:31:33 JST +09:00,
# updated_at: Tue, 24 Oct 2017 16:31:48 JST +09:00>
User.count
#=> 0
# 論理削除されたレコードの中に同じメールアドレスのものが存在するが、
# 部分インデックスの効果で作成に成功する。
User.create(name: '西住 みほ', email: 'nishizumi@example.com')
#=> #<User:0x00007f87a5a42390
# id: 2,
# email: "nishizumi@example.com",
# name: "西住 みほ",
# deleted_at: nil,
# created_at: Tue, 24 Oct 2017 16:32:02 JST +09:00,
# updated_at: Tue, 24 Oct 2017 16:32:02 JST +09:00>
# 論理削除されているレコードを含めた User レコードを取得する。
# 同じメールアドレスを持つレコードが 2 件存在する。
User.with_deleted
#=> [#<User:0x00007f87a174cd88
# id: 1,
# email: "nishizumi@example.com",
# name: "西住 まほ",
# deleted_at: Tue, 24 Oct 2017 16:31:48 JST +09:00,
# created_at: Tue, 24 Oct 2017 16:31:33 JST +09:00,
# updated_at: Tue, 24 Oct 2017 16:31:48 JST +09:00>,
# #<User:0x00007f87a1739198
# id: 2,
# email: "nishizumi@example.com",
# name: "西住 みほ",
# deleted_at: nil,
# created_at: Tue, 24 Oct 2017 16:32:02 JST +09:00,
# updated_at: Tue, 24 Oct 2017 16:32:02 JST +09:00>]