Entity Framework使用Code First的實體繼承模式

Entity Framework的Code First模式有三種實體繼承模式

1、Table per Type (TPT)繼承

2、Table per Class Hierarchy(TPH)繼承

3、Table per Concrete Class (TPC)繼承

一、TPT繼承模式

當領域實體類有繼承關系時,TPT繼承很有用,我們想把這些實體類模型持久化到數據庫中,這樣,每個領域實體都會映射到單獨的一張表中。這些表會使用一對一關系相互關聯,數據庫會通過一個共享的主鍵維護這個關系。

假設有這麼一個場景:一個組織維護瞭一個部門工作的所有人的數據庫,這些人有些是拿著固定工資的員工,有些是按小時付費的臨時工,要持久化這個場景,我們要創建三個領域實體:Person、Employee和Vendor類。Person類是基類,另外兩個類會繼承自Person類。實體類結構如下:

1、Person類

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TPTPattern.Model
{
    public class Person
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public string Email { get; set; }

        public string PhoneNumber { get; set; }
    }
}

Employee類結構

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TPTPattern.Model
{
    [Table("Employee")]
    public class Employee :Person
    {
        /// <summary>
        /// 薪水
        /// </summary>
        public decimal Salary { get; set; }
    }
}

Vendor類結構

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TPTPattern.Model
{
    [Table("Vendor")]
    public class Vendor :Person
    {
        /// <summary>
        /// 每小時的薪水
        /// </summary>
        public decimal HourlyRate { get; set; }
    }
}

在VS中的類圖如下:

對於Person類,我們使用EF的默認約定來映射到數據庫,而對Employee和Vendor類,我們使用瞭數據註解,將它們映射為我們想要的表名。

然後我們需要創建自己的數據庫上下文類,數據庫上下文類定義如下:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TPTPattern.Model;

namespace TPTPattern.EFDatabaseContext
{
    public class EFDbContext :DbContext
    {
        public EFDbContext()
            : base("name=Default")
        { }

        public DbSet<Person> Persons { get; set; }
    }
}

在上面的上下文中,我們隻添加瞭實體類Person的DbSet,沒有添加另外兩個實體類的DbSet。因為其它的兩個領域模型都是從這個模型派生的,所以我們也就相當於將其它兩個類添加到瞭DbSet集合中瞭,這樣EF會使用多態性來使用實際的領域模型。當然,也可以使用Fluent API和實體夥伴類來配置映射細節信息。

2、使用數據遷移創建數據庫

使用數據遷移創建數據庫後查看數據庫表結構:

在TPT繼承中,我們想為每個領域實體類創建單獨的一張表,這些表共享一個主鍵。因此生成的數據庫關系圖表如下:

3、填充數據

現在我們使用這些領域實體來創建一個Employee和Vendor類來填充數據,Program類定義如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TPTPattern.EFDatabaseContext;
using TPTPattern.Model;

namespace TPTPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new EFDbContext())
            {
                Employee emp = new Employee()
                {
                  Name="李白",
                  Email="[email protected]",
                   PhoneNumber="18754145782",
                   Salary=2345m
                };

                Vendor vendor = new Vendor()
                {
                   Name="杜甫",
                   Email="[email protected]",
                   PhoneNumber="18234568123",
                   HourlyRate=456m
                };

                context.Persons.Add(emp);
                context.Persons.Add(vendor);
                context.SaveChanges();
            }

            Console.WriteLine("信息錄入成功");
        }
    }
}

查詢數據庫填充後的數據:

我們可以看到每個表都包含單獨的數據,這些表之間都有一個共享的主鍵。因而這些表之間都是一對一的關系。

註:TPT模式主要應用在一對一模式下。

二、TPH模式

當領域實體有繼承關系時,但是我們想將來自所有的實體類的數據保存到單獨的一張表中時,TPH繼承很有用。從領域實體的角度,我們的模型類的繼承關系仍然像上面的截圖一樣:

但是從數據庫的角度講,應該隻有一張表保存數據。因此,最終生成的數據庫的樣子應該是下面這樣的:

註意:從數據庫的角度看,這種模式很不優雅,因為我們將無關的數據保存到瞭單張表中,我們的表是不標準的。如果我們使用這種方法,那麼總會存在null值的冗餘列。

1、創建有繼承關系的實體類

現在我們創建實體類來實現該繼承,註意:這次創建的三個實體類和之前創建的隻是沒有瞭類上面的數據註解,這樣它們就會映射到數據庫的單張表中(EF會默認使用父類的DbSet屬性名或復數形式作為表名,並且將派生類的屬性映射到那張表中),類結構如下:

2、創建數據上下文

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TPHPattern.Model;

namespace TPHPattern.EFDatabaseContext
{
    public class EFDbContext :DbContext
    {
        public EFDbContext()
            : base("name=Default")
        { }

        public DbSet<Person> Persons { get; set; }

        public DbSet<Employee> Employees { get; set; }

        public DbSet<Vendor> Vendors { get; set; }
    }
}

3、使用數據遷移創建數據庫

使用數據遷移生成數據庫以後,會發現數據庫中隻有一張表,而且三個實體類中的字段都在這張表中瞭, 創建後的數據庫表結構如下:

註意:查看生成的表結構,會發現生成的表中多瞭一個Discriminator字段,它是用來找到記錄的實際類型,即從Person表中找到Employee或者Vendor。

