1. Home
  2. PHP
  3. CakePHP
  4. CakePHPで論理削除(ソフトデリート)を実現する(cakephp3-soft-delete)

CakePHPで論理削除(ソフトデリート)を実現する(cakephp3-soft-delete)

  • 公開日
  • カテゴリ:CakePHP
  • タグ:PHP,CakePHP,cakephp3-soft-delete,SoftDelete
CakePHPで論理削除(ソフトデリート)を実現する(cakephp3-soft-delete)

CakePHP の delete(削除)メソッドは、デフォルトでは物理削除が行われます。

でも要件によっては論理削除を行いたい場合があると思います。

その時にもし、単純に delete カラムを作り、1 を仕込んで更新する方式の場合、一つだけ潜在的な不具合の種を蒔く事になります。

それは、SELECT する時には常に delete が 1 ではない事を条件として加えなければならないという事です。

もしどこかでこの条件を含め忘れると、削除したはずのデータも結果として現れる為、不具合となります。

そして、「削除」という動作を行うのにも関わらず delete メソッドが使えなくなります。(ちょっと悲しい)

今回はそれらを全て解決する為に、cakephp3-soft-delete というプラグインを使って、論理削除(ソフトデリート)を実現していきます。

[Github]CakeSoftDelete plugin for CakePHP
https://github.com/PGBI/cakephp3-soft-delete

Contents

  1. 開発環境
  2. プラグインの導入
  3. マイグレーション
  4. モデル設定
  5. 動作確認
  6. 関連メソッド

開発環境

今回の開発環境に関しては以下の通りです。

  • PHP 7.2
  • CakePHP 3.5

尚、CakePHP のルートディレクトリを「cakephp/」とします。

プラグインをで導入するのに composer を使用します。

プラグインの導入

まずは論理削除を実現する為のプラグイン「PGBI/cakephp3-soft-delete 」をインストールします。

CakePHP のルートディレクトリへ移動し、以下の composer コマンドを叩きます。

# CakePHP のルートディレクトリへ移動
cd /path/to/cakephp

# プラグインのインストール
composer require pgbi/cakephp3-soft-delete "~1.0"

#実行結果
[demo@localhost cakephp]# composer require pgbi/cakephp3-soft-delete "~1.0"
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing pgbi/cakephp3-soft-delete (1.3.0): Downloading (100%)         
Writing lock file
Generating autoload files
> Cake\Composer\Installer\PluginInstaller::postAutoloadDump

bootstrap.php にプラグインの読み込みを記述します。

cakephp/config/bootstrap.php
Plugin::load('SoftDelete');

マイグレーション

次に、実際に操作を行うテーブルを作成します。

今回は CakePHP にデフォルトで定義されている。 Users テーブルを使っていきます。

マイグレーションファイルの記述は以下のようになります。

cakephp/config/Migrations/XXXX_CreateUsers.php
<?php
use Migrations\AbstractMigration;

