【Laravel】実行されたDBクエリの確認ができるやつを書いた

## はじめに

自分はここ最近Laravelを使ったお仕事をしているのですが、こういった悩みがありました。

- QueryBuilder便利だけど、実行されるクエリが全くわからん
  - レビューする側だと共感していただけるのでは??
- 狙ったクエリが書かれているのか自信が無い
- このメソッド叩くと無駄なクエリが流れるんだが
- etc

今までは、PHPUnitでseederなどでデータを用意してクエリを実行するメソッド書いてアサーションして、お、期待するレコード取れたよね、やったやった！！で済ましていたのですが、いちいち条件網羅するデータを用意するのが難儀だったりします。
(実行されるクエリだけ分かればいいケースも中にはあると思うのです)

くぅ、実行されるクエリだけ見たい、見せたい、それさえ出来れば、、そんな悩みを抱えていたのであります。[^1]
実行されるクエリの確認をなるべく楽にサクッとできるものが欲しい!! というわけで、

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Laravelで実行されたDBクエリの確認できるやつ書いた<br><br>shimabox/laqu: Laqu is Laravel Db Query Helper. <a href="https://t.co/vYLHeXUxuk">https://t.co/vYLHeXUxuk</a></p>&mdash; しまぶ (@shimabox) <a href="https://twitter.com/shimabox/status/1280870773603721216?ref_src=twsrc%5Etfw">July 8, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

Laravelで実行されたDBクエリの確認ができるやつを書いてみました。[^2]
(よく分からない前フリですいません。。)

## 何を解決するか

