技术分享

Laravel 查询构造器避坑指南:为什么你的 OR 条件会绕过 AND 限制?

作者头像 人称外号大脸猫
97 阅读
Laravel 查询构造器避坑指南:为什么你的 OR 条件会绕过 AND 限制?

背景:拉黑功能引发的数据泄露

公司客服系统需要实现消息拉黑功能:当用户拉黑对方后,被拉黑方发送的消息不展示在拉黑方的聊天记录中。原始查询逻辑如下:

$query = ChatMessage::with(['send'])
    ->whereRaw('(? = send_id AND ? = accept_id) OR (? = send_id AND ? = accept_id)',[$sendId, $acceptId, $acceptId, $sendId]);
return $query->orderByDesc('id')->paginate(20);

新增拉黑功能时,我"理所当然"地添加了过滤条件:

return ChatMessage::with(['send'])
            ->where('is_black', 0) // 或 ->where('is_black', '0')
            ->where(function ($q) use ($sendId, $acceptId) {
                $q->where([
                    'send_id' => $sendId,
                    'accept_id' => $acceptId
                ])->orWhere([
                    'send_id' => $acceptId,
                    'accept_id' => $sendId
                ]);
            })
            ->orderByDesc('id')
            ->paginate(20);

事故现场:

✅ 开发&前端自测通过 ❌ 测试发现拉黑消息仍出现在聊天记录

排查过程:

  • 检查代码逻辑未发现问题
  • 日志输出实际 SQL 语句:
WHERE is_black = 0
AND (send_id = A AND accept_id = B)
OR (send_id = B AND accept_id = A)

发现了问题所在,原来的代码中,OR 条件被放在了 AND 条件的后面,导致查询逻辑被错误拆分。

一、问题本质:SQL 的隐式优先级陷阱

错误逻辑解析

/* 预期逻辑 */
WHERE (is_black=0) 
  AND (
    (A→B消息) OR (B→A消息)
  )

/* 实际执行 */
WHERE (is_black=0 AND A→B消息) 
   OR (B→A消息) -- 此部分绕过拉黑检查!

预期效果: ✅ 获取用户A→B或B→A的消息 ✅ 过滤所有 is_black=1 的消息

实际效果: ❌ 拉黑消息(is_black=1)仍然出现在结果中

二、解决方案:用闭包强制逻辑分组

方案1:基础闭包分组(推荐)

->where('is_black', 0)
->where(function ($q) use ($sendId, $acceptId) {
    $q->where([
            ['send_id', $sendId],
            ['accept_id', $acceptId]
        ])
        ->orWhere([
            ['send_id', $acceptId],
            ['accept_id', $sendId]
        ]);
})

方案2:多层闭包(更直观)

->where('is_black', 0)
->where(function ($q) use ($sendId, $acceptId) {
    $q->where(function ($q1) use ($sendId, $acceptId) {
            $q1->where('send_id', $sendId)
               ->where('accept_id', $acceptId);
        })
        ->orWhere(function ($q2) use ($sendId, $acceptId) {
            $q2->where('send_id', $acceptId)
               ->where('accept_id', $sendId);
        });
})

方案3:参数绑定安全写法

->where('is_black', 0)
->where(function ($q) use ($sendId, $acceptId) {
    $q->whereRaw('(? = send_id AND ? = accept_id)', [$sendId, $acceptId])
      ->orWhereRaw('(? = send_id AND ? = accept_id)', [$acceptId, $sendId]);
})

三、技术原理:闭包如何拯救查询

修正后的 SQL 结构

WHERE is_black = 0 
  AND (  -- 闭包生成的安全隔离层
    (send_id = 'A' AND accept_id = 'B') 
    OR 
    (send_id = 'B' AND accept_id = 'A')
  )

闭包的三大作用

  • 逻辑隔离:创建独立的查询上下文
  • 优先级控制:通过括号显式定义条件执行顺序
  • 防污染:避免链式调用导致的意外关联

四、工程师的自我修养

必守规则

1. OR 必闭包:只要出现 `orWhere()` 立即包裹闭包
2. 调试先行:复杂查询必用 `DB::getQueryLog()` 验证
3. 字段验型:数字/字符串类型需与数据库严格一致

调试模板

// 开启日志
DB::enableQueryLog();

// 执行查询
$messages = ChatMessage::...->paginate(20);

// 打印SQL(重要!)
logger('Final SQL', [
    'sql' => DB::getQueryLog()[0]['query'],
    'bindings' => DB::getQueryLog()[0]['bindings']
]);

五、总结:敬畏每一行代码

血泪教训

"这个功能很简单" ➜ "代码不可能有问题" ➜ "测试怎么可能不通过" 是工程师翻车的经典三部曲。

核心认知

ORM 不是 SQL 的简单封装,而是需要深度理解的抽象层。 当混合使用 AND/OR 时,闭包分组是保障逻辑正确的唯一路径。

终极建议: 在团队规范中强制要求:所有包含 orWhere() 的查询,必须用闭包包裹整个 OR 条件组。这能避免 90% 的条件逻辑错误。