4、不使用默認生成的區別多張表的類型

使用Fluent API,修改數據上下文類,修改後的類定義如下:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TPHPattern.Model;

namespace TPHPattern.EFDatabaseContext
{
    public class EFDbContext :DbContext
    {
        public EFDbContext()
            : base("name=Default")
        { }

        public DbSet<Person> Persons { get; set; }

        public DbSet<Employee> Employees { get; set; }

        public DbSet<Vendor> Vendors { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // 強制指定PersonType是鑒別器 1代表全職職員 2代表臨時工
            modelBuilder.Entity<Person>()
                .Map<Employee>(m => m.Requires("PersonType").HasValue(1))
                .Map<Vendor>(m => m.Requires("PersonType").HasValue(2));
            base.OnModelCreating(modelBuilder);
        }
    }
}

重新使用數據遷移把實體持久化到數據庫,持久化以後的數據庫表結構:

生成的PersonType列的類型是int。

5、填充數據

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TPHPattern.EFDatabaseContext;
using TPHPattern.Model;

namespace TPHPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new EFDbContext())
            {
                Employee emp = new Employee()
                {
                    Name = "李白",
                    Email = "[email protected]",
                    PhoneNumber = "18754145782",
                    Salary = 2345m
                };

                Vendor vendor = new Vendor()
                {
                    Name = "杜甫",
                    Email = "[email protected]",
                    PhoneNumber = "18234568123",
                    HourlyRate = 456m
                };

                context.Persons.Add(emp);
                context.Persons.Add(vendor);
                context.SaveChanges();
            }

            Console.WriteLine("信息錄入成功");
        }
    }
}

6、查詢數據

註意:TPH模式和TPT模式相比,TPH模式隻是少瞭使用數據註解或者Fluent API配置子類的表名。因此,如果我們沒有在具有繼承關系的實體之間提供確切的配置,那麼EF會默認將其對待成TPH模式,並把數據放到單張表中。

三、TPC模式

當多個領域實體類派生自一個基類實體,並且我們想將所有具體類的數據分別保存在各自的表中,以及抽象基類實體在數據庫中沒有對應的表時,使用TPC繼承模式。實體模型還是和之前的一樣。

然而,從數據庫的角度看,隻有所有具體類所對應的表,而沒有抽象類對應的表。生成的數據庫如下圖:

1、創建實體類

創建領域實體類,這裡Person基類應該是抽象的,其他的地方都和上面一樣:

2、配置數據上下文

接下來就是應該配置數據庫上下文瞭,如果我們隻在數據庫上下文中添加瞭Person的DbSet泛型屬性集合,那麼EF會當作TPH繼承處理,如果我們需要實現TPC繼承,那麼還需要使用Fluent API來配置映射(當然也可以使用配置夥伴類),數據庫上下文類定義如下:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TPCPattern.Model;

namespace TPCPattern.EFDatabaseContext
{
    public class EFDbContext :DbContext
    {
        public EFDbContext()
            : base("name=Default")
        { }

        public virtual DbSet<Person> Persons { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            //MapInheritedProperties表示繼承以上所有的屬性
            modelBuilder.Entity<Employee>().Map(m =>
            {
                m.MapInheritedProperties();
                m.ToTable("Employees");
            });
            modelBuilder.Entity<Vendor>().Map(m =>
            {
                m.MapInheritedProperties();
                m.ToTable("Vendors");
            });
            base.OnModelCreating(modelBuilder);
        }
    }
}

上面的代碼中,MapInheritedProperties方法將繼承的屬性映射到表中,然後我們根據不同的對象類型映射到不同的表中。

3、使用數據遷移生成數據庫

生成的數據庫表結構如下:

查看生成的表結構會發現,生成的數據庫中隻有具體類對應的表,而沒有抽象基類對應的表。具體實體類對應的表中有所有抽象基類裡面的字段。

4、填充數據

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TPCPattern.EFDatabaseContext;
using TPCPattern.Model;

namespace TPCPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new EFDbContext())
            {
                Employee emp = new Employee()
                {
                    Name = "李白",
                    Email = "[email protected]",
                    PhoneNumber = "18754145782",
                    Salary = 2345m
                };

                Vendor vendor = new Vendor()
                {
                    Name = "杜甫",
                    Email = "[email protected]",
                    PhoneNumber = "18234568123",
                    HourlyRate = 456m
                };

                context.Persons.Add(emp);
                context.Persons.Add(vendor);
                context.SaveChanges();
            }

            Console.WriteLine("信息錄入成功");
        }
    }
}

查詢數據庫:

註意:雖然數據是插入到數據庫瞭,但是運行程序時也出現瞭異常,異常信息見下圖。出現該異常的原因是EF嘗試去訪問抽象類中的值,它會找到兩個具有相同Id的記錄,然而Id列被識別為主鍵,因而具有相同主鍵的兩條記錄就會產生問題。這個異常清楚地表明瞭存儲或者數據庫生成的Id列對TPC繼承無效。

如果我們想使用TPC繼承,那麼要麼使用基於GUID的Id,要麼從應用程序中傳入Id,或者使用能夠維護對多張表自動生成的列的唯一性的某些數據庫機制。

到此這篇關於Entity Framework使用Code First實體繼承模式的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: