RitoLabo

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

  • 公開:
  • カテゴリ: PHP CakePHP
  • タグ: PHP,CakePHP,cakephp3-soft-delete,SoftDelete

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

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

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

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

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

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

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

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

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

開発環境

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

  • Linux CentOS 7
  • Apache 2.4
  • 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);

まとめ

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

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