EF Core从TPH迁移到TPT

Posted 漫思

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了EF Core从TPH迁移到TPT相关的知识,希望对你有一定的参考价值。

EF Core从TPH迁移到TPT

 

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。

作者:波多尔斯基

出处:https://www.cnblogs.com/podolski/p/17284449.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

欢迎转载,转载请保留原文链接~喜欢的观众老爷们可以点下推荐或者右下角关注不迷路~

EF Core 5 TPT 和身份 (CRUD)

【中文标题】EF Core 5 TPT 和身份 (CRUD)【英文标题】:EF Core 5 TPT and Identity (CRUD) 【发布时间】:2021-03-25 14:13:22 【问题描述】:

我想使用 EF Core 的 TPT 功能,我想将它与 Identity 集成,但我在添加到数据库时遇到了一些问题。

 public class ApplicationUser : IdentityUser<Guid>
    
        public string FirstName  get; set; 
        public string LastName  get; set; 
        public string NationalCode  get; set; 
        public DateTime BirthDate  get; set; 
        public Gender Gender  get; set; 
    
 public class Student : ApplicationUser
    
        public string FatherName  get; set; 
        public string PlaceOfBirth  get; set; 
        public string Address  get; set; 
        public string HomePhoneNumber  get; set; 
    
 public class Teacher : ApplicationUser
    
        public string FieldOfStudy  get; set; 
        public AcademicDegree AcademicDegree get; set; 
        public int YearsOfExperience  get; set; 
    

这是我的数据库上下文类

 public class SchoolMgmtContext : IdentityDbContext<ApplicationUser,ApplicationRole,Guid>
    


        public SchoolMgmtContext(DbContextOptions<SchoolMgmtContext> dbContextOptions)
            :base(dbContextOptions)
        

        

        public DbSet<Teacher> Teachers  get; set; 
        public DbSet<Student> Students  get; set; 

        protected override void OnModelCreating(ModelBuilder builder)
        
            base.OnModelCreating(builder);

            builder.Entity<ApplicationUser>().ToTable("Users");
            builder.Entity<Student>().ToTable("Students");
            builder.Entity<Teacher>().ToTable("Teachers");

            builder.Entity<ApplicationRole>().ToTable("Roles");
        
    

一切正常。

但我不知道如何插入新教师或新学生。

例如,这是添加新教师的代码。

    public IActionResult CreateTeacher()
        

            ViewBag.Users = new SelectList(_db.Users.Select(u => new  u.Id, FullName = u.FirstName + " " + u.LastName ), "Id", "FullName");
            return View();
        

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> CreateTeacher(TeacherCreationDto teacherCreationDto)
        
            if (ModelState.IsValid)
            
                var newTeacher = new Teacher()
                
                    AcademicDegree = teacherCreationDto.AcademicDegree,
                    FieldOfStudy = teacherCreationDto.FieldOfStudy,
                    YearsOfExperience = teacherCreationDto.YearsOfExperience,
                    Id= teacherCreationDto.UserId
                ;

                 // _db.Teachers.Update(newTeacher); // here I tested Attach - Add - Update, but none of this work.
                await _db.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            
            else
            
                ViewBag.Users = new SelectList(_db.Users.Select(u => new  u.Id, FullName = u.FirstName + " " + u.LastName ), "Id", "FullName");
                return View(teacherCreationDto);
            
         
        

我应该如何添加新的学生或教师?谢谢,

更新:

【问题讨论】:

