新人PGのLaravel備忘録～論理削除の実装～

# はじめに
こちらは筆者がLaravelに関する知識の定着を図るための投稿です。
以下の点にご注意ください
* この記事の読者は私と同じ実務歴半年~1年程度の新人PGの方を想定しています
* 記事内において間違いや冗長な説明が多々あることをご容赦ください
* 公式ドキュメントと相違がある場合はそちらを参考にしてください
* 公の場で記事を引用していただく場合は予めご連絡ください
* コメント大歓迎です！！出来るだけ返信しようと思っております！！
# 今回のテーマ
この記事ではLaravelにおける論理削除の実装について具体的なコードを用いて説明します。[^1]

:::note warn
今回の実装はlaravel8での実装となります。他のバージョン(特に5.x以前)で実装する際はコードが異なる可能性があるため[公式ドキュメント](https://laravel.com/docs/8.x/eloquent)の#Soft Deletingの項をご確認ください。
:::
[^1]:今回の記事はあくまでも「論理削除の実装」に関する記事であり、導入を推奨するものではありません。
# 論理削除とは
そもそも論理削除とは何なのでしょうか。論理削除とはデータベース(DB)におけるレコード削除の考え方の一種です。DB内のレコード削除には主に物理削除と論理削除があります。
これら二つの削除方式には以下のような違いがあります。
|  | 物理削除 | 論理削除 |
|:-:|:-:|:-:|
| 概要 | 対象レコードを完全に削除 | レコード自体は削除せず削除フラグを立てる|
| メリット | DBの容量負荷の軽減<br>処理が高速 | レコードの復元が容易に出来る |
| デメリット | レコードを復元できない[^2] | 保存容量の圧迫<br>処理速度の低下 |

論理削除において重要なのは実際にはレコードを削除せず、削除フラグを立てることで既存のレコードと区別するということです。なので、**論理削除≒使わないレコードを区別する**というように考えることもできます。
[^2]:バックアップやログを別のDBなどに保存してある場合は復元することができます
# Laravelでの論理削除の実装
LaravelはマイグレーションとSoftDeletesトレイトを用いることで簡単に論理削除が実装できます。
## ①マイグレーションの実行
マイグレーションを実行して削除フラグとしてdeleted_atカラムを含んだテーブルを作成します。マイグレーションとはDBのバージョン管理ファイルのようなものです。DBの変更をマイグレーションファイルで毎回行うことによって変更履歴が保存され必要な時に元のDBに巻き戻すことができます。
### 既存のテーブルに新しくdeleted_atカラムを追加する場合
<details><summary>実装コード例</summary>

```terminal
php artisan make:migration add_column_to_existing_table --table=existing_table_name //--table=以降に変更したいテーブル名を入れる
```
これにより変更用のマイグレーションファイルが作成されるので以下のように追加するカラムを記述します。
```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddColumnToExistingTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('existing_table_name', function (Blueprint $table) {
            $table->softDeletes()->nullable();//deleted_atカラムを追加
    }

    public function down()
    {
        Schema::table('existing_table_name', function (Blueprint $table) {
            $table->dropSoftDeletes();
        });
    }

}
```
次にマイグレーションを実行します。
```terminal
php artisan migrate
```
</details>

### deleted_atを含む新しいテーブルを作成する場合
<details><summary>実装コード例</summary>

```terminal
php artisan make:migration create_example_table
```
これにより新規作成用のマイグレーションファイルが作成されるので以下のようにカラムを記述します。
```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateExampleTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('example_table', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps(); // timestampsメソッドはcreated_atおよびupdated_at　TIMESTAMPカラムを作成します。
            $table->softDeletes(); // deleted_atカラムを追加
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('example_table');
    }
}
```
次にマイグレーションを実行します。
```terminal
php artisan migrate
```
</details>

## ②論理削除を使用できるようにモデルで宣言
次に論理削除を実装したいテーブルに対応するモデルファイルを編集します。LaravelにはSoftDeletesトレイトが実装されています。トレイトとはクラス定義時に利用可能なメソッドのセットを提供するためのPHPの仕組みのことです。Pythonを使ったことがある人はライブラリのようなものだと考えると分かりやすいかもしれません。[^3]
[^3]:コードの再利用性を高めるという点では共通していますが、厳密にはライブラリが提供しているのはメソッドが記述されているPythonファイル群です。
<details><summary>実装コード例</summary>

```php:app/Model/ExampleModel.php
<?php

namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;

class ExampleModel extends Model
{
    use SoftDeletes;
    protected $table = 'example_table'; //明示的に対応するテーブル名を指定することもできる
                                        //指定しない場合、複数形のスネークケースにしたもの(example_models)が対応テーブルとみなされる
}
```

</details>

# 具体的な操作例
最後に論理削除を実装した後に具体的にどうやって論理削除を実行するのかを説明します。
## レコードを論理削除
論理削除を実行する場合は通常の削除と同様にdelete()メソッドを呼び出すだけです。

<details open><summary>コード例</summary>

```php:ExampleController.php
<?php

namespace App\Http\Controllers;

use App\Models\ExampleModel;
use Illuminate\Http\Request;

class ExampleController extends Controller
{
    public function delete($id)//削除するレコードのIDを引数にとる
    {
        $example = ExampleModel::find($id);
        if ($example) {
            $example->delete();
            return response()->json(['message' => 'Resource deleted']);
        } else {
            return response()->json(['message' => 'Resource not found'], 404);
        }
    }
}
```
</details>

## 論理削除したレコードを取得する場合

<details open><summary>コード例</summary>

```php:ExampleController.php
<?php

namespace App\Http\Controllers;

use App\Models\ExampleModel;
use Illuminate\Http\Request;

class ExampleController extends Controller
{
    public function show_withTrashed($id)//論理削除されたレコードも含むテーブルを取得
    {
        $example = ExampleModel::withTrashed()->find($id);
        if ($example) {
            return response()->json($example);
        } else {
            return response()->json(['message' => 'Resource not found'], 404);
        }
    }
    
    public function trashed()//論理削除されたレコードのみを取得
    {
        $trashedExamples = ExampleModel::onlyTrashed()->get();
        return response()->json($trashedExamples);
    }
}
```
</details>

## 論理削除したレコードを復元する場合

<details open><summary>コード例</summary>

```php:ExampleController.php
<?php

namespace App\Http\Controllers;

use App\Models\ExampleModel;
use Illuminate\Http\Request;

class ExampleController extends Controller
{
    public function restore($id)//復元したいレコードのIDを引数にとる
    {
        $example = ExampleModel::onlyTrashed()->find($id);
        if ($example) {
            $example->restore();
            return response()->json(['message' => 'Resource restored']);
        } else {
            return response()->json(['message' => 'Resource not found'], 404);
        }
    }
}
```
</details>