数据库ORM后端【免费下载链接】ormDoctrine Object Relational Mapper (ORM)项目地址https://gitcode.com/gh_mirrors/or/orm点击查看免费下载装饰器模式Decorator Pattern允许在不修改原有类的前提下动态地为一个对象附加职责其对象结构天然形成一条链。当这条链上的对象需要落库时传统关系型数据库的扁平化表结构往往让人无从下手。本指南基于 decorator-pattern.rst 的完整配方结合 Doctrine ORM 仓库源码演示如何利用单表继承Single Table Inheritance MappedSuperclass 级联持久化Cascade Persist三个机制的组合把装饰器链当作普通实体一样持久化与查询。读完本文你将掌握装饰器模式与 ORM 映射的正确结合方式理解级联操作在 UnitOfWork 中的底层执行逻辑并能直接复用本文的完整代码。一、问题背景装饰器模式为什么难以持久化装饰器模式的核心结构包含四类角色Component抽象组件定义对象接口ConcreteComponent具体组件被装饰的原始对象Decorator抽象装饰器持有对 Component 的引用接口与 Component 保持一致ConcreteDecorator具体装饰器真正附加额外职责的类。在内存中ConcreteDecorator内部引用着另一个Component可能又是一个装饰器形成一条可无限嵌套的装饰链。而关系型数据库没有引用概念只有外键。因此要把装饰器模式持久化必须回答两个问题同一继承层次中的实体Component、ConcreteComponent、ConcreteDecorator如何映射到数据库表装饰器持有的被装饰对象如何映射为关联并保证整条链被一次性写入Doctrine ORM 给出的答案是继承映射解决类层次落库MappedSuperclass OneToOne 级联解决装饰链落库。二、整体设计四个类、一张表本文配方由四个 PHP 类组成其继承与关联关系如下类角色映射类型说明Test\Component抽象组件#[Entity] 单表继承继承层次的根定义鉴别器列与映射表Test\Component\ConcreteComponent具体组件#[Entity]仅继承ComponentTest\Decorator抽象装饰器#[MappedSuperclass]不被持久化但持有对Component的 OneToOne 关联Test\Decorator\ConcreteDecorator具体装饰器#[Entity]继承Decorator新增special字段由于Component位于继承层次顶端它必须定义持久化继承策略。本配方使用Single Table Inheritance单表继承即所有子类共享同一张数据库表通过鉴别器列discriminator column区分具体类型文档同时指出 Class Table InheritanceJOINED每类一表同样适用。在鉴别器映射表discriminator map中需要注册两个具体子类ConcreteComponent与ConcreteDecorator。三、Component定义继承根与鉴别器?php namespace Test; #[Entity] #[InheritanceType(SINGLE_TABLE)] #[DiscriminatorColumn(name: discr, type: string)] #[DiscriminatorMap([cc Component\ConcreteComponent::class, cd Decorator\ConcreteDecorator::class])] abstract class Component { #[Id, Column] #[GeneratedValue(strategy: AUTO)] protected int|null $id null; #[Column(type: string, nullable: true)] protected $name; public function getId(): int|null { return $this-id; } public function setName(string $name): void { $this-name $name; } public function getName(): string { return $this-name; } }这段代码中几个关键属性的底层语义在仓库源码中都有明确对应#[InheritanceType(SINGLE_TABLE)]对应 src/Mapping/InheritanceType.php其构造参数被限定为NONE | JOINED | SINGLE_TABLE三种取值。选择SINGLE_TABLE时Doctrine 会为整个层次生成一张表并在每条记录上写入鉴别器列的值#[DiscriminatorColumn(name: discr, type: string)]对应 src/Mapping/DiscriminatorColumn.php支持name、type、length、columnDefinition、enumType、options等参数。这里指定数据库列名为discr类型为字符串#[DiscriminatorMap([cc ..., cd ...])]对应 src/Mapping/DiscriminatorMap.php其参数是一个鉴别器值 → 类名的映射数组。cc与cd是写入数据库discr列的实际值Doctrine 依据该值在查询时实例化正确的类。需要特别强调的是鉴别器映射表必须覆盖继承层次中所有需要落库的具体实体类。本例中需要注册ConcreteComponent与ConcreteDecorator两个类而中间的Decorator是 MappedSuperclass不会单独落库因此无需也不应出现在映射表中。四、ConcreteComponent最简单的具体组件?php namespace Test\Component; use Test\Component; #[Entity] class ConcreteComponent extends Component {}ConcreteComponent几乎没有任何额外代码仅仅为了保持示例简单而继承抽象类Component。它是装饰链的末端节点——被装饰的原始对象也是最终写入数据库表中的一个普通行discr cc。五、DecoratorMappedSuperclass 与装饰关联?php namespace Test; #[MappedSuperclass] abstract class Decorator extends Component { #[OneToOne(targetEntity: Component::class, cascade: [all])] #[JoinColumn(name: decorates, referencedColumnName: id)] protected $decorates; /** * initialize the decorator * param Component $c */ public function __construct(Component $c) { $this-setDecorates($c); } /** * (non-PHPdoc) * see Test.Component::getName() */ public function getName(): string { return Decorated . $this-getDecorates()-getName(); } /** the component being decorated */ protected function getDecorates(): Component { return $this-decorates; } /** sets the component being decorated */ protected function setDecorates(Component $c): void { $this-decorates $c; } }这是整个配方的核心包含三个值得深挖的设计决策5.1 为什么装饰器用 MappedSuperclassDecorator本身是一个抽象类不需要被持久化但它需要声明与持久化实体Component的关联。#[MappedSuperclass]正是为此设计它对应的 src/Mapping/MappedSuperclass.php 标注了Attribute::TARGET_CLASS其含义是该类本身不成为实体、不映射为表但其映射定义会被继承它的实体类所复用。在 src/Mapping/ClassMetadataFactory.php 中可以看到当构建子类元数据时如果父类是 MappedSuperclass其自定义仓库类等信息会被继承同时 第 177 行 的逻辑表明isMappedSuperclass的类不会触发缺少继承类型声明的校验。这意味着ConcreteDecorator继承自 MappedSuperclass 后会自动获得decorates关联字段的定义而Decorator自身不会生成数据库表。5.2 OneToOne 关联与cascade: [all]装饰器包装一个 Component本质是一个一对一OneToOne关系。#[OneToOne(targetEntity: Component::class, cascade: [all])]对应 src/Mapping/OneToOne.php该属性支持targetEntity、mappedBy、inversedBy、cascade、fetch默认LAZY、orphanRemoval等参数。cascade: [all]是整条装饰链能够一键落库的关键它表示对关联目标执行所有生命周期级联操作persist、remove、merge、detach、refresh 等。正如文档所述对Decorator的一切操作持久化、删除等都会级联到其装饰的Component上——当你持久化一个Decorator时Doctrine 会替你持久化整条被装饰对象的链条。从持久化角度而言Decorator完全可以被当作一个普通Component对待。底层实现可以在 src/UnitOfWork.php#L2230 的cascadePersist()方法中看到它遍历当前实体元数据中所有isCascadePersist()为真的关联映射逐个取出关联实体并调用doPersist()递归持久化cascadeRemove()在 src/UnitOfWork.php#L2292 对应删除方向。这正是持久化一个装饰器 持久化整条链的原理支撑。#[JoinColumn(name: decorates, referencedColumnName: id)]指定外键列名为decorates引用Component表的主键id。其完整参数name、referencedColumnName、nullable、onDelete、unique、columnDefinition、foreignKeyName等定义在 src/Mapping/JoinColumnProperties.php 中。5.3 用 protected 方法隐藏装饰关系setDecorates()/getDecorates()被声明为protected这是装饰器模式的一个精妙细节对外部调用者隐藏这个对象正在装饰另一个对象的事实从而保证Decorator的对外接口与Component完全一致——调用方无需感知自己拿到的是普通组件还是被装饰过的组件。getName()被重写为Decorated . $this-getDecorates()-getName()演示了装饰器如何在调用链上附加数据每嵌套一层装饰器返回字符串就多一层前缀。六、ConcreteDecorator真正附加职责的装饰器?php namespace Test\Decorator; use Test\Decorator; #[Entity] class ConcreteDecorator extends Decorator { #[Column(type: string, nullable: true)] protected string|null $special null; public function setSpecial(string|null $special): void { $this-special $special; } public function getSpecial(): string|null { return $this-special; } /** * (non-PHPdoc) * see Test.Component::getName() */ public function getName(): string { return [ . $this-getSpecial() . ] . parent::getName(); } }ConcreteDecorator是完成装饰器模式实现所需的最后一个类。为了进一步演示装饰器在数据穿过装饰链时如何修改数据它新增了一个可空字符串字段special并再次重写getName()在父类返回值的基础上追加getSpecial()的结果最终输出形如[Really] Decorated Test Component 2的字符串。至此四层继承结构全部就绪可以开始落库。七、完整示例持久化与查询装饰链下面是在已配置好 Doctrine ORM 且$em为EntityManager实例的前提下完整的持久化与检索代码?php use Test\Component\ConcreteComponent, Test\Decorator\ConcreteDecorator; // assumes Doctrine ORM is configured and an instance of // an EntityManager is available as $em // create a new concrete component $c new ConcreteComponent(); $c-setName(Test Component 1); $em-persist($c); // assigned unique ID 1 // create a new concrete decorator $c new ConcreteComponent(); $c-setName(Test Component 2); $d new ConcreteDecorator($c); $d-setSpecial(Really); $em-persist($d); // assigns c as unique ID 2, and d as unique ID 3 $em-flush(); $c $em-find(Test\Component, 1); $d $em-find(Test\Component, 3); echo get_class($c); // prints: Test\Component\ConcreteComponent echo $c-getName(); // prints: Test Component 1 echo get_class($d) // prints: Test\Component\ConcreteDecorator echo $d-getName(); // prints: [Really] Decorated Test Component 27.1 执行流程剖析整个流程可以拆解为四步持久化第一个组件persist($c)后flush()单表继承下Test Component 1以discrcc写入共享表获得主键 ID 1构造装饰链新建第二个组件Test Component 2再以它为构造参数创建ConcreteDecorator并设置specialReally。此时内存中的对象图是d.decorates c级联持久化persist($d)只针对装饰器但由于decorates关联配置了cascade: [all]UnitOfWork在flush()时会顺着关联链把c一并持久化。注意$c此处没有单独调用persist()它的落库完全由级联驱动最终c获得 ID 2d获得 ID 3按基类查询由于单表继承$em-find(Test\Component, ...)能直接查出整个继承层次中的任意具体类型。ID 1 的行鉴别器值为cc实例化为ConcreteComponentID 3 的行鉴别器值为cd实例化为ConcreteDecorator且其decorates外键指向 ID 2 的组件。Doctrine 依据鉴别器映射在查询阶段完成了正确的类型还原。7.2 输出验证get_class($c)输出Test\Component\ConcreteComponent$c-getName()输出Test Component 1——普通组件未被装饰名称原样返回get_class($d)输出Test\Component\ConcreteDecorator$d-getName()输出[Really] Decorated Test Component 2——装饰链完整生效ConcreteDecorator的specialReallyDecorator的前缀Decorated 被装饰组件的原始名称Test Component 2。八、级联持久化的源码级验证上述只 persist 装饰器、链上组件自动落库的行为可以在 src/UnitOfWork.php#L2230 的cascadePersist()中看到完整实现通过$class-associationMappings过滤出所有isCascadePersist()为真的关联即配置了cascade包含 persist 的关联用propertyAccessors读取关联字段的实际值根据值是PersistentCollection、Collection、数组to-many还是单个实体to-one对应$relatedEntities ! null分支分别处理对每个关联实体递归调用doPersist()从而把整个对象图包括嵌套的装饰链纳入持久化计划。同理删除方向由 src/UnitOfWork.php#L2292 的cascadeRemove()负责确保删除装饰器时被装饰的组件也会按级联规则处理。这也解释了为何文档强调Decorator可以被当作Component一样持久化——级联机制抹平了装饰器与普通组件在持久化语义上的差异。九、继承策略选型SINGLE_TABLE 还是 JOINED本配方使用SINGLE_TABLE单表继承但文档明确指出Class Table InheritanceJOINED同样适用。二者的取舍可以从持久化实现上理解单表继承SINGLE_TABLE整个层次共一张表所有类的字段合并存放通过鉴别器列区分。查询无需 JOIN性能好但表结构会随层次中字段增多而膨胀。其持久化逻辑位于 src/Persisters/Entity/SingleTablePersister.php类表继承JOINED每个类一张表父类与子类表通过主键关联需要 JOIN 查询数据更规范但写入与查询开销更大。其持久化逻辑位于 src/Persisters/Entity/JoinedSubclassPersister.php。对于装饰器模式这种层次较浅、链较长的场景单表继承通常更合适装饰链在内存中层层嵌套落库时所有节点写同一张表查询一条链无需多次 JOIN。若你的业务对表结构规范性要求更高、且层次中字段差异很大则可改用#[InheritanceType(JOINED)]其余映射代码保持不变。两种策略的完整对比可参阅 继承映射参考文档。十、实践要点与注意事项鉴别器映射必须完整DiscriminatorMap要包含所有需要落库的具体实体子类本例为ConcreteComponent、ConcreteDecorator。遗漏会导致运行期无法正确实例化对应类型MappedSuperclass 不参与鉴别Decorator作为 MappedSuperclass 不落库、不进入鉴别器映射它只是把decorates关联传递给ConcreteDecorator。抽象装饰器也可以直接在Component之下再嵌套一层实体继承但那样会额外引入一张继承分支通常没有必要级联配置决定链的完整性cascade: [all]保证持久化、删除等操作沿装饰链传导。如果只想持久化而不想级联删除也可以显式指定cascade: [persist, merge]等子集OneToOne的cascade参数本身是字符串数组单表继承下的可空列装饰器特有的special字段在单表继承中会落在共享表上因此普通ConcreteComponent的该列值为 NULL。代码中special声明为nullable: true正是为此——若改为非空向表中写入普通组件将失败依赖注入与构造器Decorator的构造函数要求传入一个Component实例符合装饰器模式的语义Doctrine 持久化时并不调用该构造器通过反射直接实例化因此构造函数的存在不影响 ORM 正常工作。十一、总结本配方完整演示了用 Doctrine ORM 持久化装饰器模式的四个关键技巧用单表继承承载继承层次、用鉴别器列/映射表区分具体类型、用MappedSuperclass承载不被持久化的抽象装饰器、用带级联的 OneToOne 关联把装饰链映射为外键并实现整链级联落库。其核心结论是只要把装饰器当作普通实体把被装饰对象建模为级联关联装饰器模式的对象图就能被 ORM 透明地持久化与还原且查询时通过鉴别器自动还原出正确的具体类型。文中涉及的映射属性、级联逻辑与继承持久化实现均可直接在 src/Mapping 目录、src/UnitOfWork.php 以及 src/Persisters/Entity 目录下的源码中进一步研读相关的继承与关联映射细节还可参考 继承映射、关联映射 与 基础映射 文档。赞分享数据库ORM后端【免费下载链接】ormDoctrine Object Relational Mapper (ORM)项目地址https://gitcode.com/gh_mirrors/or/orm点击查看免费下载相关推荐eggjs/orm-decorator 使用指南在 Tegg 中以装饰器方式声明 Leoric 数据模型eggjs/orm decorator 使用指南在 Tegg 中以装饰器方式声明 Leoric 数据模型 导读 eggjs/orm decorator 是后端Web框架装饰器模式Decorator Pattern实战指南在 .NET/C 中优雅地处理横切关注点装饰器模式Decorator Pattern实战指南在 .NET/C 中优雅地处理横切关注点 装饰器模式是 GoF 二十三种经典设计模式之一它允许在不修文档知识库eggjs/orm-decorator 全指南tegg 声明式 ORM 模型的版本演进、装饰器 API 与源码实现eggjs/orm decorator 全指南tegg 声明式 ORM 模型的版本演进、装饰器 API 与源码实现 导读 eggjs/orm decora后端Web框架上一篇Laravel Page Speed 实战案例如何在电商网站中应用性能优化下一篇从入门到精通custom-device-emulation-chrome的完整学习路径 创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