您添加新 TPT 记录的方式与添加新 TPH 或常规记录的方式相同 - 通过使用某些 Add 方法,例如对于新的Teacher,其中任何一个都应该这样做-_db.Add(newTeacher)_db.Users.Add(newTeacher)_db.Teachers.Add(newTeacher)。你是什​​么意思“没有这个工作”?您是否遇到异常或? @IvanStoev 感谢您的回答,但这些方法都不起作用,是的,我有 2 个例外,第一个是关于 DbConcurreny,第二个是关于无法插入重复的主键。 这是因为这里 Id= teacherCreationDto.UserId 您正在将现有 ID 分配给新用户(教师)。使用 Id = Guid.NewGuid() 或者不分配它,让 EFC 自动生成它。 @IvanStoev 为什么在 Microsoft docs 或 *** 中没有关于带有 TPT 的 EF Core 5 中的 CRUD 的代码? 嗯,看来你误解了继承的概念。教师用户,学生也是如此。您不会创建单独的用户并将其“连接”到教师或学生 - 您创建的新教师也是新用户。 TPT 和 TPH 只是在数据库中存储此类继承数据的不同方式。如果您想将对象“连接”到 User,则不应使用继承,而应使用 FK 关系。 【参考方案1】:

我同意“Ivan Stoev”的观点:您不能从 User 继承 Student 和 Teacher。 如果需要,那么您的“TeacherCreationDto”应包含以下字段/有效负载:

“学习领域、经验年限、名字、姓氏、国家代码、出生日期、性别、ID、用户名、规范化用户名、电子邮件、规范化电子邮件、电子邮件确认、密码哈希、SecurityStamp、ConcurrencyStamp、电话号码、电话号码确认、TwoFactorEnabled、LockoutEnd、LockoutEnabled、AccessFailedCount” .. . 当我通过使用自动脚手架创建控制器/api时,以下编码对我有用:

    public async Task<IActionResult> Create([Bind("FieldOfStudy,YearsOfExperience,FirstName,LastName,NationalCode,BirthDate,Gender,Id,UserName,NormalizedUserName,Email,NormalizedEmail,EmailConfirmed,PasswordHash,SecurityStamp,ConcurrencyStamp,PhoneNumber,PhoneNumberConfirmed,TwoFactorEnabled,LockoutEnd,LockoutEnabled,AccessFailedCount")] Teacher teacher)
    
        if (ModelState.IsValid)
        
            teacher.Id = Guid.NewGuid();
            _context.Add(teacher);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        
        return View(teacher);
    

【讨论】:

谢谢,这是我在 Ivan github.com/ArminShoeibi/SchoolManagementSystem 的帮助下创建的示例项目 好吧,除非您的用例暗示TeacherStudent 是您系统的Users。但我也同意,通常您将用户与您的应用程序逻辑联系起来,因此在大多数情况下,外键更有可能【参考方案2】:

正如问题作者所说,Microsoft official docs 中没有足够的文档。

与我们对 TPT 的期望相反,当我们想要向派生表中插入新记录时,我们必须提供派生类型(包括父类型)的所有属性,因为 EF Core 会在内部创建两个插入语句,一个用于父表,另一个用于派生表。

因此,如果我们提供现有 id 来创建新记录,则会抛出带有“重复键”消息的 DbUpdateException

我可以说,当我们的继承重叠时,这种方法是不合适的(考虑一个用户既是学生又是教师的情况,甚至想从用户更改为学生)。

最后,我建议您手动创建 TPT 关系。你的模型应该是这样的:

 public class ApplicationUser : IdentityUser<Guid>
    
        public string FirstName  get; set; 
        public string LastName  get; set; 
        public string NationalCode  get; set; 
        public DateTime BirthDate  get; set; 
        public Gender Gender  get; set; 
    
    public class Student 
    
        [ForignKey(nameof(User))]
        public Guid Id get; set;  // Id and forignKey 
        
        /* Student specific fields */

        public ApplicationUser User get; set; 
    
    public class Teacher
    
        [ForignKey(nameof(User))]
        public Guid Id get; set;  // Id and forignKey 

        /* Teacher specific fields */

        public ApplicationUser User get; set; 
    

【讨论】:

以上是关于EF Core从TPH迁移到TPT的主要内容,如果未能解决你的问题,请参考以下文章

Inheritance with EF Code First: Part 2 – Table per Type (TPT)

EF Core 5 TPT - 具有不同 ID 名称的继承对象

嵌套 tph 继承成员的 ef-core 负载收集属性

ef core 中的 TPH 模式用于不同实体之间的共享表

从 EF Core 5 迁移到 EF Core 6 时出错

从 EF6 迁移到 EF Core 2.0