正直 mysqldump の方がより柔軟に過不足なく使いやすいです。やんごとなき理由で mysqldump が使えない場合、にデータベースの中身をダンプ、復元するスクリプトを紹介します。
内容は以下です。
<?php
namespace App\Console\Commands\ForDevelop;
use App\Console\BaseCommand;
use DB;
use Doctrine\DBAL\Schema\Column;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class DumpDbAsSerializeLines extends BaseCommand
{
protected $name = 'dev:dump-db-as-serialize-lines';
protected $description = 'テーブル名.txt の名前でシリアライズされた各レコードの結果を出力。dev:restore-db-from-serialize-lines と対';
public function handle(): void
{
// Doctrine のテーブル接続ツールを使用
$dbal = Model::resolveConnection()->getDoctrineSchemaManager();
// テーブル名を取得
foreach ($dbal->listTableNames() as $tableName) {
// 開始通知
$this->info('start: '.$tableName);
// 書き込み用のファイルを用意
$fp = new \SplFileObject(storage_path('mysqlBackUp/'.$tableName.'.txt'), 'wb+');
// chunk メソッドに orderBy が必須のためカラム名を取得
$colNames = array_map(static fn (Column $col) => $col->getName(), $dbal->listTableColumns($tableName));
// chunk メソッドを用いてメモリに対して優しい SELECT を繰り返す
DB::query()
->select()->from($tableName)
->orderBy($colNames[array_key_first($colNames)])
->chunk(5000, static function (Collection $collection) use ($fp) {
// SELECT して得た各レコードを serialize してファイルに書き込み
// serialize を用いると null を始めとした様々な型を処理しやすい
$collection->each(static fn (object $stdObj) => $fp->fwrite(serialize($stdObj)."\n"));
});
// 終了通知
$this->info('finish: '.$tableName);
// 開いたファイルへの接続はファイルポインタが参照不能になった時点で PHP が自動で閉じてくれる
}
}
}
<?php
namespace App\Console\Commands\ForDevelop;
use App\Console\BaseCommand;
use DB;
use Illuminate\Database\Eloquent\Model;
class RestoreDbFromSerializeLines extends BaseCommand
{
protected $name = 'dev:restore-db-from-serialize-lines';
protected $description = 'テーブル名.txt の名前でシリアライズされた各レコードの結果を元に INSERT。dev:dump-db-as-serialize-lines と対';
/**
* @throws \Throwable
*/
public function handle(): void
{
// 外部キー制約を無視する様に設定変更
DB::statement('set foreign_key_checks = 0;');
try {
// 半端に終わると再開が手間であったり、外部キー制約が壊れたりして手間なのでトランザクションを使用
// INSERT すべきデータが巨大な場合、途中まで行った処理巻き戻しのペナルティの方が辛いので、どうにか途中から再開できる&途中まで巻き戻せる仕組みを作った方がいい
DB::statement('BEGIN;');
$this->main();
DB::statement('COMMIT;');
} catch (\Throwable $e) {
DB::statement('ROLLBACK;');
throw $e;
} finally {
// 外部キー制約を有効にする様に設定変更
// try finally によって多少の異常事態ではまず実行される様に用意
DB::statement('set foreign_key_checks = 1;');
}
}
/**
* @return void
*/
protected function main(): void
{
// Doctrine のテーブル接続ツールを使用
$dbal = Model::resolveConnection()->getDoctrineSchemaManager();
// テーブル名を取得
foreach ($dbal->listTableNames() as $tableName) {
// 開始通知
$this->info('start: '.$tableName);
// 処理を続行しないパターンを列挙
if ($tableName === 'migrations') {
$this->info('テーブル定義はしてあるものとし migrations を無視しました。');
continue;
}
if (! file_exists(storage_path('mysqlBackUp/'.$tableName.'.txt'))) {
$this->warn($tableName.' に対応するCSVファイルが見つかりませんでした。');
continue;
}
// INSERT 用のファイルを用意
$fp = new \SplFileObject(storage_path('mysqlBackUp/'.$tableName.'.txt'), 'rb+');
$i = 0; // ある程度の行ごとにまとめて INSERT する
$recordStacks = []; // INSERT 待ちの行を保持しておく
while ($line = $fp->fgets()) {
if (empty($line)) {
continue; // 空行対策
}
// unserialize することで SELECT した時のオブジェクトそのままを復元
// 第二引数の stdClass は不意に想定外のクラスを unserialize しないための安全弁
// (array) キャストは insert メソッドを使うための都合
$recordStacks[] = (array) unserialize($line, [\stdClass::class]);
++$i;
if ($i > 5000) {// 行が貯まったら INSERT
DB::query()->from($tableName)->insert($recordStacks);
$recordStacks = []; // 次からまた貯めるために初期化
$i = 0; // 次からまた貯めるために初期化
}
}
// 5000貯まっていない最後のスタックを INSERT
DB::query()->from($tableName)->insert($recordStacks);
// 終了通知
$this->info('finish: '.$tableName);
// 開いたファイルへの接続はファイルポインタが参照不能になった時点で PHP が自動で閉じてくれる
}
}
}
これらの特筆すべき点の一つが依存の小ささです。Laravelのソースコードのうち使っているのはコマンドラインとしてのベースと Doctrine を使うためのデータベース接続ぐらいです。Doctrine によるデータベース操作が既にできる環境ならばあっという間に移し替えれます。
これを次の様に使うと例の様に serialize されたレコードの行が貯まったファイルが作られ、
php artisan dev:dump-db-as-serialize-lines
# O:8:"stdClass":3:{s:2:"id";i:7;s:9:"migration";s:46:"1970_01_01_000000_create_password_resets_table";s:5:"batch";i:1;
次の様にして↑コマンドで作られたファイルからデータベースを復元できます。
php artisan dev:restore-db-from-serialize-lines