首頁 > 軟體

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

2022-03-05 13:00:19

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="LiBai@163.com",
                   PhoneNumber="18754145782",
                   Salary=2345m
                };

                Vendor vendor = new Vendor()
                {
                   Name="杜甫",
                   Email="DuFu@qq.com",
                   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 = "LiBai@163.com",
                    PhoneNumber = "18754145782",
                    Salary = 2345m
                };

                Vendor vendor = new Vendor()
                {
                    Name = "杜甫",
                    Email = "DuFu@qq.com",
                    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 = "LiBai@163.com",
                    PhoneNumber = "18754145782",
                    Salary = 2345m
                };

                Vendor vendor = new Vendor()
                {
                    Name = "杜甫",
                    Email = "DuFu@qq.com",
                    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實體繼承模式的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com