理解 Entity Framework 中的表级层次(TPH)继承的初学者教程






4.89/5 (10投票s)
本文讨论了使用 Entity Framework 的表继承(Table per Hierarchy)关系。
引言
本文讨论了使用 Entity Framework 的表继承(Table per Hierarchy)关系。我们将逐步了解如何实现它。
背景
有时,我们的数据库设计中的表与应用程序所需的实体在逻辑上并不匹配。数据库中的表可能比应用程序逻辑上需要的实体过多或过少。还有些时候,表是为每个逻辑实体创建的,但从应用程序和实体的角度来看,它们之间的关系并不合乎逻辑。
Entity Framework 中的继承提供了一种创建所需的逻辑实体来操作一组数据库表的方法,同时还可以通过继承在实体之间创建更有意义的关系。
Entity Framework 中有三种继承关系:表类型(TPT)、表层次(TPH)
和表具体类型(TPC)
。
之前我们已经讨论过 TPT 继承。现在,在本文中我们将讨论表层次继承关系,即 TPH。
使用代码
TPH 继承实际上为层次结构中的所有类只保留一个数据库表。从数据库的角度来看,这种解决方案不被推荐,因为使用这种解决方案会要求底层表的结构设计不规范,并且会有大量的冗余列(冗余列的数量取决于派生类的数量,我们稍后会看到)。
现在,为了理解这个概念,让我们尝试实现一个类似的(像我们在 TPT 文章中那样)项目,但这次我们不记录Cars
,而是为Cars
设计解决方案。假设我们有一个表来跟踪Cars
数据。该表将记录当前商店中的所有汽车。现在,商店还提供二手车和打折车的选项。为此,他们在同一辆车表中创建了列。为了区分实际的汽车类型,与其他类型相关的列将为null
,即:

在上表中,如果汽车是Preowned
类型,那么YearsOld
列中的数据将具有某个值,而DiscountRate
列将为 null,打折车也是如此。所以我们可以看到,从数据库的角度来看,这不是一个高效且规范化的解决方案。
尽管如此,让我们尝试在我们的解决方案中使用 Entity Framework 对这个表进行建模。当我们为这个表添加实体数据模型时,我们会得到以下默认创建的实体。

现在,我们不希望这种糟糕的表设计传播到我们的应用程序中(否则随着时间的推移,将出现太多的变通方法)。因此,我们将其分解为更好的逻辑设计。我们为DicountedCars
和PreOwnedCars
创建了另外两个实体。然后,我们将在它们之间创建一个继承关系,其中Car
将是这两个实体的基类实体。最后,我们从Car
实体中删除相应的属性,并将它们移动到这些新创建的实体中。(所有这些都可以通过在实体设计器上使用上下文菜单来完成)。生成的实体将看起来像

接下来,我们需要定义这两个实体的表映射和映射条件。我们希望实体将数据推送到表中的相应列,并确保与其他实体相关的列为 null。DiscountedCar
实体的表映射

PreOwnedCar
实体的表映射

我们最后需要做的是将Car
实体标记为abstract
,这样就没人能创建Car
类型而不创建具体类型了。
现在我们有了具有所需逻辑关系的实体,并且可以处理这些实体了。现在让我们看看如何对这些相关实体执行各种 CRUD 操作。
Insert
让我们从插入操作开始。看看我们如何向PreOwnedCar
实体添加一个实体。
string name = txtNameP.Text;
string manf = txtManfP.Text;
int years = Convert.ToInt32(txtYearsP.Text);
using (CarsDbEntities entities = new CarsDbEntities())
{
PreOwnedCar car = new PreOwnedCar { CarName = name, Manufacturer = manf, YearsOld = years };
entities.AddToCars(car);
entities.SaveChanges();
Response.Redirect("Default.aspx");
}
在这里,我们从用户那里获取输入,然后创建一个PreOwnedCar
对象。然后,我们将它添加到Cars
实体集合中(由于我们有继承关系,所有PreOwnedCars
都是Cars
,因此此调用将处理其余部分)。
同样,我们也可以插入到DiscountedCar
表中。
string name = txtNameD.Text;
string manf = txtManfD.Text;
int years = Convert.ToInt32(txtRateD.Text);
using (CarsDbEntities entities = new CarsDbEntities())
{
DiscountedCar car = new DiscountedCar { CarName = name, Manufacturer = manf, DiscountRate = years };
entities.AddToCars(car);
entities.SaveChanges();
Response.Redirect("Default.aspx");
}

