6

EF Core从TPH迁移到TPT - 波多尔斯基

 1 year ago
source link: https://www.cnblogs.com/podolski/p/17284449.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Intro#

EF Core支持多种方式处理具有继承关系的表,现在支持TPHTPC(EF Core 7)、TPT,具体的实现方式可以参考官方文档这篇文章

大致总结一下不同的方式的区别:
TPH:所有的类型都放在一张表中,使用discriminator字段用以区别不同的类型
TPT:不同的子类型有单独的表存放子类独有的字段,父虚类型也有一张单独的表存放共有的字段。
TPC:不为父虚类新建表,只有子类型有单独的表,并且表内有父类和子类所有的字段。

由于TPT两张表的外键关联设计,在进行查询时,会自动进行的JOIN等连表查询操作,因此极限性能不太行。需要经常用查询父类的情况,TPH就挺好;需要经常查询子类的时候,TPC就非常适合。按照官方的说法,正常情况TPH就已经满足大多数的场景(这也是EF Core的默认设置),性能也是数一数二的,如果遇到了需要经常单独查询子类型的问题,可以优先考虑TPC,仅在一些特殊情况下应该考虑TPT。哪些是特殊情况?

请查阅官网这篇文章的详细讨论以了解三种不同方式对EF Core生成SQL的影响。

可能适合的场景#

我遇到的这么一个场景,有以下特点:

  • 子类非常多,并且不同的子类字段的区别也很大,使用TPH会使得这个表格的规格非常大,并且空字段非常多。
  • 继承的层级很短,只有一层继承关系。
  • 需要经常进行基于父类的查询,直接在一张表执行查询的效率要比在的TPC分布在不同表中查询的效率高。(注意,这里说的父类的查询是指直接使用Raw SQL的查询,使用EF Core在父类的查询会翻译成非常多的LEFT JOIN,导致性能低下。)

直接使用TPH或者使用TPC都不是非常满意,而TPT提供了一张父类的表存储公共的字段的这种方法,就显得非常适合。

注:TPC不符合数据库范式设计原则,TPH在空字段非常多的情况下也非常不优雅,强迫症可以使用TPT。

迁移#

如果是空表的话,直接使用EF Migration就可以了,麻烦的已经有既有数据的情况,由于数据表引用的对象从的总表转移到了子类表,因此直接执行的数据库迁移会提示违反了外键约束。

23503: insert or update on table "AD_AnimalCamera_Data" violates foreign key constraint "FK_AD_AnimalCamera_Data_AD_AnimalCamera_Infos_AttachDeviceId"

解决方案:

  1. 手动创建表,并将TPH表中的不同的子类型记录转移到不同的子类表中。
  2. 通过自编程序载入对象,进行持久化,然后清空所有表的数据,创建表,载入数据并通过EF Core插入。

由于数据量比较大,而且还有继承关系,手动去操作还是麻烦了一些,可以使用SQL查询进行简化;而第二个方案将由EF Core帮我们将数据插入到正确的位置。

方案1#

准备临时数据库#

将原来的数据库结构复制一份,并设置为开发环境。接下来修改数据库结构,TPH迁移到TPT模式,只需要在每一个子类表上使用[Table("")]标记就行了(当然也可以使用FluentAPI)。标记好了之后,使用EF Migration:

add-migration migrateTPT

由于是只有结构的空表,直接操作就可以成功了。

迁移数据到临时数据库#

将旧有数据传输到新的数据表中,尤其注意TPH与TPT之间表的在处理继承关系时的不同。

以AttachDeviceInfo为abstract类,AD_Insect_Info作为其中的一个子类

更新之后TPH表中的大量字段转移到了子类表中,因此可以使用数据库同步工具进行数据同步,忽略多余的字段就可以了。对于的TPT生成的子类表,通过Id字段与抽象类表进行匹配连接,因此需要手动插入对应类别的数据。

INSERT into "AD_Insect_Infos"
SELECT "Id",FALSE from "AttachDeviceInfos" WHERE "AttachDeviceTypeId" = 1

如果没有AttachDeviceTypeId字段,那么需要在TPH阶段先通过discriminator将不同子类区分开,这个会麻烦一点。

转移回数据库#

清空目标数据库(包括结构),并将临时数据库中的表同步到目标数据库中,手动调整_EFMigration表格的记录(指向最新版本),完成切换。

方案2#

备份数据#

在数据库还是原来结构的情况下,我们需要将现有的数据进行序列化,之前我写过一篇序列化文章,使用的是PROTOBUF序列化。这里由于传输的数据结构比较简单,可以使用System.Text.Json类库Json序列化到文件。

对于有继承关系的表的序列化,.NET 7的System.Text.Json新增了对应的支持,可以参考文档的相关实现。

准备临时数据库#

将原来的数据库结构复制一份,并设置为开发环境。接下来修改数据库结构,TPH迁移到TPT模式,只需要在每一个子类表上使用[Table("")]标记就行了(当然也可以使用FluentAPI)。标记好了之后,使用EF Migration:

add-migration migrateTPT

由于是只有结构的空表,直接操作就可以成功了。

迁移数据到临时数据库#

由于临时数据库结构已经和既有数据库不同,无法通过程序直接连接两个数据库进行数据导入的操作,因此需要将数据反序列化到的新的数据库。

转移回数据库#

清空目标数据库(包括结构),并将临时数据库中的表同步到目标数据库中,手动调整_EFMigration表格的记录(指向最新版本),完成切换。

总结#

迁移到TPT时,可以使用临时数据库中转,将数据库的数据以新的结构存储下来,然后再同步到新数据库。当然也可以直接在正式数据库中操作:直接持久化,清空数据,然后再还原数据。当然这么风险更高,强调一点,在生产的数据库中进行操作需要格外谨慎,务必做好备份。

可以发现,在数据库中使用外键约束时,虽然给基于导航属性的应用(例如OData)提供了便利,同时将数据完整性检查后置到了数据库中;但是进行架构调整是一件比较麻烦的工作,对分布式应用也非常不友好。

P.S. TPT的查询性能很差,因此绝大多数场景都不推荐,仅在自己完全清楚并权衡了利弊的情况下再使用TPT。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK