不能直接用 db::raw() 拼接用户输入,因其导致 sql 注入;db::raw() 仅限固定字符串(如函数名、字段路径),动态内容须用参数绑定或白名单校验;原生查询应优先使用 db::select() / selectone() / statement() 等带绑定的方法。

为什么不能直接用 DB::raw() 拼接用户输入
因为这等于把 SQL 注入大门敞开。比如 DB::select("SELECT * FROM users WHERE name = '" . $_GET['name'] . "'"),只要传入 ' OR '1'='1 就全表泄露。Laravel 的原始查询构建器不是用来绕过安全机制的,而是补足 Query Builder 不支持的语法场景,比如窗口函数、CTE、特定数据库的 hint 或自定义聚合。
正确做法永远优先用 Query Builder:DB::table('users')->where('name', request('name'))->get();只有遇到 ROW_NUMBER() OVER() 或 WITH RECURSIVE 这类无法用链式方法表达的结构时,才动用 DB::select() / DB::statement(),且必须用参数绑定。
- 所有用户可控输入必须走
?占位符 + 第二个参数数组传入,例如:DB::select("SELECT * FROM logs WHERE level = ? AND created_at > ?", ['error', $since]) -
DB::raw()只能用于字段名、表名、函数名等**固定字符串**,绝不能拼接变量。比如DB::raw('JSON_EXTRACT(data, "$.email")')合法,DB::raw("{$field} = ?")是危险写法 - 如果必须动态表名(如分表),需白名单校验:
in_array($table_suffix, ['202401', '202402'], true),再拼进 SQL
执行带返回值的原生查询该用 DB::select() 还是 DB::selectOne()
看你要什么结果。DB::select() 总是返回数组(哪怕只查一行一列),而 DB::selectOne() 返回单个对象或 null,适合 SELECT COUNT(*) 或 SELECT id FROM ... LIMIT 1 这类明确只取一条的场景。
常见错误是以为 selectOne() 能自动解包字段值——它返回的是 stdClass 对象,仍要访问 ->count 或 ->id。如果只想拿一个标量值(比如数字),得自己取:DB::selectOne('SELECT COUNT(*) as c FROM users')->c。
- 查多行:用
DB::select(),返回array<stdclass></stdclass> - 查单行且确定存在:用
DB::selectOne(),返回stdClass|null - 执行无返回 DML(INSERT/UPDATE/DELETE):用
DB::statement(),返回受影响行数(int) - 执行 DDL(CREATE TABLE)或带 OUT 参数的存储过程:只能用
DB::unprepared(),不支持绑定参数
DB::transaction() 套着原生查询时要注意什么
事务本身没问题,但容易忽略两点:一是连接复用,二是异常捕获粒度。Laravel 默认每个 DB:: 调用都复用同一个 PDO 连接,所以 DB::transaction() 内部的 DB::select() 和 DB::update() 天然在同一个事务里;但如果你手动调了 DB::connection('other')->select(...),那就跑偏了。
另一个坑是 PHP 异常没被事务感知。比如你在事务块里执行 DB::select('INVALID SQL'),PDO 抛出 PDOException,DB::transaction() 会自动 rollback —— 但如果你在查询后加了 throw new RuntimeException(),它照样 rollback;可一旦你用 try/catch 吞掉了异常,事务就变成“已提交未报错”的静默失败状态。
- 事务内所有 DB 操作必须用同一个连接实例(默认即可,勿显式切换 connection)
- 不要在事务回调里 catch 住异常再忽略,要么让异常冒泡,要么显式调
DB::rollback() - 长事务慎用原生查询,尤其含
FOR UPDATE的语句,容易锁表卡住其他请求
MySQL 和 PostgreSQL 的原生语法差异怎么兼容
别硬扛。Laravel 的 Query Builder 之所以存在,就是为屏蔽这些差异;一旦你写了原生 SQL,就得自己负责方言适配。比如分页:LIMIT 10 OFFSET 20(MySQL)和 LIMIT 10 OFFSET 20(PostgreSQL)表面一样,但 PostgreSQL 支持 FETCH FIRST 10 ROWS ONLY,MySQL 8.0+ 才跟上;而生成 UUID,MySQL 用 UUID(),PostgreSQL 用 gen_random_uuid()(需扩展)或 md5(random()::text)。
真要跨库,方案只有两个:一是用 DBAL 的平台抽象层(如 Doctrine\DBAL\Platforms\MySqlPlatform),二是把差异点抽成配置或策略类。但绝大多数项目没这个必要——上线前就该锁定数据库类型。
- 避免在原生查询中使用数据库特有函数,除非业务强依赖(如地理围栏用 MySQL 的
ST_Distance_Sphere) - 如果必须用,至少用
DB::getDriverName()做运行时判断,分支写不同 SQL - 建表语句、索引类型(BTREE vs HASH)、字符集声明(
utf8mb4_unicode_civsen_US.UTF-8)这些 DDL 更难对齐,原生查询尽量避开
实际项目里,90% 的“必须用原生查询”需求,最后发现是没吃透 Query Builder 的 when()、union()、fromSub() 或模型作用域。真正需要 DB::select() 的地方,往往藏在性能瓶颈点或老系统数据迁移脚本里——那些地方,SQL 正确性比“看起来优雅”重要得多。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