Select
要从表中选择数据,我们可以选择表中的所有数据,也可以指定一个选择条件来从表中选择。要选择各自表中的所有数据,我们只需使用Context
类的集合属性。
using (CarsDbEntities entities = new CarsDbEntities())
{
GridView1.DataSource = entities.Cars;
GridView1.DataBind();
GridView2.DataSource = entities.Cars.OfType<PreOwnedCar>();
GridView2.DataBind();
GridView3.DataSource = entities.Cars.OfType<DiscountedCar>();
GridView3.DataBind();
}

另一种情况是,当我们需要使用某些搜索条件选择一种Car
类型时,我们可以从Cars
集合中获取结果,并使用typeof
运算符来检查实体的实际类型。
int id = Convert.ToInt32(TextBox1.Text);
using (CarsDbEntities entities = new CarsDbEntities())
{
Car car = entities.Cars.SingleOrDefault<Car>(c => c.CarID == id);
if (car != null)
{
txtNameP.Text = car.CarName;
txtManfP.Text = car.Manufacturer;
if (car.GetType() == typeof(PreOwnedCar))
{
lblField.Text = "Years Old";
txtFieldData.Text = ((PreOwnedCar)car).YearsOld.ToString();
}
else if (car.GetType() == typeof(DiscountedCar))
{
lblField.Text = "Discount Rate";
txtFieldData.Text = ((DiscountedCar)car).DiscountRate.ToString();
}
}
}
在上面的代码中,我们从用户那里询问汽车的 id,然后相应地选择Car
。上面代码中需要注意的重要一点是,我们通过基类Car
获取详细信息,然后使用typeof
检查其类型以获取派生实体的详细信息。
更新和删除
要更新记录,我们首先需要确定汽车的实际类型,我们可以通过使用typeof
运算符来完成,然后我们可以像处理普通实体一样更新汽车的属性。
int id = Convert.ToInt32(TextBox1.Text);
using (CarsDbEntities entities = new CarsDbEntities())
{
Car car = entities.Cars.SingleOrDefault<Car>(c => c.CarID == id);
if (car != null)
{
car.CarName = txtNameP.Text;
car.Manufacturer = txtManfP.Text;
if (car.GetType() == typeof(PreOwnedCar))
{
((PreOwnedCar)car).YearsOld = Convert.ToInt32(txtFieldData.Text);
}
else if (car.GetType() == typeof(DiscountedCar))
{
lblField.Text = "Discount Rate";
((DiscountedCar)car).DiscountRate = Convert.ToInt32(txtFieldData.Text);
}
entities.SaveChanges();
Response.Redirect("Default.aspx");
}
}
在上面的代码中,我们直接更新与基实体相关的详细信息,并且为了更新与派生实体相关的详细信息,我们在检查typeof
后,将类型转换为适当的类型,然后更改实体的属性。
删除也将遵循相同的原则,它不会更新记录,而是直接从表中删除记录。
int id = Convert.ToInt32(TextBox1.Text);
using (CarsDbEntities entities = new CarsDbEntities())
{
Car car = entities.Cars.SingleOrDefault<Car>(c => c.CarID == id);
if (car != null)
{
entities.DeleteObject(car);
entities.SaveChanges();
Response.Redirect("Default.aspx");
}
}

执行上述所有操作将更改单个Car
表中的数据,尽管从我们的应用程序的角度来看,我们正在处理各种逻辑实体,但实际上它们都使用单个Cars
表进行数据操作,因此是表层次。
注意:请查看示例代码以全面了解代码。本文中的代码片段仅显示与讨论主题相关的代码。
值得关注的点:
在这篇短小的文章中,我们了解了如何创建实体之间的逻辑关系以更好地使用它们。我们在本文中专门讨论了表层次继承关系。本文是从初学者的角度撰写的。我希望这篇文章能有所启发。
历史
- 2013 年 2 月 14 日:初稿。