class CreateUsers extends AbstractMigration
{
    public function change()
    {
        $table = $this->table('users');
        $table->addColumn('username', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('email', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('password', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('deleted', 'datetime', [
            'default' => null,
            'null' => true,
        ]);
        $table->addColumn('last_login_at', 'datetime', [
            'default' => null,
            'null' => true,
        ]);
        $table->addColumn('created', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('modified', 'datetime', [
            'default' => null,
            'null' => true,
        ]);
        $table->addIndex([
            'email',
        ], [
            'name' => 'EMAIL_INDEX',
            'unique' => true,
        ]);
        $table->create();
    }
}

今回は、deleted カラムに対して datetime 型を指定し、それを論理削除の為のカラムにしています。また、デフォルトは null にしておきます。

ついでに、適当な初期データも seeding で定義しておきます。

<?php
use Migrations\AbstractSeed;
use Cake\Auth\DefaultPasswordHasher;

/**
 * Users seed.
 */
class UsersSeed extends AbstractSeed
{
    public function run()
    {
        $datetime = date('Y-m-d H:i:s');
        $data = [
            [
                'username' => 'test01',
                'email' => 'test01@test.com',
                'password' => $this->_setPassword(123456),
                'created' => $datetime,
            ],
            [
                'username' => 'test02',
                'email' => 'test02@test.com',
                'password' => $this->_setPassword(123456),
                'created' => $datetime,
            ],
            [
                'username' => 'test03',
                'email' => 'test03@test.com',
                'password' => $this->_setPassword(123456),
                'created' => $datetime,
            ],
            [
                'username' => 'test04',
                'email' => 'test04@test.com',
                'password' => $this->_setPassword(123456),
                'created' => $datetime,
            ],
            [
                'username' => 'test05',
                'email' => 'test05@test.com',
                'password' => $this->_setPassword(123456),
                'created' => $datetime,
            ],
        ];

        $table = $this->table('users');
        $table->insert($data)->save();
    }

    protected function _setPassword($value)
    {
        $hasher = new DefaultPasswordHasher();
        return $hasher->hash($value);
    }
}

これらを実行して、テーブルを作成します。

# マイグレーション実行
bin/cake migrations migrate

# シーディング実行
bin/cake migrations seed

モデル設定

モデル側にプラグインを使うように設定します。三か所を追記します。

cakephp/src/Model/Table/UsersTable.php
<?php
namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use SoftDelete\Model\Table\SoftDeleteTrait; // ← 追記

class UsersTable extends Table
{
    use SoftDeleteTrait; // ← 追記

    protected $softDeleteField = 'deleted'; // ← 追記

    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('users');
        $this->setDisplayField('id');
        $this->setPrimaryKey('id');

        $this->addBehavior('Timestamp', [
            'events' => [
                'Model.beforeSave' => [
                    'created' => 'new',
                    'modified' => 'always'
                ],
            ]
        ]);
    }

    public function validationDefault(Validator $validator)
    {
        $validator
            ->integer('id')
            ->allowEmpty('id', 'create');

        $validator
            ->scalar('username')
            ->maxLength('username', 255)
            ->requirePresence('username', 'create')
            ->notEmpty('username');

        $validator
            ->email('email')
            ->requirePresence('email', 'create')
            ->notEmpty('email')
            ->add('email', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']);

        $validator
            ->scalar('password')
            ->maxLength('password', 255)
            ->requirePresence('password', 'create')
            ->notEmpty('password');

        $validator
            ->datetime('deleted')
            ->allowEmpty('deleted');

        $validator
            ->dateTime('last_login_at')
            ->allowEmpty('last_login_at');

        return $validator;
    }

    public function buildRules(RulesChecker $rules)
    {
        $rules->add($rules->isUnique(['username']));
        $rules->add($rules->isUnique(['email']));

        return $rules;
    }
}

ちなみに、最後の部分は必須ではありません。デフォルトでは「deleted 」カラムに対して論理削除処理を行うのですが、これを別のカラムで行いたい場合はここにカラム名を記述します。

動作確認

設定が完了したので、実際に動かしてみます。

いつも通りに delete() メソッドでユーザ削除を行い、データを確認します。

mysql> select id, username, deleted from users;
+----+----------+---------------------+
| id | username | deleted             |
+----+----------+---------------------+
|  1 | test01   | NULL                |
|  2 | test02   | NULL                |
|  3 | test03   | NULL                |
|  4 | test04   | NULL                |
|  5 | test05   | NULL                |
|  6 | test06   | 2018-04-15 11:52:28 |
+----+----------+---------------------+

物理削除は行われず、deleted カラムに日時が挿入されました。これによって論理削除が成立し、SELECT 時には出てこなくなります。

これは、実行されている SQL 文を確認するとわかりますが、このプラグインを挟む事で、SELECT 時に deleted が null のものだけを取得しているからです。

SELECT
  Users.id AS `Users__id`,
  Users.username AS `Users__username`,
  Users.email AS `Users__email`,
  Users.password AS `Users__password`,
  Users.deleted AS `Users__deleted`,
  Users.last_login_at AS `Users__last_login_at`,
  Users.created AS `Users__created`,
  Users.modified AS `Users__modified`
FROM
  users Users
WHERE
  Users.deleted IS NULL # ← ここ
LIMIT
20 OFFSET 0

関連メソッド

これで論理削除を実装する事ができましたが、論理削除管理に関する操作を以下に紹介します。

論理削除されたレコードも含めて検索を行う
// 論理削除含め、全て取得
$users = $this->Users->find('all', ['withDeleted']);

// 論理削除含め、条件付き取得
$userss = $this->Users->find('all', ['withDeleted'])->where(['id >' => 3]);

// 論理削除含め、1件のみ条件付きで取得
$usersss = $this->Users->find('all', ['withDeleted'])->where(['id' => 3])->first();

// ペジネートは適宜
$usersa = $this->paginate($users);
論理削除されたレコードを元に戻す
$this->Users->restore($user);
論理削除ではなく、物理削除を行う
$this->Users->hardDelete($user);
既に論理削除を行った複数レコードに対して、削除日時を元にして物理削除を行う
// 指定した日時以前のデータが物理削除されます
$date = new \DateTime('2018-04-15 19:00:00');
$this->Users->hardDeleteAll($date);

まとめ

以上で作業は完了です。論理削除はソフトデリートとも呼ばれる通り、表面上は削除を行った風に動作しますが、データベース上ではレコードは削除されません。案件や要件によって、本当に論理削除で良いのかを考えつつ決定していきたいところです。

とはいえ、論理削除を行う際は素早く導入できて使るプラグインなので、是非試してみてください。

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級