Laravel 中数据库异常需深入 QueryException 的 getPrevious() 获取真实错误,否则无法定位字段超长、外键不匹配或唯一索引冲突等具体原因。

数据库异常在 Laravel 中不能只靠 try-catch 包一层就完事——多数情况下你捕获到的 QueryException 是个“壳”,真实错误藏在 $exception->getPrevious() 里,不挖出来根本没法定位是字段超长、外键不匹配,还是唯一索引冲突。
捕获 QueryException 后必须展开 getPrevious() 才能看到真实错误
直接 $exception->getMessage() 只会返回类似 "SQLSTATE[23000]: Integrity constraint violation" 这种泛泛而谈的信息,对排查毫无帮助。
- MySQL 唯一约束失败时,底层报错是
"Duplicate entry 'xxx' for key 'users_email_unique'",但只有调用$exception->getPrevious()?->getMessage()才能拿到 - 外键插入失败时,真实错误码是
1452,但$exception->getCode()返回的是通用 SQLSTATE23000,必须从getPrevious()拿原始 PDO 异常 - 日志记录时如果只记外壳,等于白记;线上查问题时翻
storage/logs/laravel.log看不到关键字段和值,只能干瞪眼
区分业务可预期异常和真错误,别把 1062 当 500 上报
像邮箱重复(1062 Duplicate entry)、外键缺失(1452)这类错误,在注册、导入等场景中是用户输入或数据状态导致的正常分支,不是系统崩溃。
- 在
Handler@report()中判断$exception instanceof QueryException后,再检查$exception->getPrevious()?->getCode()是否为1062或1452 - 如果是,改用
Log::warning()记录,而不是Log::error(),避免告警风暴 - 不要在控制器里对每个
save()都包try-catch——该由Handler@render()统一拦截并转成422或400JSON 响应
事务中遇到外键异常,必须手动 rollback() 否则事务卡住
Laravel 的 DB::transaction() 在抛出 QueryException 时**不会自动回滚**,尤其在外键约束失败时,事务可能停留在“已开始未提交”状态,下次请求复用连接会直接报错。
- 务必用
try-catch包住整个事务块,且在catch中显式调用DB::rollback() - 别只依赖
DB::transaction(function () { ... })的自动回滚——它只对未被捕获的异常生效,一旦你在里面catch了却没rollback,连接就脏了 - 检查是否为外键异常:用
$exception->getPrevious()?->getCode() === 1452判断,比字符串匹配"foreign key"更可靠
开发环境禁用外键检查?只限单次 CLI 导入,别进 Web 请求
有人在控制器里写 DB::statement('SET FOREIGN_KEY_CHECKS = 0') 来绕过约束,这是高危操作——它会影响整个数据库连接生命周期,后续所有查询都可能跳过外键校验。
- 仅允许在 Artisan 命令中临时使用,且必须配对执行
SET FOREIGN_KEY_CHECKS = 1,并在命令结束前确保恢复 - Web 请求中绝对禁止,因为连接池复用会导致下一个用户请求也失去外键保护
- 更安全的做法是预检父表数据存在性:比如插入订单前,先
User::whereIn('id', $userIds)->count() === count($userIds)
真正难的不是捕获异常,而是从 QueryException 里一层层剥开,确认到底是数据问题、约束设计问题,还是事务顺序问题——这些细节不抠清楚,日志里永远只有“SQLSTATE[23000]”,而你还在猜。











