应使用 Nette\Database\Explorer 替代 Connection::query(),因其解耦查询逻辑与数据映射,支持自动转义、懒加载、构建式查询和实体映射;手动拼 SQL 易错且难维护。

用 Nette\Database\Explorer 替代原始 Connection
直接调用 Connection::query() 写复杂 SQL 容易拼错、难复用、没法自动转义。Nette 推荐走 Explorer 这条路——它不是语法糖,而是把查询逻辑和数据映射解耦的核心入口。
常见错误是手动 new Connection 后硬塞 SQL 字符串,结果参数没绑定、类型没推导、后续想加 join 或分页就得重写整条语句。
- 从 DI 容器取
$database(通常是Explorer实例),别自己 newConnection -
Explorer自动支持懒加载、查询构建、实体映射,Connection只适合极简的 DDL 或调试 - 如果项目已用
Connection::query(),迁移时注意:原query('SELECT * FROM users WHERE id = ?', $id)要改成$explorer->table('users')->where('id', $id)->fetch()
where() 嵌套与复合条件怎么写才不翻车
Nette 的 where() 支持数组、关联数组、闭包三种写法,但混用时容易漏括号或误解优先级。比如想查「状态为 active 且 (邮箱以 gmail 结尾 或 手机号非空)」,写成 where(['status' => 'active', ['email LIKE ?', '%@gmail.com'], ['phone IS NOT NULL']]) 会出错——因为数组结构不合法,AND 和 OR 分组必须显式用子数组包裹。
- 多条件
OR必须用子数组:['status' => 'active', ['OR' => [['email LIKE ?', '%@gmail.com'], ['phone IS NOT NULL']]]] - 避免在
where()里写裸 SQL 片段(如'created_at > NOW() - INTERVAL 7 DAY'),改用where('created_at > ?', new DateTime('-7 days'))更安全 - 日期比较慎用字符串,
DateTime对象会被自动格式化,而'2024-01-01'可能被当成字符串字面量导致索引失效
JOIN + SELECT 表达式必须用 select() 显式声明
很多人以为 $explorer->table('users')->join('profiles')->on('users.id = profiles.user_id') 就能直接取 profile 字段,结果 fetch() 返回的还是纯 users 数据。Nette 不会自动合并 select 列表,所有要取的字段都得进 select()。
立即学习“PHP免费学习笔记(深入)”;
- 基础写法:
$explorer->table('users')->select('users.*, profiles.bio, profiles.avatar')->join('profiles')->on('users.id = profiles.user_id') - 聚合函数要用别名:
select('COUNT(orders.id) AS order_count'),否则 PHP 数组键是COUNT(orders.id)这种非法标识符 - LEFT JOIN 后某字段可能为
NULL,用fetch()时记得检查,别直接$row->bio硬取
分页和性能陷阱:别在 limit() 前漏掉 order()
$explorer->table('posts')->limit(10, 20) 看似能分页,但如果没指定 order(),MySQL 的排序行为不可控,同一页可能反复刷出不同数据。更隐蔽的是,limit() 加在复杂查询末尾时,Nette 默认不加 SQL_CALC_FOUND_ROWS,getCount() 会执行全表扫描。
- 分页必带确定性排序:
->order('created_at DESC')->limit(10, 20) - 要总数就用
$result->getCount(),它会生成独立 COUNT 查询,不是靠 FOUND_ROWS();别自己写SELECT COUNT(*)再套一遍条件 - 大表分页慎用 offset,考虑用游标分页(比如
where('id limit(10))
复杂查询真正难的不是语法组合,而是每个链式调用背后触发的 SQL 生成逻辑是否可预期。比如 having() 必须跟在 group() 后,select() 如果放在 join() 前,某些字段别名会失效——这些细节不跑一次 $query->getSql() 看不到。










