この記事を書こうと思ったきっかけ
初心者向けにお話をする機会があって、論理削除の話をした際に資料を用意しました。たくさんスクショをとったものが残っていたこともあり、折角なので記事にしてみました。
内容
論理削除についてです。
対象読者
「CRUDは書けるけど、論理削除はピンとこない」という方を対象にしています。
どうやら、スクールとかでもあまり触れられないみたいです。(地味だからでしょうか?)
論理削除とは
データを削除するときに、記憶媒体からデータを削除をしてしまうのではなく、あたかも削除されたように扱うことを論理削除といいます。(本当に削除してしまうことを物理削除といいます。)
操作間違いがあっても、データは消えていないから元に戻せますし、分析を行う際にもデータをとっておくことができるので何かと便利です。
論理削除を実現するには
「そのデータが論理削除されているか否か」を管理するフラグを用意します。
カラム名は何でも良いのですが、deleted
などとして、真偽値で値を持たせたり、deleted_at
という名前にして、タイムスタンプで持たせたり、いろいろな方法があります。「何時くらいに間違えて大量削除をしてしまったんですけど、戻せます?」みたいな時に対応をするには、タイムスタンプの方がちょっと便利かもしれません。
カラムを用意したら、論理削除フラグが立っているものを、あの手この手で削除されたかのように見せていきます。
今回はサンプルのアプリケーションをいじる形で進めていきます。削除フラグは便宜的にflag
という名前で真偽値にしていますが、もちろんあまりいい名前ではありません。お好きな名前や型をお試しください。
アプリケーションを準備
[app_name]
のところに、好きなアプリケーション名をつけてください。
今回は、名前と削除フラグだけを持ったUser
を作成します。
rails new [app_name]
cd [app_name]
rails g scaffold User name:string flag:boolean
rails db:migrate
rails server
以下にアクセスします。
http://localhost:3000/users

こんな画面ができたら準備完了です。
ユーザーを登録する
それでは、2人ユーザーを登録します。一人はflag
にチェックを入れて、もう一人は入れないでください。

ユーザーAの削除フラグを真にしてみました。これからコードに手を加えて、Aが消えてないのに消えているよう見せていきます。
Aを削除されたかのように扱う
今回はindex
とdestroy
の2箇所を書きかえていきます。
indexを書き換える
コントローラーのindex
のところをご覧ください。
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
# GET /users
# GET /users.json
def index
@users = User.all
end
Aは論理削除されたユーザーなので、見えないようにしたいです。削除フラグが立っていないユーザーを取得するように書き換えてみます。
def index
@users = User.where(flag: false)
end

Aが削除されたかのように見えます。
削除を書き換える
scaffold
では、destroy
アクションは物理削除になっていますので、書き換えます。単純にフラグをtrue
にするようにupdate
します。
def destroy
@user.destroy #@user.update(flag: true)に変更
respond_to do |format|
format.html { redirect_to users_url, notice: 'User was successfully
destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
@user = User.find(params[:id])
end
@user
の中身がわかりにくいかもしれませんが、コールバックでset_user
が呼ばれており、User.find(params[:id])
が入っています。
削除を試してみる
それでは、Bも削除してみます。画面上では綺麗に消えたように見えます。
画面から見えなくなっただけで、もちろんデータは存在しています。確認してみます。
irb(main):001:0> User.all
=> #<ActiveRecord::Relation [#<User id: 1, name: "user A", flag: true,
created_at: "2020-03-29 09:30:59", updated_at: "2020-03-29 09:30:59">, #<User
id: 2, name: "User B", flag: true, created_at: "2020-03-29 09:31:09",
updated_at: "2020-03-29 09:43:01">]>
スコープを使う
今回のサンプルではUser.where(flag: false)
は論理削除されていないユーザーを取得しています。
「論理削除されたいないものを取得する」のは何度も使いますから、スコープに切り出しておくと便利です。
今回の話題とは直接関係はないのですが、スコープを作成する際には、事前の結合が必要であったりするなど特定条件下でないと機能しないようなものを作ってしまわないように気をつけてください。(いつでも使えると思って、他の人が何気なく使ってしまいます。)
記述する場所はモデルです。簡単に書き方を紹介するのに留めます。
scope :名前, -> {検索条件}
論理削除されていないユーザーをアクティブなユーザーとして、以下のように書いてみました。
class User < ApplicationRecord
scope :active, -> { where(flag: false) }
scope :deleted, -> { where(flag: true) }
end
詳しくはこちらもご覧ください。
Active Recordクエリインターフェース
コントローラーを書き換える
最後に、今作ったスコープを使って、コントローラーを書き換えてみます。
def index
@users = User.active
end
実際に使うときには
実際のアプリケーションに論理削除を取り入れる時は、今回のサンプルの他にまだ手を加えなければいけない箇所があります。
他のアクションにも対応する
今回は簡単なサンプルなので、index
とdestroy
を見ていくだけですが、他のアクションにも手を加える必要があります。(直接パスを打って入ろうとした場合に、「存在しないユーザーです」などメッセージを出す、専用のリダイレクト先を用意しておく、など)
削除、削除済、以外ができないようにする
知らないうちに削除フラグのカラムの値がtrue
とfalse
とnil
の3つになってしまったりすることがあるかもしれません。そうならないように、default: false
などで確実に初期値を用意しておいたり、バリデーションを用意してnil
を弾いたり、あるいは論理削除されていないユーザーの取得方法を工夫する必要があります。
削除フラグがnil
のユーザーCを用意すると、今回のコードでは削除された訳でも、アクティブなわけでもないことになってしまいます。コンソールで確認してみます。
irb(main):001:0> User.active
=> #<ActiveRecord::Relation []>
irb(main):002:0> User.deleted
=> #<ActiveRecord::Relation
[#<User id: 1, name: "user A", flag: true, created_at: "2020-03-29 09:30:59", updated_at: "2020-03-29 09:30:59">,
# <User id: 2, name: "User B", flag: true, created_at: "2020-03-29 09:31:09", updated_at: "2020-03-29 09:43:01">]>
irb(main):003:0> User.all
=> #<ActiveRecord::Relation
[#<User id: 1, name: "user A", flag: true, created_at: "2020-03-29 09:30:59", updated_at: "2020-03-29 09:30:59">,
# <User id: 2, name: "User B", flag: true, created_at: "2020-03-29 09:31:09", updated_at: "2020-03-29 09:43:01">,
#<User id: 3, name: "User C", flag: nil, created_at: "2020-05-04 19:03:27", updated_at: "2020-05-04 19:03:27">]>
おしまい
お役に立てれば幸いです。ありがとうございました。