Help us understand the problem. What is going on with this article?

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

はじめに

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

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

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

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

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

何を解決するか

Laqu(ラク?レイク?) は、

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

を行うことができます。実行時間のところはBasic Database Usage - Laravel - The PHP Framework For Web Artisansで出された値(time)を利用しているので、そこまで当てにならないかもしれませんが実行されたクエリの確認は行なえます。

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

要件

  • PHP7.2 以上
  • Laravel 5.8.x, 6.x, 7.x

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

インストール

composerを使います。

$ composer require --dev shimabox/laqu

使い方

QueryAssertion

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

<?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

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()

/*
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()

/*
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()

/*
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は配列として扱えます

// 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の処理をラップしたものです。

QueryLog::getQueryLog()

<?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 を利用しています

QueryHelper::buildedQuery()

<?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. を利用しています。
デフォルトはNullHighlighterを利用していますが、Cli、HTMLでのフォーマットも可能です。

詳しくは https://github.com/shimabox/laqu#queryformatter を参照してください。

その他

Facade

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

GitHub Actions

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

おわりに

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


  1. QueryBuilderなどを使ってクエリを作るところと実行するところを分けている設計ならば、toSql()を使って確認することも可能でしょうがそんなフェーズはとうに過ぎていた。。 

  2. Basic Database Usage - Laravel - The PHP Framework For Web Artisans のように、enableQueryLog()などを書くのも方法としてはありますが、それをいちいち書くのも面倒だったのでライブラリで吸収しちゃおうという作戦です 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした