25
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rails + PostgreSQL で論理削除と一意性制約を両立させる

Last updated at Posted at 2017-10-24

はじめに

最近読んだ @yuba さんの 論理削除と一意性制約を両立させる方法・DB製品別 という記事で、部分インデックス というインデックスについて知ることができました。そこで今回、Rails + PostgreSQL の環境でこの部分インデックスを活用するには具体的にどうすればいいかをまとめました。

TL;DR

add_indexwhere オプションを使う。

add_index :users, :email, unique: true, where: 'deleted_at IS NULL'

問題

論理削除の機能を使った User モデルがあるとします。deleted_at 列に (削除された際の) 日付が設定された場合に論理削除されていると見なします。ここでは paranoia という Gem を使用していることを想定します。ただし、この記事では Gem を使用するかどうかやその種類は重要ではありません。

次のマイグレーションでは users テーブルの email 列に対して UNIQUE インデックスを作成しています。

db/migrate/20171024065340_create_users.rb
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
app/models/user.rb
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 の引数を次のように更新します。

db/migrate/20171024065340_create_users.rb
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>]

参考

Qiita

Rails

PostgreSQL

25
14
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?