Doctrine getEntityChangeSet处理JSON字段误判变更的解决方案
问题场景
编写方法获取实体变更字段:
public function getUpdatedColumns(Shop $shop): array { $uow = $this->entityManager->getUnitOfWork(); $uow->computeChangeSets(); return $uow->getEntityChangeSet($shop); }
执行后返回结果如下,新旧值仅存在JSON格式的空格差异,语义完全一致,但被判定为字段更新:
array(1) { ["mailConfig"]=> array(2) { [0]=> string(25) "{"transport": "sendgrid"}" [1]=> string(24) "{"transport":"sendgrid"}" } }
数据库中该字段类型为JSON,需要解决非语义格式差异导致的变更集误判问题。
Doctrine ORM的UnitOfWork默认对字段值做严格字节/值比对:如果JSON字段在PHP侧存储的是字符串格式,它不会解析JSON语义,会直接比对字符串的字节差异,因此空格、键排序、转义格式这类不影响实际业务含义的格式差异,都会被判定为字段发生更新,产生无意义的变更集。
按推荐优先级从高到低排列:
自定义语义化JSON字段类型(一劳永逸方案)
继承Doctrine内置的JsonType,重写值比对逻辑和入库编码逻辑,从ORM层面统一处理JSON的语义比对和格式标准化,不需要在业务代码中做额外判断。示例代码:<?php namespace App\DBAL\Types; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\JsonType; class SemanticJsonType extends JsonType { public function getName(): string { return 'semantic_json'; } /** * 入库时统一用固定参数编码JSON,从根源消除格式差异 */ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string { if ($value === null) { return null; } // 统一编码规则:无多余空格、不转义中文和斜杠,编码错误直接抛出异常 return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); } /** * 重写值比对逻辑,做语义级比对而非字符串严格比对 */ public function equals(mixed $oldValue, mixed $newValue): bool { return $this->convertToDatabaseValue($oldValue, null) === $this->convertToDatabaseValue($newValue, null); } }写完后在Doctrine配置中注册该自定义类型,将所有需要语义比对的JSON字段的映射类型从默认
json改为semantic_json即可,后续UnitOfWork计算变更集会自动走自定义比对逻辑,不会出现格式差异导致的误判。字段赋值时统一做标准化处理
如果不想修改全局类型映射,可以在JSON字段的Setter方法中,对传入值做统一标准化处理,保证实体属性中存储的JSON值永远是格式统一的,避免出现字节差异。示例:public function setMailConfig(array|string $mailConfig): self { // 传入字符串先解码为数组,统一数据结构 if (is_string($mailConfig)) { $mailConfig = json_decode($mailConfig, true, 512, JSON_THROW_ON_ERROR); } $this->mailConfig = $mailConfig; return $this; }注意要将字段的Doctrine映射类型设为
json,不要设为string:此时Doctrine从数据库读取、写入该字段时会自动做JSON编解码,PHP侧始终操作数组/对象结构,不会出现字符串格式差异问题。变更集计算后做二次过滤(补丁方案)
如果不方便修改字段类型和实体逻辑,可以在调用$uow->computeChangeSets()之后,手动遍历获取到的变更集,对JSON字段的新旧值做解码后按统一规则编码再比对,把无实际语义变更的字段从变更集中剔除即可。该方案侵入性强,逻辑分散,不推荐长期使用。
注意:不要直接用
json_decode之后的松散等于(==)做比对,该比对会忽略数据类型差异(比如字符串格式的数字和数字类型会被判定为相等),可能漏判真实的业务变更,统一编码为标准格式JSON字符串后做严格相等比对是最稳妥的方案。
内容的提问来源于stack exchange,提问作者Toma Tomov

