ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Doctrine ORM Partial Hydration 详解:数组水合下按需加载实体部分字段

Doctrine ORM Partial Hydration 详解:数组水合下按需加载实体部分字段 Doctrine ORM Partial Hydration 详解数组水合下按需加载实体部分字段【免费下载链接】ormDoctrine Object Relational Mapper (ORM)项目地址: https://gitcode.com/gh_mirrors/or/ormPartial Hydration部分水合是 Doctrine ORM 提供的一种查询优化手段在使用数组水合array hydration时只从数据库加载实体字段的一个子集同时保留基于实体关联关系构建的嵌套结果结构。本文以 docs/en/reference/partial-hydration.rst 为骨架结合 Doctrine ORM 3.x 源码完整讲解PARTIAL关键字的 DQL 语法、解析与校验规则、SQL 生成原理、可用性边界为何仅限数组水合以及版本演进历史帮助你在大批量导出、列表渲染等场景中安全地使用这一优化特性。Partial Hydration 是什么Partial Hydration 指的是查询结果以数组而非实体对象的形式返回但只加载实体的一部分字段。与完全加载所有字段相比它减少了从数据库读取的列数、网络传输的数据量和水合阶段的内存占用。其核心特性是只加载字段子集DQL 中用PARTIAL 别名.{字段列表}声明需要加载的字段嵌套结构不变即使字段是部分加载通过 JOIN 关联产生的嵌套数组结构如user.addresses依然按照实体关系组织仅限数组水合该特性只在Query#getArrayResult()对应的 array hydrator 中允许使用。原文档给出的经典示例?php $users $em-createQuery( SELECT PARTIAL u.{id,name}, partial a.{id,street} FROM MyApp\Domain\User u JOIN u.addresses a )-getArrayResult();这个查询只从users表取出id、name两列从addresses表取出id、street两列但结果仍然是[[id …, name …, addresses [[id …, street …]]]]这样基于User → Address关联关系组织的嵌套数组。为什么需要 Partial Hydration原文档明确指出这是一项性能优化useful optimization适用于不需要实体全部字段的场景例如数据导出将大量记录导出为 CSV、JSON 或 Excel通常只需要少量关键列批量渲染列表页、报表页只需要展示几个字段却要为每条记录加载TEXT/BLOB等大字段内存敏感场景结果集很大时少加载一个包含大文本的列就能显著降低峰值内存占用。注意区分Partial Hydration 针对的是数组结果如果查询结果要水合成实体对象Doctrine 默认是禁止部分加载的详见下文为什么仅限数组水合因为部分对象会破坏实体不变式broken invariants这正是 docs/en/reference/partial-objects.rst 中详细讨论的问题。PARTIAL 的 DQL 语法语法规则从 src/Query/Parser.php 的语法注释可以看到PARTIAL表达式的正式文法PartialObjectExpression :: PARTIAL IdentificationVariable . PartialFieldSet PartialFieldSet :: { SimpleStateField {, SimpleStateField}* }即PARTIAL关键字 识别变量查询别名 点号 花括号包裹的字段列表字段之间用逗号分隔。?php // 单实体部分加载 $users $em-createQuery( SELECT PARTIAL u.{id, name} FROM MyApp\Domain\User u )-getArrayResult(); // 关联实体也部分加载原文档示例 $users $em-createQuery( SELECT PARTIAL u.{id, name}, partial a.{id, street} FROM MyApp\Domain\User u JOIN u.addresses a )-getArrayResult(); // 带 WHERE 条件与参数 $users $em-createQuery( SELECT PARTIAL u.{id, name} FROM MyApp\Domain\User u WHERE u.username ?1 )-setParameter(1, alice)-getArrayResult();嵌套embeddable字段从 Parser.php 的解析逻辑可以看出字段列表中的第一个字段以及逗号之后的每个字段都允许通过额外的点号继续解析——这意味着可嵌入对象embeddable的字段可以展开书写?php // 假设 User 内嵌了 AddressEmbeddable含 street、city 字段 $users $em-createQuery( SELECT PARTIAL u.{id, name, address.street, address.city} FROM MyApp\Domain\User u )-getArrayResult();字段集最后会作为字符串数组保存在 PartialObjectExpression.php 这个 AST 节点的partialFieldSet属性中供后续语义验证与 SQL 生成使用。语义约束哪些字段能出现在 PARTIAL 中PARTIAL不是想写什么字段就写什么字段。Parser 在解析完整个 DQL 后会通过processDeferredPartialObjectExpressions()Parser.php对每个部分表达式做延迟语义校验规则有两条1. 字段必须真实存在且可加载对partialFieldSet中的每个字段必须满足以下条件之一否则抛出语义错误There is no mapped field named X on class Y.是实体的普通字段映射存在于fieldMappings是to-one 关联的拥有侧owning side映射——即外键所在的一方associationMappings[field]-isToOneOwningSide()。?php // 错误status 字段并不存在于 User 的映射中 SELECT PARTIAL u.{id, nonExistent} FROM MyApp\Domain\User u2. 部分字段集必须包含完整标识符校验代码Parser.php要求if (array_intersect($class-identifier, $expr-partialFieldSet) ! $class-identifier) { $this-semanticalError( The partial field selection of class . $class-name . must contain the identifier., ... ); }即字段列表必须包含实体的全部主键字段复合主键则全部列出。这是因为 Doctrine 需要主键来组装结果、维护身份映射缺少主键的 PARTIAL 查询会在解析阶段直接被拒绝?php // 错误缺少主键 id SELECT PARTIAL u.{name} FROM MyApp\Domain\User u底层原理从 DQL 到 SQL 的转换1. 词法与语法解析PARTIAL是 DQL 词法器TokenType.php识别的一个关键字。Parser 在 SELECT 子句表达式中遇到T_PARTIAL标记时进入PartialObjectExpression()解析分支Parser.php将字段集包装成 AST 节点。2. 标记部分加载并生成 SQLSQL 生成阶段在 SqlWalker.php 完成。当 SELECT 表达式是PartialObjectExpression时SqlWalker 会给当前查询设置内部提示HINT_PARTIALdoctrine.partial定义于 SqlWalker.php见 SqlWalker.php调用walkObjectExpression()为每个被选中实体生成列清单。walkObjectExpression()的核心逻辑SqlWalker.php是遍历类的全部fieldMappings然后做字段过滤foreach ($class-fieldMappings as $fieldName $mapping) { if ($partialFieldSet ! in_array($fieldName, $partialFieldSet, true)) { continue; // 部分加载不在字段集中的列直接跳过 } // ... 生成 sqlTableAlias.columnName AS columnAlias }换句话说未列出的字段根本不会进入 SELECT 列表——这正是 Partial Hydration 减少数据库 IO 的根本原因。对于继承映射SqlWalker 还会依据字段所属的继承层级选择正确的表$mapping-inherited见 SqlWalker.php。3. 注册到结果集映射生成 SQL 的同时SqlWalker 会把部分加载信息注册进ResultSetMapping顶层实体通过markPartialEntityResult()标记SqlWalker.php被 JOIN 的实体通过addJoinedEntityResult(..., $isPartial)的布尔参数标记ResultSetMapping.php这些标记最终落入ResultSetMapping::$partialAliasesResultSetMapping.php水合器正是依据它来判断哪些别名是部分加载的。4. 数组水合在 array hydrator 中部分加载的列被直接组装进嵌套数组。整个链路为Query#getArrayResult()→ArrayHydrator→ResultSetMapping→ 按关联结构组装嵌套结果。为什么仅限数组水合对象水合被明确禁止原文档强调 Partial Hydration 只允许在array hydrator中使用。如果试图用getResult()对象水合执行包含PARTIAL的查询会抛出异常。这个行为在源码中有两处明确体现1. 水合层的异常src/Internal/Hydration/HydrationException.php 定义了专门异常public static function partialObjectHydrationDisallowed(): self { return new self(Hydration of entity objects is not allowed when DQL PARTIAL keyword is used.); }2. 对象水合器对部分别名的识别ObjectHydrator.php 与 SimpleObjectHydrator.php 会读取partialAliases并把isPartial提示传递给UnitOfWork::createEntity()而 UnitOfWork.php 仅在启用了 PHP 8.4 原生懒对象native lazy objects时才允许通过isPartial提示创建部分加载的懒 ghost 对象否则相关路径会受限。为什么禁止对象水合的部分对象原因在 partial-objects.rst 中讲得很清楚部分对象是不变量被破坏的对象broken invariants调用方无法区分关联字段本来就是 NULL还是关联字段尚未加载极易引发空指针等问题。因此 Doctrine 的默认策略是要对象就加载完整对象只要部分字段就请使用数组水合。部分加载与查询缓存、二级缓存从 DefaultQueryCache.php 可以看到查询缓存对携带HINT_PARTIAL或历史遗留的HINT_FORCE_PARTIAL_LOAD提示的查询做了特殊分流处理。可以推断部分加载查询带有结果取决于运行时字段子集的特性不应与常规查询共享同样的缓存策略。实际项目中如果既用了PARTIAL又期望命中二级缓存需要仔细核对缓存键与提示的交互避免拿到错误的缓存结果。历史遗留的 HINT_FORCE_PARTIAL_LOAD 与版本演进版本演进ORM 3.x 的反复PARTIAL关键字的历史在 UPGRADE.md 中有完整记录这是理解本文档背景的关键ORM 3.0作为 BC BREAKPARTIAL关键字、PartialObjectExpressionAST、SqlWalker::HINT_PARTIAL、Query::HINT_FORCE_PARTIAL_LOAD以及EntityManager::getPartialReference()全部被移除ORM 3.2PARTIAL关键字被重新引入但只允许用于数组水合即本文讨论的 partial hydrationPartialObjectExpression与SqlWalker::HINT_PARTIAL也随之一并恢复。这也解释了为什么当前文档明确限定Partial hydration of entities is allowed in the array hydrator——这是 3.2 重新引入时的刻意收窄数组水合安全对象水合仍被禁止。遗留的 HINT_FORCE_PARTIAL_LOAD在更早的 ORM 2.x 时代开发者可以通过查询提示Query::HINT_FORCE_PARTIAL_LOAD常量定义于 src/Query.php强制查询以部分对象形式水合实体。相关说明仍保留在 dql-doctrine-query-language.rstQuery::HINT_FORCE_PARTIAL_LOAD— Allows to hydrate objects although not all their columns are fetched... 该提示已废弃并将在未来移除。历史异常 QueryException::partialObjectsAreDangerous() 的文案也印证了这条演进路径Loading partial objects is dangerous. Fetch full objects or consider using a different fetch mode. —— 当前代码中这条路径已基本被仅数组水合 可选 PHP 8.4 原生懒对象取代。PHP 8.4 原生懒对象下的 Partial 对象关联阅读如果要在对象形态下享受先加载部分字段、按需补全的收益partial-objects.rst 给出了 PHP 8.4 的现代方案启用原生懒对象后PARTIAL查询返回的对象表现为lazy ghost仅PARTIAL列出的字段被急切加载其余标量字段保持未初始化首次访问未加载字段时Doctrine 自动发起SELECT补全整个实体此后该对象与普通查询加载的对象无异在懒初始化触发之前修改已加载字段是安全的——内存中的修改值会被保留不会被数据库值覆盖。这一点在 UnitOfWork.php 的HINT_REFRESH_ENTITY处理与 tests/Tests/ORM/Functional/PartialObjectsTest.php 中都有源码级验证测试用例证实部分查询后originalEntityData只包含id、name修改name再触发懒初始化name的修改不被覆盖且变更集快照仍保留数据库原始值。注意getPartialReference()API 在 ORM 3.0 中已被移除UPGRADE.md不要在新代码中使用。性能验证与适用边界仓库的基准测试目录提供了两个与本文主题直接相关的性能基准SimpleQueryPartialObjectHydrationPerformanceBench.php简单查询的部分对象水合性能基准MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php混合 fetch join 场景下的部分水合基准。两者都通过Query::HINT_FORCE_PARTIAL_LOAD驱动见 MixedQueryFetchJoinPartialObjectHydrationPerformanceBench.php可用phpbench运行参考仓库根目录 phpbench.json。何时该用 / 何时不该用推荐场景结果集大、字段多尤其是包含TEXT/BLOB/长字符串列只需要少量列用于导出、渲染、统计使用getArrayResult()而非对象水合。谨慎/避免场景需要实体对象的完整行为请加载完整对象或使用 PHP 8.4 原生懒对象字段子集无法确定包含全部主键语法校验直接拒绝字段集太小导致反复查询过早优化反而增加复杂度。总结Partial Hydration 是 Doctrine ORM 为数组化、少字段、大批量读取场景提供的精准优化工具语法上使用PARTIAL 别名.{字段,...}字段集必须包含完整主键且只能引用真实字段或 to-one 拥有侧关联底层由 Parser 延迟语义校验、SqlWalker 过滤字段生成精简 SQL、ResultSetMapping 记录部分别名、ArrayHydrator 组装嵌套数组链路清晰完整它被刻意限制在数组水合范围内——对象水合遇PARTIAL会直接抛异常这是 Doctrine 对部分对象危险性broken invariants的防御性设计该特性经历了 ORM 3.0 移除、3.2 为数组水合重新引入的演进理解这段历史有助于避免在升级时踩坑若坚持使用对象形态的部分加载请转向 PHP 8.4 原生懒对象方案并参考 PartialObjectsTest.php 理解其变更集语义。进一步阅读完整语法与更多 DQL 特性见 dql-doctrine-query-language.rst部分对象问题与 PHP 8.4 懒加载详见 partial-objects.rst升级注意事项见 UPGRADE.md。【免费下载链接】ormDoctrine Object Relational Mapper (ORM)项目地址: https://gitcode.com/gh_mirrors/or/orm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表