以前投稿したbulk insertで大量のデータをDBに登録するの記事では10万レコードの挿入をbulk insertで行い、通常のinsertと比べてどれだけ速くなるのか検証しました。結果は一目瞭然で、大量のレコードをDBに登録する際には強力な武器になる事が分かりました。

しかし、前回のコードを実際の運用で使うには1つ問題があります。それはbulk insertの途中でエラーが発生した場合、DBに中途半端にレコードが残ってしまうという問題です。今回はこの問題を解決するために、前回のコードをtransactionに入れ、エラーが発生した際にロールバックされるように改修します。そして、transaction処理にする事でパフォーマンスにどのような影響があるか検証してみます。

エラー発生時の挙動

改修を始める前にまずは途中でエラーが発生し処理が異常終了してしまった場合の挙動を確認してみましょう。
前回の記事で作成したUsersSeederを以下のように編集し、9999件目のレコード挿入でわざと重複エラーを発生させてみましょう。


use App\User;
use Faker\Factory as Faker;
use Illuminate\Database\Seeder;

class UsersSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $faker = Faker::create('ja_JP');

        $params = [];

        for ($i=0; $i < 100000; $i++) {
            $email = "test{$i}@example.com"; 

            // 9999件目でemailが重複
            if ($i == 9999) {
                $email = "test1@example.com";
            }

            $params[] = [ 
                'name' => $faker->name(),
                'email' => $email,
                'password' => 'testtest',
            ];

            if (count($params) >= 1000) {
                User::insert($params);
                $params = [];
            }
        }
    }
}

seederを実行すると以下のように重複エラーが発生するはずです。

% php artisan db:seed
Seeding: UsersSeeder

   Illuminate\Database\QueryException  : SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'test1@example.com' for key 'users_email_unique' (SQL: insert into `users` (`email`, `name`, `password`) values ...

この時点でDB上のレコード件数を確認してみると。。。

mysql> select count(*) from users;
+----------+
| count(*) |
+----------+
|     9000 |
+----------+
1 row in set (0.01 sec)

9999件目でエラーが発生し、処理がabortされたので最後の999件は登録されませんでしたが、それまでの9000件のレコードが登録されたままの中途半端な状態になっています。前回の記事ではseederの実行前に`truncate`メソッドを実行し、テーブルを空にしていたため、再度seederを実行しても問題ありませんでした。しかし、エラーにより処理が途中で強制終了されたのであれば、失敗した中途半端な状態は残さず、実行前の状態に戻るのが望ましいです。

transaction処理の実装

それではtransaction処理を実装し、エラー発生時にロールバックするように修正してみましょう。Laravelでは以下のようにDBファサードを使うとtransaction処理が簡単に実装できます。

// transaction開始
DB::beginTransaction();

try {

    // 何かしらのDB操作...

    // transactionコミット
    DB::commit();
} catch (\Throwable $th) {

    // ロールバック
    DB::rollBack();
}

修正を加えたコードが以下になります。


use App\User;
use Faker\Factory as Faker;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class UsersSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::beginTransaction();

        try {
            $this->insertDummies();

            DB::commit();
        } catch (\Throwable $th) {
            DB::rollBack();

            throw new Exception('failed seeding User, so rolled back.');
        }
    }

    private function insertDummies()
    {
        $faker = Faker::create('ja_JP');

        $params = [];

        for ($i=0; $i < 100000; $i++) {

            $email = "test{$i}@example.com";

            // 9999件目でemailが重複
            if ($i == 9999) {
                $email = "test1@example.com";
            }

            $params[] = [
                'name'  => $faker->name(),
                'email' => $email,
                'password' => 'testtest',
            ];

            if (count($params) >= 1000) {
                User::insert($params);
                $params = [];
            }
        }
    }
}

runメソッドが冗長になってしまう為、レコード作成処理をinsertDummiesメソッドに切り出しました。
レコード作成処理の途中でエラーが発生した場合はDB::rollback()によってinsertDummiesメソッド実行前に巻き戻されるはずです。

再度、seederを実行してみましょう。

% php artisan db:seed
Seeding: UsersSeeder

   Exception  : failed seeding User, so rolled back.
...

Errorが発生し期待通りロールバックされました。DB側を確認してみましょう。

mysql> select count(*) from users;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)

実行前の状態に戻されていますね。

処理速度に影響は?

通常のinsert処理からbulk insertに切り替える事の最大のメリットはその速さでしたが、transaction処理に切り替える事で何か影響は在るでしょうか?チェックしてみましょう。

insertDummiesメソッド内のエラーを発生させていた以下の行を削除し、正常終了するように直します。

...
            // 9999件目でemailが重複
            if ($i == 9999) {
                $email = "test1@example.com";
            }
...

再度、seederを実行してみましょう。

% php artisan db:seed
Seeding: UsersSeeder
Seeded:  UsersSeeder (6.87 seconds)
Database seeding completed successfully.

約6.9秒、transaction無しの状態で約5.8秒だったので約1秒遅くなりましたが、気にするほどでは無さそうですね。

By hikaru