关系可以定义为一对一或一对多。对于多对多关系,在EF Core 2.0中,需要在关系中指定一个中间类,从而将该关系分割为一对多关系和多对一关系。
关系可以使用约定、注释和流利API来指定。下一节将讨论这三种变体。
1. 使用约定的关系
定义关系的第一个方法是使用约定。下面看一个使用Book和Chapter类型的例子。一本书可以有多个章节;因此,这是一对多关系。Book类型还定义了与作者的关联。这里,作者由User类表示。稍后,使用注释定义关系时,会解释这个名称的原因。书与作者定义了一对一的关系。(对于有多个作者的书,书中指定的作者是主要作者。)
书是由Book类定义的。这个类有一个主键BookId,它是根据其名称而创建的。关系由Chapters属性定义。Chapters属性为List<Chapter>类型;这就是Book类型定义一对多关系所需的全部内容。书与作者的关系由类型User的Author属性指定。还可以定义该类型的AuthorId属性来指定外键,该属性与User类中的键相同。如果没有这个定义,就会创建一个阴影属性:
public class Book
{
public int BookId { get; set; }
public string Title { get; set; }
public List<Chapter> Chapters { get; } = new List<Chapter>();
public User Author { get; set; }
}
注意:
Chapter属性的另一种可能实现是定义List<Chapter>类型的读/写属性,而不需要实现创建实例。有了这样的实现,实例将自动从EF Core上下文中创建。
章节由Chapter类定义。使用Book属性定义关系。一对多关系定义了一方的集合(Book定义了Chapter对象的集合),和另一方的简单关系(Chapter定义了一个简单的属性Book)。使用一章的Book属性,可以直接访问相关的图书。对于这种类型,BookId属性指定Book的外键。正如Book类型所述,如果未将BookId指定为类的成员,则通过约定创建阴影属性:
public class Chapter
{
public int ChapterId { get; set; }
public int Number { get; set; }
public string Title { get; set; }
public int BookId { get; set; }
public Book Book { get; set; }
}
User类定义了主键的UserId、Name和关系的AuthoredBooks属性:
public class User
{
public int UserId { get; set; }
public string Name { get; set; }
public List<Book> AuthoredBooks { get; set; }
}
上下文只需要使用DbSet<T>类型的属性为Book、Chapter和User类型指定属性:
public class BooksContext : DbContext
{
private const string ConnectionString =
@"server=(localdb)\MSSQLLocalDb;database=Books;trusted_connection=true";
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlServer(ConnectionString);
}
public DbSet<Book> Books { get; set; }
public DbSet<Chapter> Chapters { get; set; }
public DbSet<User> Users { get; set; }
}
在启动应用程序时,使用按照约定定义的映射创建数据库。下图显示了Books、Chapters和Users表及其关系。Book类没有显示为User类型定义外键属性,而是创建一个AuthorUserId阴影属性来填充:
在介绍定义关系的不同方法(使用注释和流利API)之前,先看看如何使用查询访问相关数据。
2. 显示加载相关数据
如果查询书籍并希望显示相关属性(例如,相关章节和相关作者),则可以使用关系的显示加载。
请看下面的代码片段。查询请求所有具有指定标题的图书,并且只需要一个记录。如果试图在启动查询之后直接访问所得图书实例的Chapters和Author属性,那么这些属性的值为null。关系不是隐式加载的。EF Core使用上下文的Entry方法来支持显示加载,该方法通过传递一个实体来返回EntityEntry对象。EntityEntry类定义了允许显示加载关系的Collection和Reference方法。对于一对多关系,可以使用Collection方法来指定集合,而一对一关系需要Reference方法来指定关系。使用Load方法进行显示加载:
private static void ExplicitLoading()
{
Console.WriteLine(nameof(ExplicitLoading));
using (var context = new BooksContext())
{
var book = context.Books
.Where(b => b.Title.StartsWith("Professional C# 7"))
.FirstOrDefault();
if (book != null)
{
Console.WriteLine($"book result: {book.Title}");
context.Entry(book).Collection(b=>b.Chapters).Load();
foreach (var chapter in book.Chapters)
{
Console.WriteLine($"chapters results: {chapter.Number}, {chapter.Title}");
}
context.Entry(book).Reference(b => b.Author).Load();
Console.WriteLine($"author result: {book.Author.Name}");
}
}
}
将数据库表填充一些数据后,运行示例。
实现Load方法的NavigationEntry类也实现了IsLoaded属性,可以在其中检查关系是否已经加载。在调用Load方法之前,不需要检查加载的关系;在调用Load方法时,如果关系已经加载,就不会再次查询数据库。
当对图书的查询运行应用程序时,下面的SELECT语句将在SQL Server上执行。此查询仅访问Books表:
ExplicitLoading
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.1.1-rtm-30846 initialized 'BooksContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (31ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT TOP(1) [b].[BookId], [b].[AuthorUserId], [b].[Title]
FROM [Books] AS [b]
WHERE [b].[Title] LIKE N'Professional C# 7' + N'%' AND (LEFT([b].[Title], LEN(N'Professional C# 7')) = N'Professional C# 7')
book result: Professional C# 7
Professaional C# 7为查询结果的Title。
使用以下Load方法检索书中的章节时,SELECT语句基于图书ID检索章节:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (25ms) [Parameters=[@__get_Item_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT [e].[ChapterId], [e].[BookId], [e].[Number], [e].[Title]
FROM [Chapters] AS [e]
WHERE [e].[BookId] = @__get_Item_0
chapters results: 5624, Professional C# 7
chapters results: 5625, Professional C# 7
chapters results: 5626, Professional C# 7
使用第三个查询,从Users表中检索用户信息:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[@__get_Item_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT [e].[UserId], [e].[Name]
FROM [Users] AS [e]
WHERE [e].[UserId] = @__get_Item_0
author result: Wrox
EF Core除了显示地加载相关数据,导致向SQL Server发送多个查询之外,还支持即使加载,如下所示。
3. 即使加载相关数据
当执行查询时,可以通过调用Include方法来指定关系,来立即加载相关数据。下面的代码片段包括成功应用Where表达式的书中的章节和作者:
private static void EagerLoading()
{
Console.WriteLine(nameof(EagerLoading));
using (var context = new BooksContext())
{
var book = context.Books
.Include(b => b.Chapters)
.Include(b => b.Author)
.Where(b