最稳妥方式是用createcommand()配合bindvalue()绑定参数、queryall()等方法获取结果,并在事务中统一使用闭包参数$db对象。需防范sql注入、大小写键名、内存溢出等问题。

Yii2里用createCommand()写原生SQL最稳妥的方式
直接用createCommand()是最轻量、最可控的原生SQL入口,不绕开框架的连接池和事务管理,也避开了Query Builder的抽象干扰。它适合需要精确控制SQL语句结构、含复杂子查询、变量绑定或非标准语法的场景。
常见错误是把SQL当字符串拼接:"SELECT * FROM user WHERE id = " . $id——这会引发SQL注入;或者忘了调用execute()/queryAll()就以为执行了。
- 必须用
bindValue()或bindParam()传参,尤其对WHERE、IN、ORDER BY动态部分 - 查询结果用
queryAll()(返回数组)、queryOne()(单行)、queryScalar()(单值),别用execute()查数据 -
execute()只用于INSERT/UPDATE/DELETE,返回影响行数,不是结果集 - 如果SQL含占位符
:name,参数必须用bindValue(':name', $value),不能只传$value
示例:
use Yii;
$result = Yii::$app->db->createCommand('SELECT id, name FROM user WHERE status = :status AND created_at > :since')
->bindValue(':status', 1)
->bindValue(':since', '2024-01-01')
->queryAll();
事务中执行多条原生SQL要手动beginTransaction()
DAO本身不自动包裹事务。即使你在控制器里写了Yii::$app->db->transaction(function ($db) { ... }),里面用createCommand()仍需自己处理回滚逻辑——因为闭包里的$db是新连接实例,而createCommand()默认走的是Yii::$app->db主连接。
容易踩的坑:在事务闭包里混用Yii::$app->db->createCommand()和闭包参数$db->createCommand(),导致部分SQL没进事务。
- 事务内执行原生SQL,统一用闭包参数
$db:即$db->createCommand(...)->execute() - 不要在事务闭包里再调用
Yii::$app->db->...,那会脱离当前事务上下文 - 批量插入时,避免每条都
createCommand()——可拼成一条INSERT INTO ... VALUES (),(),()提升性能 - MySQL下注意
max_allowed_packet限制,超长SQL会报SQLSTATE[HY000]: General error: 2006 MySQL server has gone away
queryAll()返回的数组字段名默认小写,跟数据库实际大小写无关
MySQL表字段明明是UserEmail,但queryAll()返回的键却是useremail——这是PDO默认行为,不是Yii的bug。很多开发者因此在模板里写$row['UserEmail']取不到值,调试半天才发现是键名被转小写了。
这个转换发生在PDO层,Yii没干预,也没提供开关。想保持原始大小写,只能换驱动(不现实)或手动映射。
- 开发时用
print_r(array_keys($result[0]))确认实际键名 - MySQL建议统一用小写下划线命名(
user_email),避免这类混淆 - 如必须保留驼峰,可在SQL里用别名:
SELECT user_email AS UserEmail FROM user - PostgreSQL更严格,默认区分大小写,但需用双引号包裹字段名,实际反而更易出错
用createCommand()查大数据量时注意内存和超时
queryAll()会一次性把全部结果载入PHP内存。查百万级记录时,很容易OOM或触发max_execution_time超时,错误信息通常是Allowed memory size exhausted或SQLSTATE[HY000]: General error: 2013 Lost connection to MySQL server during query。
这不是SQL写得不对,而是加载方式不合理。DAO没提供游标式遍历接口,得自己拆解。
- 用
LIMIT+OFFSET分页,但注意深分页性能衰减(OFFSET 100000很慢) - 更稳的方式是基于自增ID或时间戳游标分页:
WHERE id > 100000 ORDER BY id LIMIT 100 - 导出类需求,改用
yield逐行处理:while ($row = $command->queryRow()) { yield $row; }(需确保PDO设置PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false) - PHP CLI模式下记得调大
memory_limit和max_execution_time,Web模式下绝不允许全量查大表
复杂点在于:游标分页要自己维护状态,yield要配合生成器和流式响应,这些都不是DAO自动帮你做的。写原生SQL省了抽象,但该操的心一点没少。











