論理削除を考える

論理削除という単語を学んだので、Rails でやってみる。

論理削除とは

論理削除とは?という答えのためには「物理削除」とは?を理解している必要がありました。
ということで以下の通り理解しました。

  • データベースのデータを本当に消してしまうのが「物理削除」
  • データに「削除したってことにするフラグを立てる」のが「論理削除」

自前でやってみる

任意のディレクトリで以下の通り実行して、準備する。

モデルの作成

1
2
3
4
5
6
7
8
9
bundle exec rails g scaffold user name:string deleted:boolean

# db/migrate/20200113130113_create_users.rb
# t.boolean :deleted
# を
# t.boolean :deleted, default: false, null: false
# に書き換える

bundle exec rails db:migrate

モデルの修正

app/models/user.rb を以下のように修正する。

app/models/user.rb
1
2
3
4

class User < ApplicationRecord
scope :active, ->{where(deleted: :false)}
end

コントローラの修正

app/controllers/users_controller.rb を以下のように修正します。
先に作った active をはさむ形で、.all と.find を実行するようにし、
destroy アクションは、deleted カラムを true に書き換えます。

app/controllers/users_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class UsersController < ApplicationController
# 省略
def index
@users = User.active.all
end

# 省略

def destroy
@user.update(deleted:true)
respond_to do |format|
format.html { redirect_to users_url, notice: 'User was successfully destroyed.' }
format.json { head :no_content }
end
end

# 省略

private
def set_user
@user = User.active.find(params[:id])
end

def user_params
params.require(:user).permit(:name)
end
end

ビューの修正

deleted カラムを表示、入力項目にしないように修正。

app/views/users/_form.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<%= form_with(model: user, local: true) do |form| %>
<% if user.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>

<ul>
<% user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>

<!-- ここから削除 -->
<div class="field">
<%= form.label :deleted %>
<%= form.text_field :deleted %>
</div>
<!-- ここまで削除 -->

<div class="actions">
<%= form.submit %>
</div>
<% end %>

上記に加え以下 2 つを編集する。

  • app\views\users\index.html.erb
  • app\views\users\show.html.erb

確認

bundle exec rails sで実行し、localhost:3000/users にアクセスする。
いつもの画面になっている。

適当にデータを追加して、削除してみる。

見た目ではデータが消えているように見える。

rails コンソールで確認してみる。

1
2
3
4
bundle exec rails c
irb(main):009:0> User.all
User Load (0.2ms) SELECT "users".* FROM "users" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 1, name: "test", deleted: true, created_at: "2020-01-1313:13:06", updated_at: "2020-01-13 13:13:06">]>

確かに、deleted が true になっていて、実データが保存されている。
論理削除ができた。


gem でやってみる

論理削除を実現するための gem は様々あるそうです。
今回は、github のスターが多かったparanoiaを導入してみます。
(しかし、paranoia って穏やかな名前じゃないな。)

paranoia の導入

Gemfile に以下の通り追記。

1
gem "paranoia", "~> 2.2"

追記出来たら、bundle installを実行。

モデルの作成 2

以下コマンドで、モデルの作成とカラムの追加を実施。

1
2
3
4
5
6
7
# モデルの作成
bundle exec rails g scaffold guest name:string
bundle exec rails db:migrate

# deleted_atカラムの追加
bundle exec rails g migration AddDeletedAtToGuests deleted_at:datetime:index
bundle exec rails db:migrate

モデルの修正 2

app/models/guest.rb を通り修正し、acts_as_paranoidを記述し以下のようにする。

1
2
3
class Guest < ApplicationRecord
acts_as_paranoid
end

確認 2

bundle exec rails sで実行し、localhost:3000/guests にアクセスする。
いつもの画面になっている。

適当にデータを追加して、削除してみる。

今回は 2 件登録して 1 件だけ削除しています。
見た目ではデータが消えているように見える。

rails コンソールで確認してみる。

実行コマンド(任意に改行挿入済み)
1
2
3
4
5
bundle exec rails c
irb(main):001:0> Guest.all
Guest Load (0.2ms) SELECT "guests".* FROM "guests" WHERE "guests"."deleted_at" IS NULL LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation[
#<Guest id: 2, name: "test1", created_at: "2020-01-13 14:25:12", updated_at: "2020-01-13 14:25:12", deleted_at: nil>]>

あれ?全件出てこないと思ったが、WHERE "guests"."deleted_at" IS NULL LIMITが SQL に挿入されていた。
全件取得には.with_deleteを使うそうだ。

実行コマンド(任意に改行挿入済み)
1
2
3
4
5
irb(main):002:0> Guest.with_deleted
Guest Load (0.2ms) SELECT "guests".* FROM "guests" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [
#<Guest id: 1, name: "test", created_at: "2020-01-13 14:17:28", updated_at: "2020-01-13 14:17:40", deleted_at: "2020-01-13 14:17:40">,
#<Guest id: 2, name: "test1", created_at: "2020-01-13 14:25:12", updated_at: "2020-01-13 14:25:12", deleted_at: nil>]>

deleted_at に値が入ったものと、nil のものが確認できました。

paranoiaには、論理削除から元に戻す.restoreなども用意されていました。
自前で作った時と大きく違うのは、既存のメソッド.All.destroy をオーバーライドすることです。
論理削除を意識せずに使えるけど、消したつもりで消えてないなんてこともありそうなので気を付けて使用する必要がありそうです。


今回は論理削除を 2 種類の方法でやってみました。
自前でやってみて基本原理の確認、gem で便利なオーバーライドを確認しました。
これまで削除するときには、物理削除しか知らなかったのでしっかり消してきましたが今後は論理削除を頭においておきます。

ではでは。