1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クエリを見るには ddRawSql() を使おう

Posted at

あまりにも便利だったので備忘録も兼ねてご紹介。

ddRawSql()とは

読んで字の如く、dump and die Raw Sql(生SQLを出力して死ぬ)。
Laravel 10.15以降で使用可能で、Illuminate\Database\Query\Builderが持つメソッドです。
具体的にはこんな挙動を見せます:

// DBファサード
DB::table('users')
    ->where('status', 'active')
    ->where('age', '>', 20)
    ->ddRawSql();

// 出力: select * from `users` where `status` = 'active' and `age` > 20


// Eloquentビルダ
User::where('status', 'active')
    ->where('created_at', '>=', '2026-01-01')
    ->ddRawSql();

// 出力: select * from `users` where `status` = 'active' and `created_at` >= '2024-01-01'

クエリビルダに定義されているメソッドですが、Eloquentのビルダでも問題なく呼び出せます。この存在をもっと早く知っていれば、クエリの整形で四苦八苦せずに済んだと思う......

ちなみに、dumpRawSql()なんてものもあります。

DB::table('users')->where('status', 'active')->dumpRawSql();

ここから蛇足

ここからは思い出話なので、ddRawSql()のおいしいところだけ味わいたい人は別の解説記事をご参照ください。

いままでどうしてきたのか

ひよっこ時代

Laravelに触れ始めた頃は、dd()の中でtoSql()を呼んでいた:

$query = User::query()
    ->where('status', 'active')
    ->where('created_at', '>=', '2026-01-01');

dd($query->toSql());

// 出力: select * from `users` where `status` = ? and `created_at` >= ?

この書き方の問題は、主に2つほどありました:

プレースホルダばかりになる

SQLインジェクション対策としてとても優秀な?ですが、人間にとっては可読性が低いと感じます。
業務で実際に書くような複雑なクエリだと、プレースホルダだらけの出力を見ただけですこし気分が落ちる...

戻り値に直接クエリを書いている場合に書き直さなければいけない

toRawSql()を使っていると、こんなコードをよく生み出すことがあります:

// return User::query()
//    ->where('status', 'active')
//    ->where('created_at', '>=', '2026-01-01')
//    ->get();

$query = User::query()
    ->where('status', 'active')
    ->where('created_at', '>=', '2026-01-01');
//    ->get();    

dd($query->toSql()); 

特に厄介なのは、return文にそのままqueryを書くような処理の場合です。
わざわざreturn句をコメントアウトしてdd()で囲ったり、面倒くさくて処理まるごとコピペした上で元のreturn文をコメントアウトしたり...こういう細かな手間が、日々のコーディングの効率を地味に下げる要因たり得ます。

少し成長:バインドを覚える

すこし学んで、?にバインドさせて出せばいいのでは?ということに気づき、こんな記述を使うようになりました:

$query = User::query()
    ->where('status', 'active')
    ->where('created_at', '>=', '2026-01-01');

$sql = preg_replace_array('/\?/', $query->getBindings(), $query->toSql());

dd($sql);

// 出力: select * from `users` where `status` = 'active' and `created_at` >= '2026-01-01'

思いついた当時は「これをメモか何かに書いておいて、使いたい時にコピペすればいいじゃん!」と心躍らせていました...が、これにも2つほど問題がありました。

コピペするのが面倒臭い

しばらくはウキウキでコピペして「おおーめっちゃ分かりやすい...」と感動すら覚えていたが、そのうち面倒臭くなってdd($query->toSql)を目grepする日々に戻ってしまいました。

PHP未経験からバインドするという小細工を覚える頃には、?まみれのプリペアドステートメントを読むのも割とスムーズになってきます。

何より、->toSql()と打つだけで実現できるような手軽さが捨てられず、目grepでいいやとなるまで、さして時間はかかりませんでした。

戻り値に直接クエリを書いている場合に書き直さなければいけない

先述のtoSql()と変わらず、わざわざreturnを潰してdd()で囲み直さなければいけないことには変わりません。

ddRawSql()で革命が起こる

しばらくしてReaDoubleを読んでいた時、不意に電流が走ります。

Readouble Laravel 10.x データベース:クエリビルダ

dumpRawSqlメソッドとddRawSqlメソッドをクエリに対して呼び出すと、すべてのパラメータバインディングを適切に置換した状態で、クエリのSQLをダンプできます。

DB::table('users')->where('votes', '>', 100)->dumpRawSql();
    
DB::table('users')->where('votes', '>', 100)->ddRawSql();

簡潔な記法で煩雑さはなく、return文の末尾に付け加えるだけで処理を止めて確認可能な魔法のメソッドと出会いました。
これならば、return文をコメントアウトすることも、コピペすることもなく、手軽に望んだデバッグができそうです。

実装を少し見てみる

Illuminate\Database\Query\Builder.php: L4131~
/**
 * Die and dump the current SQL with embedded bindings.
 *
 * @return never
 */
public function ddRawSql()
{
    dd($this->toRawSql());
}
もっと深く知りたい方向け
Illuminate\Database\Query\Builder.php: L2803~
/**
 * Get the raw SQL representation of the query with embedded bindings.
 *
 * @return string
 */
public function toRawSql()
{
    return $this->grammar->substituteBindingsIntoRawSql(
        $this->toSql(), $this->connection->prepareBindings($this->getBindings())
    );
}
Illuminate\Database\Query\Builder.php: L1388~
/**
 * Substitute the given bindings into the given raw SQL query.
 *
 * @param  string  $sql
 * @param  array  $bindings
 * @return string
 */
public function substituteBindingsIntoRawSql($sql, $bindings)
{
    $bindings = array_map(fn ($value) => $this->escape($value), $bindings);

    $query = '';

    $isStringLiteral = false;

    for ($i = 0; $i < strlen($sql); $i++) {
        $char = $sql[$i];
        $nextChar = $sql[$i + 1] ?? null;

        // Single quotes can be escaped as '' according to the SQL standard while
        // MySQL uses \'. Postgres has operators like ?| that must get encoded
        // in PHP like ??|. We should skip over the escaped characters here.
        if (in_array($char.$nextChar, ["\'", "''", '??'])) {
            $query .= $char.$nextChar;
            $i += 1;
        } elseif ($char === "'") { // Starting / leaving string literal...
            $query .= $char;
            $isStringLiteral = ! $isStringLiteral;
        } elseif ($char === '?' && ! $isStringLiteral) { // Substitutable binding...
            $query .= array_shift($bindings) ?? '?';
        } else { // Normal character...
            $query .= $char;
        }
    }

    return $query;
}

埋め込む予定の全ての値($bindings)に対してエスケープ処理を行い、単にstr_replace?を置き換えるのではなく、文字としての?(文字列リテラル)とプレースホルダとしての?を厳密に区別して処理しているようです。

これによって、例えば'What is your favorite fruit?'というような文字列で検索する際も、検索文末尾の?をリテラルだと判断してそのまま出力できます。

こうしてみると、至って簡単な実装でした。
これくらいだったら自前で実装できたかもしれない...が、こういうものを標準として用意してくれるのがフレームワークの嬉しいところです。痒いところに手が届く。

おわりに

初めて記事を書きましたが、必要な物事を簡潔に伝えるほど難しいことはないと思い知りました。
ここまで読んでいただいた方、ありがとうございました。
AIによるコーディングが主流となりつつある現代でこそ、AIの責任を取る人間がコードを理解するため、こういった知識の重要度が増していくと思います。

困ったらドキュメントを読もう!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?