使用 Code First 方法和 Fluent API 在 Entity Framework 中建立关系






4.87/5 (80投票s)
在本文中,您将学习如何在 Entity Framework 中使用 Code First 方法和 Fluent API 处理关系。
引言
在数据库的上下文中,关系是指两个关系数据库表之间存在的一种情况,其中一个表有一个引用另一个表主键的外键。关系允许关系数据库将数据拆分并存储在不同的表中,同时链接不同的数据项。例如,如果我们想存储关于 `Customer` 及其 `Order` 的信息,那么我们需要创建两个表,一个用于 `Customer`,另一个用于 `Order`。 `Customer` 和 `Order` 这两个表将具有一对多关系,因此每当我们检索一个 `customer` 的所有订单时,我们都可以轻松地检索它们。
数据库关系有几种类型。在本文中,我将介绍以下内容
- 一对一关系
- 一对多或多对一关系
- 多对多关系
Entity Framework Code First 允许我们使用自己的域类来表示 Entity Framework 依赖的模型,以执行查询、更改跟踪和更新功能。Code First 方法遵循约定优于配置的原则,但它也提供了两种方式为我们的类添加配置。一种是使用简单的属性,称为 `DataAnnotations`,另一种是使用 Code First 的 Fluent API,它提供了一种通过代码以命令式的方式描述配置的方法。本文将重点介绍 Fluent API 中关系的处理。
为了理解 Entity Framework Code First 方法中的关系,我们创建一个实体并使用 Fluent API 定义它们的配置。我们将创建两个类库项目,一个库项目 (`EF.Core`) 包含实体,另一个项目 (`EF.Data`) 包含这些实体的配置以及 `DbContext`。我们还将创建一个单元测试项目 (`EF.UnitTest`) 来测试我们的代码。我们将使用下面的类图中的类来解释前面的三种关系。
如前面的类图所示,`BaseEntity` 类是每个其他类继承的基类。每个派生实体代表一个数据库表。我们将使用左侧的两个派生实体组合来解释每种关系类型,因此我们创建了六个实体。
所以,首先,我们在 `EF.Core` 类库项目下创建 `BaseEntity` 类,该类被每个派生实体继承。
using System;
namespace EF.Core
{
public abstract class BaseEntity
{
public Int64 ID { get; set; }
public DateTime AddedDate { get; set; }
public DateTime ModifiedDate { get; set; }
public string IP { get; set; }
}
}
我们使用导航属性来访问一个相关实体对象。导航属性提供了一种导航两个实体类型之间关联的方法。每个对象都可以拥有其参与的每个关系的一个导航属性。导航属性允许您双向导航和管理关系,返回一个引用对象(如果基数是一个或零到一)或一个集合(如果基数是多)。
现在我们一一来看每种关系。
学习使用 Entity Framework 进行 MVC 的路线图
- 使用 Code First 方法和 Fluent API 在 Entity Framework 中建立关系
- Entity Framework 的 Code First 迁移
- 使用 MVC 中的 Entity Framework 5.0 Code First 方法进行 CRUD 操作
- 在 MVC 中使用存储库模式进行 CRUD 操作
- 在 MVC 中使用通用存储库模式和工作单元进行 CRUD 操作
- 在 MVC 中使用通用存储库模式和依赖注入进行 CRUD 操作
一对一关系
在关系的两边,两个表都只能有一个记录。每个主键值只与相关表中的一个记录(或无记录)相关。请注意,这种类型的关系并不常见,大多数一对一关系是由业务规则强制执行的,而不是自然地从数据中产生的。在没有此类规则的情况下,通常可以将两个表合并为一个表,而不会破坏任何规范化规则。
为了理解一对一关系,我们创建两个实体,一个是 `User`,另一个是 `UserProfile`。一个用户可以有一个配置文件,`User` 表将有一个主键,同一个键将是 `UserProfile` 表的主键和外键。让我们来看图 1.2 了解一对一关系。
现在我们在 `EF.Core` 项目的 *Data* 文件夹下创建 `User` 和 `UserProfile` 这两个实体。我们的 `User` 类代码片段如下:
namespace EF.Core.Data
{
public class User : BaseEntity
{
public string UserName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public UserProfile UserProfile { get; set; }
}
}
`UserProfile` 类代码片段如下:
namespace EF.Core.Data
{
public class UserProfile : BaseEntity
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public virtual User User { get; set; }
}
}
从前面的两个代码片段可以看出,每个实体都使用另一个实体作为导航属性,以便您可以访问彼此相关的对象。
现在,我们定义这两个实体的配置,当实体创建数据库表时将使用这些配置。该配置在 `EF.Data` 项目下的 *Mapping* 文件夹中定义。现在为每个实体创建两个配置类。对于 `User` 实体,我们创建 `UserMap` 实体。
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EF.Core.Data;
namespace EF.Data.Mapping
{
public class UserMap : EntityTypeConfiguration<User>
{
public UserMap()
{
//Key
HasKey(t => t.ID);
//Fields
Property(t => t.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.UserName).IsRequired().HasMaxLength(25);
Property(t => t.Email).IsRequired();
Property(t => t.AddedDate).IsRequired();
Property(t => t.ModifiedDate).IsRequired();
Property(t => t.IP);
//table
ToTable("Users");
}
}
}
我们将以与 `User` 相同的方式为其他实体创建配置。`EntityTypeConfiguration
- `HasKey()`:`Haskey()` 方法配置表的主键。
- `Property()`:`Property` 方法配置属于实体或复杂类型的每个属性的属性。它用于获取给定属性的配置对象。配置对象上的选项特定于正在配置的类型。
- `HasDatabaseGeneratedOption`:它配置数据库如何生成属性的值。
- `DatabaseGeneratedOption.Identity`:`DatabaseGeneratedOption` 是数据库注解。它枚举数据库生成选项。`DatabaseGeneratedOption.Identity` 用于通过唯一值在表中创建自动递增列。
- `ToTable()`:配置此实体类型映射到的表名。
现在创建 `UserProfile` 配置类,即 `UserProfileMap` 类。
using System.Data.Entity.ModelConfiguration;
using EF.Core.Data;
namespace EF.Data.Mapping
{
public class UserProfileMap : EntityTypeConfiguration<UserProfile>
{
public UserProfileMap()
{
//key
HasKey(t => t.ID);
//fields
Property(t => t.FirstName);
Property(t => t.LastName);
Property(t => t.Address).HasMaxLength(100).HasColumnType("nvarchar");
Property(t => t.AddedDate);
Property(t => t.ModifiedDate);
Property(t => t.IP);
//table
ToTable("UserProfiles");
//relationship
HasRequired(t => t.User).WithRequiredDependent(u => u.UserProfile);
}
}
}
在上面的代码片段中,我们定义了 `User` 和 `UserProfiles` 实体之间的“一对一”关系。这种关系是通过 Fluent API 使用 `HasRequired()` 和 `WithRequiredDependent()` 方法定义的,因此这些方法如下:
- `HasRequired()`:配置此实体类型的必需关系。除非指定了此关系,否则实体类型的实例将无法保存到数据库。数据库中的外键将不可为空。换句话说,`UserProfile` 不能独立于 `User` 实体保存。
- `WithRequiredDependent()`:(来自 MSDN)将关系配置为必需:必需,但关系另一侧没有导航属性。要配置的实体类型将是依赖项,并包含指向主键的外键。关系目标指向的实体类型将是关系中的主键。
现在在 `EF.Data` 项目下的 *App.config* 文件中定义连接字符串,以便我们可以创建具有适当名称的数据库。`connectionstring` 是:
<connectionStrings>
<add name="DbConnectionString" connectionString="Data Source=sandeepss-PC;
Initial Catalog=EFCodeFirst;User ID=sa; Password=*******" providerName="System.Data.SqlClient" />
</connectionStrings>
现在我们创建一个继承 `DbContext` 类的上下文类 `EFDbContext`(*EFDbContext.cs*)。在此类中,我们重写 `OnModelCreating()` 方法。当上下文类 (`EFDbContext`) 的模型初始化后,但在模型被锁定并用于初始化上下文之前,会调用此方法,以便在模型被锁定前可以进一步配置模型。以下是上下文类的代码片段。
using System;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration;
using System.Linq;
using System.Reflection;
namespace EF.Data
{
public class EFDbContext : DbContext
{
public EFDbContext()
: base("name=DbConnectionString")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var typesToRegister = Assembly.GetExecutingAssembly().GetTypes()
.Where(type => !String.IsNullOrEmpty(type.Namespace))
.Where(type => type.BaseType != null && type.BaseType.IsGenericType
&& type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));
foreach (var type in typesToRegister)
{
dynamic configurationInstance = Activator.CreateInstance(type);
modelBuilder.Configurations.Add(configurationInstance);
}
base.OnModelCreating(modelBuilder);
}
}
}
正如您所知,EF Code First 方法遵循约定优于配置的原则,因此在构造函数中,我们只需传递连接字符串名称,与 *App.Config* 文件中的名称相同,它就会连接到该服务器。在 `OnModelCreating()` 方法中,我们使用反射将实体映射到此特定项目中的其配置类。
我们创建一个单元测试项目 `EF.UnitTest` 来测试上述代码。我们创建一个名为 `UserTest` 的测试类,其中有一个名为 `UserUserProfileTest()` 的测试方法。此方法创建一个数据库,并根据 `User` 和 `UserProfile` 表的关系填充它们。以下是 `UserTest` 类的代码片段。
using System;
using System.Data.Entity;
using EF.Core.Data;
using EF.Data;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace EF.UnitTest
{
[TestClass]
public class UserTest
{
[TestMethod]
public void UserUserProfileTest()
{
Database.SetInitializer<EFDbContext>(new CreateDatabaseIfNotExists<EFDbContext>());
using (var context = new EFDbContext())
{
context.Database.Create();
User user = new User
{
UserName = "ss_shekhawat",
Password = "123",
Email = "sandeep.shekhawat88@test.com",
AddedDate = DateTime.Now,
ModifiedDate = DateTime.Now,
IP = "1.1.1.1",
UserProfile = new UserProfile
{
FirstName ="Sandeep",
LastName ="Shekhawat",
Address="Jaipur and Jhunjhunu",
AddedDate = DateTime.Now,
ModifiedDate = DateTime.Now,
IP = "1.1.1.1"
},
};
context.Entry(user).State = System.Data.EntityState.Added;
context.SaveChanges();
}
}
}
}
现在,运行 `Test` 方法,您将在数据库中看到具有数据的表。在数据库中运行 `select` 查询并获取结果,例如:
SELECT [ID],[UserName],[Email],[Password],[AddedDate],[ModifiedDate],[IP]FROM [EFCodeFirst].[dbo].[Users]
SELECT [ID],[FirstName],[LastName],[Address],[AddedDate] ,[ModifiedDate],[IP] FROM [EFCodeFirst].[dbo].[UserProfiles]
现在执行前面的查询,然后您将看到以下图中的结果:
一对多关系
主键表只包含一条记录,该记录与相关表中的零个、一个或多个记录相关。这是最常用的一种关系。
为了理解这种关系,我们考虑一个电子商务系统,其中单个用户可以进行许多订单,因此我们定义两个实体,一个用于 `customer`,另一个用于 `order`。让我们看一下下图:
`customer` 实体如下:
using System.Collections.Generic;
namespace EF.Core.Data
{
public class Customer : BaseEntity
{
public string Name { get; set; }
public string Email { get; set; }
public virtual ICollection<Order> Orders { get; set; }
}
}
`Order` 实体代码片段如下:
using System;
namespace EF.Core.Data
{
public class Order : BaseEntity
{
public byte Quanatity { get; set; }
public Decimal Price { get; set; }
public Int64 CustomerId { get; set; }
public virtual Customer Customer { get; set; }
}
}
您已注意到上面的代码中的导航属性。`Customer` 实体具有 `Order` 实体类型的集合,而 `Order` 实体具有 `Customer` 实体类型的属性,这意味着一个 `customer` 可以进行多个 `order`。
现在在 `EF.Data` 项目中创建一个类 `CustomerMap`,以实现 `Customer` 类的 Fluent API 配置。
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EF.Core.Data;
namespace EF.Data.Mapping
{
public class CustomerMap : EntityTypeConfiguration<Customer>
{
public CustomerMap()
{
//key
HasKey(t => t.ID);
//properties
Property(t => t.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.Name);
Property(t => t.Email).IsRequired();
Property(t => t.AddedDate).IsRequired();
Property(t => t.ModifiedDate).IsRequired();
Property(t => t.IP);
//table
ToTable("Customers");
}
}
}
现在为 `Order` 实体配置创建一个另一个映射类。
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EF.Core.Data;
namespace EF.Data.Mapping
{
public class OrderMap : EntityTypeConfiguration<Order>
{
public OrderMap()
{
//key
HasKey(t => t.ID);
//fields
Property(t => t.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.Quanatity).IsRequired().HasColumnType("tinyint");
Property(t => t.Price).IsRequired();
Property(t => t.CustomerId).IsRequired();
Property(t => t.AddedDate).IsRequired();
Property(t => t.ModifiedDate).IsRequired();
Property(t => t.IP);
//table
ToTable("Orders");
//relationship
HasRequired(t => t.Customer).WithMany(c => c.Orders).HasForeignKey_
(t => t.CustomerId).WillCascadeOnDelete(false);
}
}
}
上面的代码显示,每个订单都要求有一个 `Customer`,并且 `Customer` 可以进行多个订单,它们之间的关系由外键 `CustomerId` 建立。在这里,我们使用四种方法来定义两个实体之间的关系。`WithMany` 方法允许我们指示 `Customer` 中的哪个属性包含多重关系。我们还添加了 `HasForeignKey` 方法来指示 `Order` 中的哪个属性是回指 `customer` 的外键。`WillCascadeOnDelete()` 方法配置是否为该关系启用级联删除。
现在,我们在 `EF.UnitTest` 项目中创建另一个单元测试类来测试上面的代码。让我们看一个插入具有两个订单的客户数据的测试方法。
using System;
using System.Collections.Generic;
using System.Data.Entity;
using EF.Core.Data;
using EF.Data;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace EF.UnitTest
{
[TestClass]
public class CustomerTest
{
[TestMethod]
public void CustomerOrderTest()
{
Database.SetInitializer<EFDbContext>(new CreateDatabaseIfNotExists<EFDbContext>());
using (var context = new EFDbContext())
{
context.Database.Create();
Customer customer = new Customer
{
Name = "Raviendra",
Email = "raviendra@test.com",
AddedDate = DateTime.Now,
ModifiedDate = DateTime.Now,
IP = "1.1.1.1",
Orders = new List<Order>{
new Order
{
Quanatity =12,
Price =15,
AddedDate = DateTime.Now,
ModifiedDate = DateTime.Now,
IP = "1.1.1.1",
},
new Order
{
Quanatity =10,
Price =25,
AddedDate = DateTime.Now,
ModifiedDate = DateTime.Now,
IP = "1.1.1.1",
}
}
};
context.Entry(customer).State = System.Data.EntityState.Added;
context.SaveChanges();
}
}
}
}
现在运行 `Test` 方法,您将在数据库中看到具有数据的表。在数据库中运行 `select` 查询并获取结果,例如:
SELECT [ID],[Name],[Email],[AddedDate],[ModifiedDate],[IP]FROM [EFCodeFirst].[dbo].[Customers]
SELECT [ID],[Quanatity],[Price],[CustomerId],[AddedDate],[ModifiedDate],[IP]FROM [EFCodeFirst].[dbo].[Orders]
多对多关系
两个表中的每个记录都可以与另一个表中的任意数量的记录(或无记录)相关。多对多关系需要第三个表,称为关联表或链接表,因为关系系统无法直接容纳这种关系。
为了理解这种关系,我们考虑一个在线课程系统,其中一个 `student` 可以加入多个 `courses`,而一个 `course` 可以有多个 `students`。因此,我们定义了两个实体,一个用于 `student`,另一个用于 `course`。让我们看一下下面的多对多关系图:
`Student` 实体如下面的代码片段所示,它定义在 `EF.Core` 项目下。
using System.Collections.Generic;
namespace EF.Core.Data
{
public class Student : BaseEntity
{
public string Name { get; set; }
public byte Age { get; set; }
public bool IsCurrent { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
}
`Course` 实体如下面的代码片段所示,它定义在 `EF.Core` 项目下。
using System;
using System.Collections.Generic;
namespace EF.Core.Data
{
public class Course : BaseEntity
{
public string Name { get; set; }
public Int64 MaximumStrength { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
}
上面两个代码片段都具有集合形式的导航属性,换句话说,一个实体拥有另一个实体集合。
现在在 `EF.Data` 项目中创建一个名为 `StudentMap` 的类,以实现 `Student` 类的 Fluent API 配置。
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EF.Core.Data;
namespace EF.Data.Mapping
{
public class StudentMap : EntityTypeConfiguration<Student>
{
public StudentMap()
{
//key
HasKey(t => t.ID);
//property
Property(t => t.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.Name);
Property(t => t.Age);
Property(t => t.IsCurrent);
Property(t => t.AddedDate).IsRequired();
Property(t => t.ModifiedDate).IsRequired();
Property(t => t.IP);
//table
ToTable("Students");
//relationship
HasMany(t => t.Courses).WithMany(c => c.Students)
.Map(t => t.ToTable("StudentCourse")
.MapLeftKey("StudentId")
.MapRightKey("CourseId"));
}
}
}
上面的代码片段显示,一个 `student` 可以加入多个 `courses`,而每个 `course` 可以有多个 `students`。众所周知,要实现多对多关系,我们需要一个名为 `StudentCourse` 的第三个表。`MapLeftKey()` 和 `MapRightKey()` 方法定义了第三个表中的键名,否则键名会自动创建为 `classname_Id`。左键或第一个键是我们正在定义关系所在的键。
现在在 `EF.Data` 项目中创建一个名为 `CourseMap` 的类,以实现 `Course` 类的 Fluent API 配置。
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EF.Core.Data;
namespace EF.Data.Mapping
{
public class CourseMap :EntityTypeConfiguration<Course>
{
public CourseMap()
{
//property
Property(t => t.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.Name);
Property(t => t.MaximumStrength);
Property(t => t.AddedDate).IsRequired();
Property(t => t.ModifiedDate).IsRequired();
Property(t => t.IP);
//table
ToTable("Courses");
}
}
}
现在,我们在 `EF.UnitTest` 项目中创建另一个单元测试类来测试上面的代码。让我们看一下向所有三个表中插入数据的测试方法。
using System;
using System.Collections.Generic;
using System.Data.Entity;
using EF.Core.Data;
using EF.Data;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace EF.UnitTest
{
[TestClass]
public class StudentTest
{
[TestMethod]
public void StudentCourseTest()
{
Database.SetInitializer<EFDbContext>(new CreateDatabaseIfNotExists<EFDbContext>());
using (var context = new EFDbContext())
{
context.Database.Create();
Student student = new Student
{
Name = "Sandeep",
Age = 25,
IsCurrent = true,
AddedDate = DateTime.Now,
ModifiedDate = DateTime.Now,
IP = "1.1.1.1",
Courses = new List<Course>{
new Course
{
Name = "Asp.Net",
MaximumStrength = 12,
AddedDate = DateTime.Now,
ModifiedDate = DateTime.Now,
IP = "1.1.1.1"
},
new Course
{
Name = "SignalR",
MaximumStrength = 12,
AddedDate = DateTime.Now,
ModifiedDate = DateTime.Now,
IP = "1.1.1.1"
}
}
};
Course course = new Course
{
Name = "Web API",
MaximumStrength = 12,
AddedDate = DateTime.Now,
ModifiedDate = DateTime.Now,
IP = "1.1.1.1",
Students = new List<Student>{
new Student
{
Name = "Raviendra",
Age = 25,
IsCurrent = true,
AddedDate = DateTime.Now,
ModifiedDate = DateTime.Now,
IP = "1.1.1.1",
},
new Student
{
Name = "Pradeep",
Age = 25,
IsCurrent = true,
AddedDate = DateTime.Now,
ModifiedDate = DateTime.Now,
IP = "1.1.1.1",
}
}
};
context.Entry(student).State = System.Data.EntityState.Added;
context.Entry(course).State = System.Data.EntityState.Added;
context.SaveChanges();
}
}
}
}
现在运行 `Test` 方法,您将在数据库中看到具有数据的表。在数据库中运行 `select` 查询并获取结果,例如:
SELECT [ID],[Name],[Age],[IsCurrent],[AddedDate],[ModifiedDate],[IP] FROM [EFCodeFirst].[dbo].[Students]
SELECT [ID],[Name],[MaximumStrength],[AddedDate],[ModifiedDate],[IP] FROM [EFCodeFirst].[dbo].[Courses]
SELECT [StudentId],[CourseId] FROM [EFCodeFirst].[dbo].[StudentCourse]
结论
本文介绍了在 Entity Framework Code First 方法中使用 Fluent API 处理关系。我没有在这里使用数据库迁移,因此在运行任何单元测试方法之前,需要删除您的数据库。如果您有任何疑问,请发表评论或直接通过 https://twitter.com/ss_shekhawat 联系。