Laravel SQLの実行クエリログを出力する

:::note
2024/4/10 追記: こちらの記事をライブラリ化しました。

https://qiita.com/ucan-lab/items/dc33ab7df03ebf485898
:::

:::note info
新しいLaravelのバージョンに合わせてアップデートした記事を書きました。
Laravel 10.15.0 以降を使用している場合はご参考にどうぞ。

https://qiita.com/ucan-lab/items/ef95805818b843ce5bce
:::

LaravelではEloquentやQueryBuilderを介してデータベースとやり取りしますが、実際に実行されるSQLを確認する時に使用する方法を3つご紹介します。

## 環境

- PHP 8.0
- MySQL 8.0.17
- Laravel 8.23.1

## 1. QueryBuilderをSQLに変換したい時

クエリビルダには、 [`toSql()`](https://laravel.com/api/master/Illuminate/Database/Query/Builder.html#method_toSql) メソッドを呼び出すことでSQL文を取得できます。プレースホルダの値は [`getBindings()`](https://laravel.com/api/master/Illuminate/Database/Query/Builder.html#method_getBindings) で取得できます。

```php
$query = \App\User::where('id', 1);
dd($query->toSql(), $query->getBindings());
```

tinkerで確認する時にも便利です。

## 2. 複数のQueryBuilderのSQLを確認したい時

SQL実行前に [`DB::enableQueryLog()`](https://laravel.com/api/master/Illuminate/Database/Connection.html#method_enableQueryLog) でクエリログを有効化し、SQL実行後に [`DB::getQueryLog()`](https://laravel.com/api/master/Illuminate/Database/Connection.html#method_getQueryLog) メソッドを利用することで、そのクエリの内容や、実行時間を取得できます。

```php
\DB::enableQueryLog();
\App\User::all();
dd(\DB::getQueryLog());
```

有効化した後のクエリログを取得できます。

## 3. サービスプロバイダーに登録してログファイルに自動出力

- 環境変数でログ出力の有効/無効を切り替えする

### DatabaseQueryServiceProvider を作成

```shell-session
$ php artisan make:provider DatabaseQueryServiceProvider
```

`app/Providers/DatabaseQueryServiceProvider.php` ファイルが生成される。

### config/app.php へ追記

```php
    'providers' => [
        // ... 省略
        App\Providers\DatabaseQueryServiceProvider::class,
    ],
```

`providers` の配列に作成した `DatabaseQueryServiceProvider` を登録します。

### app/Providers/DatabaseQueryServiceProvider.php を編集

```php:app/Providers/DatabaseQueryServiceProvider.php
<?php

declare(strict_types=1);

namespace App\Providers;

use Carbon\Carbon;
use DateTime;
use Illuminate\Database\Events\TransactionBeginning;
use Illuminate\Database\Events\TransactionCommitted;
use Illuminate\Database\Events\TransactionRolledBack;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;

final class DatabaseQueryServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     */
    public function register(): void
    {
        if (config('logging.sql.enable') !== true) {
            return;
        }

        DB::listen(static function ($query): void {
            $sql = $query->sql;

            foreach ($query->bindings as $binding) {
                if (is_string($binding)) {
                    $binding = "'{$binding}'";
                } elseif (is_bool($binding)) {
                    $binding = $binding ? '1' : '0';
                } elseif (is_int($binding)) {
                    $binding = (string) $binding;
                } elseif (is_float($binding)) {
                    $binding = (string) $binding;
                } elseif ($binding === null) {
                    $binding = 'NULL';
                } elseif ($binding instanceof Carbon) {
                    $binding = "'{$binding->toDateTimeString()}'";
                } elseif ($binding instanceof DateTime) {
                    $binding = "'{$binding->format('Y-m-d H:i:s')}'";
                }

                $sql = preg_replace('/\\?/', $binding, $sql, 1);
            }

            if ($query->time > config('logging.sql.slow_query_time')) {
                Log::warning(sprintf('%.2f ms, SQL: %s;', $query->time, $sql));
            } else {
                Log::debug(sprintf('%.2f ms, SQL: %s;', $query->time, $sql));
            }
        });

        Event::listen(static fn (TransactionBeginning $event) => Log::debug('START TRANSACTION'));
        Event::listen(static fn (TransactionCommitted $event) => Log::debug('COMMIT'));
        Event::listen(static fn (TransactionRolledBack $event) => Log::debug('ROLLBACK'));
    }
}
```

### config/logging.php を編集

```php:config/logging.php
return [
    /*
    |--------------------------------------------------------------------------
    | Custom Log
    |--------------------------------------------------------------------------
    */

    'sql' => [
        'enable' => env('LOG_SQL_ENABLE', false),
        'slow_query_time' => env('LOG_SQL_SLOW_QUERY_TIME', 2000), // ms
    ],
];
```

### .env を編集

```
LOG_SQL_ENABLE=true
LOG_SQL_SLOW_QUERY_TIME=1000
```

※本番環境ではパフォーマンスやセキュリティ上の問題から無効化した方が良いです！

### お試し実行

#### routes/web.php

```php
<?php

use App\User;

Route::get('/', function () {
    $user = DB::transaction(function () {
        $user = factory(User::class)->create();
        $user->name = 'change name';
        $user->save();
        $user->delete();

        return factory(User::class)->create();
    });

    User::find($user->id);

    return view('welcome');
});
```

適当にSQLを発行しそうな処理を加えて、いつものウェルカムページを表示する。

#### storage/logs/laravel-***.log

ログファイルを確認すると次のようなデータベースクエリログが出力されていればok

```
[2019-06-15 07:14:09] local.DEBUG: START TRANSACTION  
[2019-06-15 07:14:09] local.DEBUG: SQL {"sql":"insert into `users` (`name`, `email`, `email_verified_at`, `password`, `remember_token`, `updated_at`, `created_at`) values ('Pietro Conn', 'leuschke.kiarra@example.net', '2019-06-15 07:14:09', 'yIXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'rch9DXbn5W', '2019-06-15 07:14:09', '2019-06-15 07:14:09')","time":"2.19 ms"} 
[2019-06-15 07:14:09] local.DEBUG: SQL {"sql":"update `users` set `name` = 'change name', `users`.`updated_at` = '2019-06-15 07:14:09' where `id` = 23","time":"1.66 ms"} 
[2019-06-15 07:14:09] local.DEBUG: SQL {"sql":"delete from `users` where `id` = 23","time":"1.33 ms"} 
[2019-06-15 07:14:09] local.DEBUG: SQL {"sql":"insert into `users` (`name`, `email`, `email_verified_at`, `password`, `remember_token`, `updated_at`, `created_at`) values ('Zoila Wintheiser', 'eokon@example.com', '2019-06-15 07:14:09', 'yIXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Ya6yCp6UTX', '2019-06-15 07:14:09', '2019-06-15 07:14:09')","time":"1.61 ms"} 
[2019-06-15 07:14:09] local.DEBUG: COMMIT  
[2019-06-15 07:14:09] local.DEBUG: SQL {"sql":"select * from `users` where `users`.`id` = 24 limit 1","time":"4.41 ms"} 
```

## 関連記事

- [Laravel リクエストログを出力する](https://qiita.com/ucan-lab/items/bfd15b096a916f811468)
- [Laravel ConsoleとHttpのログファイルを分ける](https://qiita.com/ucan-lab/items/4dd7b5f7a3eb57a3ef1f)