返回博客

ThinkPHP where('col', '') 无法匹配 NULL 行,UPDATE 永远返回 0

某次数据修复任务:一张门店表里有几条记录,某个关联字段明显是"空的"——print_r 打印出来是 [offline_org_id] =>,和空字符串在视觉上完全一样。于是写了一条很自然的 ThinkPHP 链式更新:

$n = $db->table('store')
    ->where('code', 'S206444')
    ->where('org_id', '')    // 看着是空,就 where 空字符串
    ->update(['org_id' => 'ORG_192645']);
// $n === 0

执行完,返回 0。反复确认 code 条件没问题,目标行就在那里,但就是改不到。

现象

  • UPDATE 受影响行数为 0,不报错,不抛异常
  • print_r / var_dump 打印该字段时显示为空([org_id] =>),与真正的空字符串视觉无差
  • 换成 find() 查询配合 where('org_id', '') 也返回 null,仿佛行不存在

根因

两个层面的陷阱叠加在一起:

第一层——显示欺骗:PHP 的 print_r 输出 NULL 和 '' 的外观完全相同,都是 [field] =>。你看到的"空",底层可能是 NULL,也可能是真正的空字符串 '',肉眼无法区分。

第二层——SQL NULL 语义:ANSI SQL 规定,任何用 = 与 NULL 比较的表达式结果都是 NULL(不是 true 也不是 false),WHERE 子句把 NULL 视为 false,该行被直接排除。ThinkPHP 的 where('col', '') 会生成 WHERE col = '',遇到列值为 NULL 的行,一条都匹配不到。

坑:ORM 把空字符串直传 SQL,NULL 行永远不命中

where('col', $val) 在 ThinkPHP 5/6 和 Laravel Eloquent 里都只是把 $val 拼成 col = $val。当 $val = '' 时生成 col = '',而数据库里列值是 NULL 的行,在 col = '' 的条件下求值结果是 NULL,被 WHERE 直接过滤掉。数据修复脚本、批量回填脚本最容易踩这个坑——表面上跑完了,实际上一行没改到。

诊断三连

一旦 UPDATE 返回 0 行,先跑这条 SQL 把 NULL 和空字符串区分开:

SELECT
    org_id,
    org_id IS NULL  AS is_null,
    org_id = ''     AS is_empty,
    LENGTH(org_id)  AS len
FROM store
WHERE code = 'S206444';

判读规则:

| is_null | is_empty | len | 实际值 | |---------|----------|------|-----------| | 1 | NULL | NULL | NULL | | 0 | 1 | 0 | '' | | 0 | 0 | N | 有内容 |

LENGTH 返回空白(不是 0)就是 NULL,锁定根因。

正解

把条件改成 IS NULL OR = '' 兜底,三种写法任选:

方案 A:Raw SQL(最稳)

$n = $db->execute(
    "UPDATE store SET org_id = :new
     WHERE code = :code
       AND (org_id IS NULL OR org_id = '')",
    ['new' => 'ORG_192645', 'code' => 'S206444']
);

方案 B:ThinkPHP 链式

$n = $db->table('store')
    ->where('code', 'S206444')
    ->where(function ($q) {
        $q->whereNull('org_id')->whereOr('org_id', '');
    })
    ->update(['org_id' => 'ORG_192645']);

方案 C:Laravel Eloquent

DB::table('store')
    ->where('code', 'S206444')
    ->where(function ($q) {
        $q->whereNull('org_id')->orWhere('org_id', '');
    })
    ->update(['org_id' => 'ORG_192645']);

修完之后立即验证:

SELECT org_id, org_id IS NULL AS still_null
FROM store WHERE code = 'S206444';
-- 期望: still_null=0, org_id='ORG_192645'
正解:任何'空'字段条件一律写 IS NULL OR = ''

写数据修复脚本时,只要字段含义是"无值",就必须用 IS NULL OR col = '' 双条件。依赖 ORM 自动处理、或凭 print_r 目测判断,都是不可靠的。

预防

  • Schema 层:如果业务语义是"无值",选定一种表达方式并坚持——要么 NOT NULL DEFAULT '',要么 DEFAULT NULL,但不要让 NULL 和空字符串共存于同一列
  • 修复脚本范式:养成双条件反射,将 IS NULL OR = '' 作为所有"空"字段匹配的默认写法
  • 对偶陷阱:注意反向坑——whereOr('col', $emptyVar) 在某些写法下会匹配所有空值行,读路径慎用

一句话外卖

print_r 看着是空,SQL 里不一定是 '';ORM 的 where('col', '') 永远跳过 NULL 行,数据修复脚本必须写 IS NULL OR = ''