“医生,我的 Query Builder 生成了些奇怪的东西……”
熟悉的场景:您编写了一个优雅的链式 Query Builder 查询,确信一切完美运行,但结果要么报错,要么数据不对。任何 PHP 开发者首先想到的都是:“让我看看你到底生成了什么!”
“How do I get the query builder to output its raw SQL query as a string?” 这个问题是 Stack Overflow 上最热门的问题之一(数千次浏览和数十个回答)。这并非偶然。能够快速获取纯 SQL 是一项超能力,可以节省数小时的调试时间。
在本文中,我们将介绍 3 种实用方法,适用于 PHP 框架(Laravel、Doctrine、CakePHP),展示真实的代码示例,并解释何时使用哪种方法。开始吧!
为什么需要这个?(简明扼要)
- 调试 — 理解查询为何返回错误数据。
- 优化 — 查看实际执行的 JOIN 和 WHERE 条件。
- 学习 — 新手常常不理解 Query Builder 如何将方法转换为 SQL。
- 日志记录 — 保存原始查询用于审计或重放。
准备好了吗?那我们直接看代码!
方法 1:Laravel Eloquent / Query Builder — 经典方法
在 Laravel 中有几种方法,但最可靠的是结合使用 toSql() 和 getBindings()。为什么要一起用?因为 toSql() 显示的是带有占位符 ? 的查询,不替换值的话毫无用处。
简单示例:带 WHERE 的 SELECT
use App\\Models\\User;
$query = User::where('email', 'like', '%@gmail.com') ->where('active', 1) ->orderBy('created_at', 'desc');
// 获取带占位符的 SQL$sql = $query->toSql();$bindings = $query->getBindings();
// 组装成可读的形式$fullSql = vsprintf(str_replace('?', '%s', $sql), $bindings);
echo $fullSql;// 结果:// SELECT * FROM users WHERE email LIKE '%@gmail.com' AND active = 1 ORDER BY created_at DESC重要提示: vsprintf 并非万能。对于复杂数据类型(例如 NULL 或数组),最好使用 Str::replaceArray() 或 laravel-query-builder-debug 包。
进阶版本:带 join 和 whereIn
$query = DB::table('orders') ->join('users', 'orders.user_id', '=', 'users.id') ->whereIn('orders.status', ['pending', 'processing']) ->where('orders.total', '>', 100) ->select('orders.*', 'users.name');
$bindings = $query->getBindings();$sql = $query->toSql();
// 美观地格式化$fullSql = preg_replace_array('/\\?/', $bindings, $sql);echo $fullSql;// SELECT orders.*, users.name FROM orders INNER JOIN users ON orders.user_id = users.id WHERE orders.status IN ('pending', 'processing') AND orders.total > 100建议: 使用 preg_replace_array() — 它比 vsprintf 更安全,尤其是当数据中包含 % 字符时。
方法 2:Doctrine DBAL — 适合喜欢严谨的人
Doctrine Query Builder(常用于 Symfony)稍微复杂一些。这里没有现成的 toSql(),但有 getSQL() 和 getParameters()。另外还需要考虑参数类型。
基础示例
use Doctrine\\DBAL\\Connection;use Doctrine\\DBAL\\Query\\QueryBuilder;
/** @var Connection $connection */$queryBuilder = $connection->createQueryBuilder();$queryBuilder ->select('p.*', 'c.name as category_name') ->from('products', 'p') ->join('p', 'categories', 'c', 'p.category_id = c.id') ->where('p.price > :minPrice') ->andWhere('p.sto