はじめに
こんにちは、nayaaaaです。
今回は物理削除と論理削除についてまとめてみました。
Railsでの動きを実際に確認しながら確認してみます。
物理削除、論理削除
イメージ図のようになっております。
物理削除
データベースからレコードそのものを完全に削除する。destroy などで行うと、DB上からデータが消え、取得できなくなる。
論理削除
実際にはレコードを残したまま、フラグを立てて「削除されたように見せる」仕組み。
そのため、削除後もデータ自体は残っており、後からリストア(復元)することも可能。
物理削除
practice(dev)> article = Article.create!(title: "test")
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Practice'*/
Article Create (4.9ms) INSERT INTO "articles" ("title", "body", "deleted_at", "created_at", "updated_at") VALUES ('test', NULL, NULL, '2025-08-17 01:49:55.076870', '2025-08-17 01:49:55.076870') RETURNING "id" /*application='Practice'*/
TRANSACTION (0.4ms) COMMIT TRANSACTION /*application='Practice'*/
=>
#<Article:0x000000012cb9dac0
...
practice(dev)> article = Article.find_by(id: article.id)
Article Load (6.1ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" = 9 LIMIT 1 /*application='Practice'*/
=>
#<Article:0x000000012cbbb7c8
...
practice(dev)> article.destroy
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Practice'*/
Article Destroy (1.7ms) DELETE FROM "articles" WHERE "articles"."id" = 9 /*application='Practice'*/
TRANSACTION (1.1ms) COMMIT TRANSACTION /*application='Practice'*/
=>
#<Article:0x000000012cbbb7c8
id: 9,
title: "test",
body: nil,
deleted_at: nil,
created_at: "2025-08-17 01:49:55.076870000 +0000",
updated_at: "2025-08-17 01:49:55.076870000 +0000">
practice(dev)> article = Article.find_by(id: article.id)
Article Load (0.5ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" = 9 LIMIT 1 /*application='Practice'*/
=> nil
実際にDELETE FROM "articles"
が発行され、データベースから完全に削除されます。
論理削除
practice(dev)> a = Article.create!(title: "test")
TRANSACTION (0.3ms) BEGIN immediate TRANSACTION /*application='Practice'*/
Article Create (2.9ms) INSERT INTO "articles" ("title", "body", "deleted_at", "created_at", "updated_at") VALUES ('test', NULL, NULL, '2025-08-16 23:41:46.723211', '2025-08-16 23:41:46.723211') RETURNING "id" /*application='Practice'*/
TRANSACTION (0.8ms) COMMIT TRANSACTION /*application='Practice'*/
=>
#<Article:0x000000012cbb7308
...
practice(dev)> Article.alive.count
Article Count (0.3ms) SELECT COUNT(*) FROM "articles" WHERE "articles"."deleted_at" IS NULL /*application='Practice'*/
=> 1
practice(dev)> a.soft_delete!
TRANSACTION (0.2ms) BEGIN immediate TRANSACTION /*application='Practice'*/
Article Update (1.4ms) UPDATE "articles" SET "deleted_at" = '2025-08-16 23:43:59.140462', "updated_at" = '2025-08-16 23:43:59.141716' WHERE "articles"."id" = 6 /*application='Practice'*/
TRANSACTION (0.4ms) COMMIT TRANSACTION /*application='Practice'*/
=> true
practice(dev)> Article.alive.count
Article Count (0.3ms) SELECT COUNT(*) FROM "articles" WHERE "articles"."deleted_at" IS NULL /*application='Practice'*/
=> 0
practice(dev)> Article.deleted.count
Article Count (0.3ms) SELECT COUNT(*) FROM "articles" WHERE "articles"."deleted_at" IS NOT NULL /*application='Practice'*/
=> 1
practice(dev)> a.soft_deleted?
=> true
deleted_atカラムに値が入り非表示扱いになリます。
practice(dev)> Article.all
Article Load (1.2ms) SELECT "articles".* FROM "articles" /* loading for pp */ LIMIT 11 /*application='Practice'*/
=>
[#<Article:0x000000012cbbcf88
id: 6,
title: "test",
body: nil,
deleted_at: "2025-08-16 23:43:59.140462000 +0000",
created_at: "2025-08-16 23:41:46.723211000 +0000",
updated_at: "2025-08-16 23:43:59.141716000 +0000">]
データ自体は残っているので、allで確認すると、削除済みでもレコード自体は残っていることが分かります。
リストアしてみる
practice(dev)> a.restore!
TRANSACTION (1.3ms) BEGIN immediate TRANSACTION /*application='Practice'*/
Article Update (28.9ms) UPDATE "articles" SET "deleted_at" = NULL, "updated_at" = '2025-08-17 00:29:06.214015' WHERE "articles"."id" = 6 /*application='Practice'*/
TRANSACTION (0.4ms) COMMIT TRANSACTION /*application='Practice'*/
=> true
practice(dev)> Article.alive.count
Article Count (6.1ms) SELECT COUNT(*) FROM "articles" WHERE "articles"."deleted_at" IS NULL /*application='Practice'*/
=> 1
practice(dev)> Article.deleted.count
Article Count (0.3ms) SELECT COUNT(*) FROM "articles" WHERE "articles"."deleted_at" IS NOT NULL /*application='Practice'*/
=> 0
practice(dev)> a.soft_deleted?
=> false
データ自体は残っているので、先述した通りリストア(復元)も可能です。
複数レコードの場合
複数のレコードを保持している場合は、以下のようにfindを使用して指定しましょう
practice(dev)> Article.alive.pluck
Article Pluck (0.6ms) SELECT "articles".* FROM "articles" WHERE "articles"."deleted_at" IS NULL /*application='Practice'*/
=>
[[6,
"test",
nil,
nil,
2025-08-16 23:41:46.723211000 UTC +00:00,
2025-08-17 00:29:06.214015000 UTC +00:00],
[7,
"test",
nil,
nil,
2025-08-17 00:48:25.173997000 UTC +00:00,
2025-08-17 00:48:25.173997000 UTC +00:00]]
practice(dev)> a = Article.find(6)
Article Load (0.3ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" = 6 LIMIT 1 /*application='Practice'*/
=>
#<Article:0x000000012cbbdc08
...
practice(dev)> a.soft_delete!
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Practice'*/
Article Update (1.7ms) UPDATE "articles" SET "deleted_at" = '2025-08-17 00:56:06.342790', "updated_at" = '2025-08-17 00:56:06.343718' WHERE "articles"."id" = 6 /*application='Practice'*/
TRANSACTION (0.8ms) COMMIT TRANSACTION /*application='Practice'*/
=> true
practice(dev)> Article.alive.count
Article Count (0.3ms) SELECT COUNT(*) FROM "articles" WHERE "articles"."deleted_at" IS NULL /*application='Practice'*/
=> 1
コールバック(before_destroy/after_destroy)
オブジェクトの削除時(レコードの削除)以下を実行。
- before_destroy
レコードが破棄される前に呼び出されるコールバックを登録します。
- after_destroy
レコードが破棄された後に呼び出されるコールバックを登録します。
これらを設定ファイルに記述した場合は、コールバックを呼び出し際に、以下の順番で呼び出されます。
記述されていない場合はスキップされます。
公式ドキュメントからの引用
3 利用可能なコールバック
Active Recordで利用可能なコールバックの一覧を以下に示します。これらのコールバックは、実際の操作中に呼び出される順序に並んでいます。
【省略】
3.3 オブジェクトのdestroy
before_destroy
around_destroy
after_destroy
公式ドキュメント
まとめ
- 物理削除はデータを完全に削除、論理削除はフラグで「非表示化」。
- 論理削除ならデータを復元できるが、条件指定を忘れると「削除済み」も参照してしまう点に注意。
- コールバックを使えば削除時に処理を挟めることができる。
学びながら、意外と覚えておくと使い分けに役立つのかなと感じました。
以上です。ありがとうございました!!