[Laqu](https://github.com/shimabox/laqu "shimabox/laqu: Laqu is Laravel Db Query Helper.")(ラク？レイク？) は、

- 期待するクエリが流れているかPHPUnitでアサーション可能です
  - traitを用意しています
- クエリが流れるメソッドを渡すと、どのようなクエリが流れるか確認することができます
  - 実行されたクエリの中から一番早かったもの、一番遅かったものが抽出できます
  - ソートも可能です
- ビルド後(パラメータがバインド済み)のクエリが確認できます

を行うことができます。実行時間のところは[Basic Database Usage - Laravel - The PHP Framework For Web Artisans](https://laravel.com/docs/5.0/database#query-logging "Basic Database Usage - Laravel - The PHP Framework For Web Artisans")で出された値(time)を利用しているので、そこまで当てにならないかもしれませんが実行されたクエリの確認は行なえます。

なお、**開発中に利用されることを想定しています**のであしからずです。

## 要件

- `PHP7.2` 以上
- Laravel `5.8.x`, `6.x`, `7.x` 

とりあえずなるべく最新のLaravelが動く環境であればOKだと思います。

## インストール

composerを使います。

```shell
$ composer require --dev shimabox/laqu
```

## 使い方

### QueryAssertion

期待するクエリが流れているかPHPUnitでアサーションするためのものです。
traitです。`assertQuery()`を使います。

```php
<?php

use Laqu\QueryAssertion;
use Tests\TestCase;

class QueryAssertionTest extends TestCase
{
    use QueryAssertion;

    private $exampleRepository;

    protected function setUp(): void
    {
        parent::setUp();

        $this->exampleRepository = new ExampleRepository(); // 仮
    }

    public function queryTest()
    {
        // 基本的な使い方
        $this->assertQuery(
            // クエリが実行される処理をクロージャに渡します
            function () {
                $this->exampleRepository->findById('a123');
            },
            // 期待するクエリを書きます
            'select from user where id = ? and is_active = ?',
            // バインドされる値を配列で定義
            // (bindするものがない場合は空配列を渡すか、引数は渡さないでOK)
            [
                'a123',
                1,
            ]
        );

        // 複数のクエリを確認
        // 基本的には 1メソッド1クエリの確認 を推奨しますが、中には1メソッドで複数クエリが流れる場合も
        // あると思います。
        // その場合は下記のようにクエリとバインド値を配列で対になるように定義してください。
        $this->assertQuery(
            function () {
                // 例えばこの処理で複数のクエリが流れるとします
                $this->exampleRepository->findAll();
            },
            // 期待するクエリをそれぞれ配列で定義
            [
                'select from user where is_active = ?', // ※1
                'select from admin_user where id = ? and is_active = ?', // ※2
                'select from something', // ※3
            ],
            // バインドされる値を二次元配列で定義(bindするものがない場合は空配列を渡してください)
            [
                [ // ※1.
                    1,
                ],
                [ // ※2.
                    'b123',
                    1,
                ],
                // ※3 はバインド無しの想定なので空配列を渡します
                [],
            ]
        );
    }
}
```
こんな感じで、クエリのアサーションが行えます。

### QueryAnalyzer

クエリが流れるメソッドを渡して、どのようなクエリが流れたのか確認することができます。
`QueryAnalyzer::analyze()` で実行されたクエリの結果(`Laqu\Analyzer\QueryList`)が取得できます。
※ QueryListの中身は`Laqu\Analyzer\Query`です

```php
<?php

use Laqu\Facades\QueryAnalyzer;

/** @var Laqu\Analyzer\QueryList */
$analyzed = QueryAnalyzer::analyze(function () { // クエリが実行される処理をクロージャに渡します
    $author = Author::find(1);
    $author->delete();
});

/*
Laqu\Analyzer\QueryList {#345
  -queries: array:2 [
    0 => Laqu\Analyzer\Query {#344
      -query: "select * from "authors" where "authors"."id" = ? limit 1"
      -bindings: array:1 [
        0 => 1
      ]
      -time: 0.08
      -buildedQuery: "select * from "authors" where "authors"."id" = 1 limit 1"
    }
    1 => Laqu\Analyzer\Query {#337
      -query: "delete from "authors" where "id" = ?"
      -bindings: array:1 [
        0 => "1"
      ]
      -time: 0.03
      -buildedQuery: "delete from "authors" where "id" = '1'"
    }
  ]
}
*/
dump($analyzed);
```

この取得結果をもとに

#### 一番実行時間が早かったクエリの抽出 `extractFastestQuery()`

```php
/*
Laqu\Analyzer\Query {#337
  -query: "delete from "authors" where "id" = ?"
  -bindings: array:1 [
    0 => "1"
  ]
  -time: 0.03
  -buildedQuery: "delete from "authors" where "id" = '1'"
}
*/
dump($analyzed->extractFastestQuery());
```

#### 一番実行時間が遅かったクエリの抽出 `extractSlowestQuery()`

```php
/*
Laqu\Analyzer\Query {#344
  -query: "select * from "authors" where "authors"."id" = ? limit 1"
  -bindings: array:1 [
    0 => 1
  ]
  -time: 0.08
  -buildedQuery: "select * from "authors" where "authors"."id" = 1 limit 1"
}
*/
dump($analyzed->extractSlowestQuery());
```

#### クエリの実行時間でソート `sortByFast()`, `sortBySlow()`

```php
/*
array:2 [
  0 => Laqu\Analyzer\Query {#337
    -query: "delete from "authors" where "id" = ?"
    -bindings: array:1 [
      0 => "1"
    ]
    -time: 0.03
    -buildedQuery: "delete from "authors" where "id" = '1'"
  }
  1 => Laqu\Analyzer\Query {#344
    -query: "select * from "authors" where "authors"."id" = ? limit 1"
    -bindings: array:1 [
      0 => 1
    ]
    -time: 0.08
    -buildedQuery: "select * from "authors" where "authors"."id" = 1 limit 1"
  }
]
*/
dump($analyzed->sortByFast());

/*
array:2 [
  0 => Laqu\Analyzer\Query {#344
    -query: "select * from "authors" where "authors"."id" = ? limit 1"
    -bindings: array:1 [
      0 => 1
    ]
    -time: 0.08
    -buildedQuery: "select * from "authors" where "authors"."id" = 1 limit 1"
  }
  1 => Laqu\Analyzer\Query {#337
    -query: "delete from "authors" where "id" = ?"
    -bindings: array:1 [
      0 => "1"
    ]
    -time: 0.02
    -buildedQuery: "delete from "authors" where "id" = '1'"
  }
]
*/
dump($analyzed->sortBySlow());
```

#### ビルド後のクエリ取得 `getBuildedQuery()`

`Laqu\Analyzer\QueryList`から要素を特定して利用します。
※ QueryListは配列として扱えます

```php

// select * from "authors" where "authors"."id" = 1 limit 1
echo $analyzed[0]->getBuildedQuery();
// delete from "authors" where "id" = '1'
echo $analyzed[1]->getBuildedQuery();
```

が利用できます。

### Helper

ヘルパーとして以下機能も提供しています。

#### QueryLog

QueryLogは[Basic Database Usage - Laravel - The PHP Framework For Web Artisans](https://laravel.com/docs/5.0/database#query-logging "Basic Database Usage - Laravel - The PHP Framework For Web Artisans")の処理をラップしたものです。

`QueryLog::getQueryLog()`

```php
<?php

use Laqu\Facades\QueryLog;

$queryLog = QueryLog::getQueryLog(function () {
    Author::find(1);
});

/*
array:1 [
  0 => array:3 [
    "query" => "select * from "authors" where "authors"."id" = ? limit 1"
    "bindings" => array:1 [
      0 => 1
    ]
    "time" => 0.12
  ]
]
*/
dump($queryLog);
```
察している人は察していると思いますが、他のメソッドはこの機能をフル活用しています。
そのため、実行時間関連はそこまで精密ではありません。

#### QueryHelper

クエリとバインドパラメータを渡すと、実行されるクエリの確認ができます。
※ [pdo-debug/pdo-debug.php at master · panique/pdo-debug](https://github.com/panique/pdo-debug/blob/master/pdo-debug.php "pdo-debug/pdo-debug.php at master · panique/pdo-debug") を利用しています

`QueryHelper::buildedQuery()`

```php
<?php

use Laqu\Facades\QueryHelper;

$now  = Carbon::now();
$from = $now->copy()->subDay();
$to   = $now->copy()->addDay();

$query = 'select * from authors where id in (?, ?) and name like :name and updated_at between ? and ?';

$bindings = [
    1,
    2,
    '%Shakespeare',
    $from,
    $to,
];

$buildedQuery = QueryHelper::buildedQuery($query, $bindings);

// select * from authors where id in (1, 2) and name like '%Shakespeare' and updated_at between '2020-07-07 00:37:55' and '2020-07-09 00:37:55'
echo $buildedQuery;
```

### Formatter

その他機能として、クエリのフォーマット機能も提供しています。
こちらは、[doctrine/sql-formatter: A lightweight php class for formatting sql statements. Handles automatic indentation and syntax highlighting.](https://github.com/doctrine/sql-formatter "doctrine/sql-formatter: A lightweight php class for formatting sql statements. Handles automatic indentation and syntax highlighting.") を利用しています。
デフォルトは`NullHighlighter`を利用していますが、Cli、HTMLでのフォーマットも可能です。

詳しくは [https://github.com/shimabox/laqu#queryformatter](https://github.com/shimabox/laqu#queryformatter "shimabox/laqu: Laqu is Laravel Db Query Helper.") を参照してください。

## その他

### Facade

Laravelっぽく？ファサードを使ってみました。
実際に作ってみると、どういう仕組で動いているのかなんとなく理解が深まった気がします。

### GitHub Actions

みようみまねで**GitHub Actions**を使ってみました。
[laqu/run-tests.yml at master · shimabox/laqu](https://github.com/shimabox/laqu/blob/master/.github/workflows/run-tests.yml "laqu/run-tests.yml at master · shimabox/laqu")
これで、PHPのバージョンやLaravelのバージョンごとにテストの実行やlintができているのでそこそこ自信をもって提供できています。

## おわりに

というわけで長くなってしまいましたが、Laravelで実行されたDBクエリの確認ができるやつを書いた話は以上となります。
[shimabox/laqu: Laqu is Laravel Db Query Helper.](https://github.com/shimabox/laqu "shimabox/laqu: Laqu is Laravel Db Query Helper.") 
もしよかったら使ってみてください。
(使ってみて、ここはこうしたらいいとか、こうすべきだとかあればプルリクください:muscle:)

[^1]: QueryBuilderなどを使ってクエリを作るところと実行するところを分けている設計ならば、[toSql()](https://laravel.com/api/master/Illuminate/Database/Query/Builder.html#method_toSql "Illuminate\Database\Query\Builder | Laravel API")を使って確認することも可能でしょうがそんなフェーズはとうに過ぎていた。。
[^2]: [Basic Database Usage - Laravel - The PHP Framework For Web Artisans](https://laravel.com/docs/5.0/database#query-logging "Basic Database Usage - Laravel - The PHP Framework For Web Artisans") のように、`enableQueryLog()`などを書くのも方法としてはありますが、それをいちいち書くのも面倒だったのでライブラリで吸収しちゃおうという作戦です