Laravel中whereDoesntHave使用or运算符实现排除同时含指定关联状态数据的逻辑疑问
orWhereDoesntHave能满足你的需求? 这个问题其实是反向逻辑运算里的经典陷阱,咱们一步步拆解就能明白其中的门道:
首先先明确你的核心需求:保留所有不同时拥有状态P和U的数据行,具体来说就是:
- 只有P没有U的 → 留
- 只有U没有P的 → 留
- 既没有P也没有U的 → 留
- 同时有P和U的 → 删
我们把每个Eloquent语句对应的实际SQL逻辑拆解开来,就能看清问题所在:
1. 先看你最初的whereHas逻辑
你用:
$data->whereHas('statuses', function($q){ $q->where('statuses.title', 'P'); });
这其实是在找至少有一个P状态关联的数据,同理第二个whereHas(U)是找至少有一个U状态关联的数据。两者单独各返回2条,那它们的交集(同时满足两个whereHas)就是你要排除的「同时有P和U」的那部分数据。
2. 你第一次尝试的错误排除逻辑
你写了:
$data->whereDoesntHave('statuses', function($q){ $q->where('statuses.title', 'P'); }); $data->whereDoesntHave('statuses', function($q){ $q->where('statuses.title', 'U'); });
whereDoesntHave的本质是NOT EXISTS(不存在符合条件的关联),所以上面的语句等价于:
找那些「完全没有P状态」并且「完全没有U状态」的数据
这就把「只有P」「只有U」的行也都排除了,只剩「既没P也没U」的,当然会排除远多于2条的数据,完全不符合你的需求。
3. 正确语句的逻辑:德摩根定律的应用
你最终用的语句:
$data->whereDoesntHave('statuses', function($q){ $q->where('statuses.title', 'P'); }) ->orWhereDoesntHave('statuses', function($q){ $q->where('statuses.title', 'U'); });
对应的SQL是:
SELECT * FROM data WHERE NOT EXISTS (SELECT * FROM statuses WHERE statuses.data_id = data.id AND statuses.title = 'P') OR NOT EXISTS (SELECT * FROM statuses WHERE statuses.data_id = data.id AND statuses.title = 'U')
这里要用到逻辑里的德摩根定律:NOT A OR NOT B 等价于 NOT (A AND B)。
在这里:
- A = 数据存在关联的P状态
- B = 数据存在关联的U状态
所以NOT A OR NOT B就等价于NOT (A AND B),也就是:
排除那些「同时存在P状态 AND 存在U状态」的数据
这正好完美匹配你的需求!
为什么or看起来像and的效果?
这是因为你操作的是否定后的条件。常规情况下,whereHas(P)->orWhereHas(U)是找「有P或有U」的数据,但反过来,对「没有P」或「没有U」做或运算,结果就是「不同时拥有P和U」——相当于把「同时有P和U」的那部分单独排除,和你直觉里的「排除P AND 排除U」完全不是一回事,这就是反向逻辑的陷阱。
和你另一问题的关联
你提到的《WhereNotIn inside WhereHas》本质也是类似的逻辑转换问题:当我们在查询中使用否定条件(whereNotIn、whereDoesntHave)时,不能直接套用正向条件的and/or逻辑,必须通过德摩根定律来转换,才能得到预期的结果。
内容的提问来源于stack exchange,提问作者Davidos

