6. 关系
关系可以定义为一对一或一对多。对于多对多关系,在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 => b.Title.StartsWith("Professional C# 7"))
.FirstOrDefault();
if (book != null)
{
Console.WriteLine(book.Title);
Console.WriteLine(book.Author.Name);
foreach (var chapter in book.Chapters)
{
Console.WriteLine($"{chapter.Number}, {chapter.Title}");
}
}
}
}
使用Include,只需要执行一行SQL语句来访问Books表,并连接Chapters和Users表:
SELECT [b.Chapters].[ChapterId], [b.Chapters].[BookId], [b.Chapters].[Number], [b.Chapters].[Title]
FROM [Chapters] AS [b.Chapters]
INNER JOIN (
SELECT DISTINCT [t].*
FROM (
SELECT TOP(1) [b0].[BookId]
FROM [Books] AS [b0]
LEFT JOIN [Users] AS [b.Author0] ON [b0].[AuthorUserId] = [b.Author0].[UserId]
WHERE [b0].[Title] LIKE N'Professional C# 7' + N'%' AND (LEFT([b0].[Title], LEN(N'Professional C# 7')) = N'Professional C# 7')
ORDER BY [b0].[BookId]
) AS [t]
) AS [t0] ON [b.Chapters].[BookId] = [t0].[BookId]
ORDER BY [t0].[BookId]
运行结果,共执行了两条SQL语句:
EagerLoading
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 (29ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT TOP(1) [b].[BookId], [b].[AuthorUserId], [b].[Title], [b.Author].[UserId], [b.Author].[Name]
FROM [Books] AS [b]
LEFT JOIN [Users] AS [b.Author] ON [b].[AuthorUserId] = [b.Author].[UserId]
WHERE [b].[Title] LIKE N'Professional C# 7' + N'%' AND (LEFT([b].[Title], LEN(N'Professional C# 7')) = N'Professional C# 7')
ORDER BY [b].[BookId]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [b.Chapters].[ChapterId], [b.Chapters].[BookId], [b.Chapters].[Number], [b.Chapters].[Title]
FROM [Chapters] AS [b.Chapters]
INNER JOIN (
SELECT DISTINCT [t].*
FROM (
SELECT TOP(1) [b0].[BookId]
FROM [Books] AS [b0]
LEFT JOIN [Users] AS [b.Author0] ON [b0].[AuthorUserId] = [b.Author0].[UserId]
WHERE [b0].[Title] LIKE N'Professional C# 7' + N'%' AND (LEFT([b0].[Title], LEN(N'Professional C# 7')) = N'Professional C# 7')
ORDER BY [b0].[BookId]
) AS [t]
) AS [t0] ON [b.Chapters].[BookId] = [t0].[BookId]
ORDER BY [t0].[BookId]
Professional C# 7
Wrox
5624, Professional C# 7
5625, Professional C# 7
5626, Professional C# 7
如果需要包含多个层次的关系,那么方法ThenInclude可以用于Include方法的结果。(并未提供演示示例)
4. 使用注释的关系
与使用约定不同,实体类型可以通过进行注释应用关系信息。下面向关系属性添加ForeignKey属性,来修改先前创建的Book类型,并指定表示外键的属性。在这里,Book不仅与该书的作者有关联,也与审稿人和项目编辑有关联。这些关系映射到User类型。外键属性定义为 int? 类型,让它们变成可选项。使用强制关系,EF Core创建级联删除;删除Book时,相关的作者、编辑和审稿人也被删除:
public class Book
{
public int BookId { get; set; }
public string Title { get; set; }
public List<Chapter> Chapters { get; } = new List<Chapter>();
public int? AuthorId { get; set; }
[ForeignKey(nameof(AuthorId))]
public User Author { get; set; }
public int? ReviewerId { get; set; }
[ForeignKey(nameof(ReviewerId))]
public User Reviewer { get; set; }
public int? ProjectEditorId { get; set; }
[ForeignKey(nameof(ProjectEditorId))]
public User ProjectEditor { get; set; }
}
User类现在与Book类型有多个关联。WrittenBooks属性列出了添加为Author属性的User的所有图书。类似地,ReviewedBooks和EditedBooks属性与Book类型仅在Reviewer和ProjectEditor属性上相关联。如果相同类型之间存在多个关系,则需要使用InverseProperty特性对属性进行注释。使用这个特性,指定关系另一端(这里指Book类)的相关属性:
public class User
{
public int UserId { get; set; }
public string Name { get; set; }
[InverseProperty("Author")]
public List<Book> WrittenBooks { get; set; }
[InverseProperty("Reviewer")]
public List<Book> ReviewedBooks { get; set; }
[InverseProperty("ProjectEditor")]
public List<Book> EditedBooks { get; set; }
}
下图显示了在SQL Server中生成的表的关系。Books表与Chapters表关联,如前面的示例所示。现在,Users表与Books表有三个关联。
5. 使用流利API的关系
指定关系的最强大的方法是使用流利API。在流利API中,使用HasOne和WithOne方法定义一对一关系,用HasOne和WithMany方法定义一对多关系,而多对一关系由HasMany和WithOne定义。
对于下面的代码示例,模型类型不包括数据库模式上的任何注释。Book类型是一个简单的POCO类型,它定义了图书信息的属性,包括关系属性:
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; }
public User Reviewer { get; set; }
public User Editor { get; set; }
}
User类型的定义也是类似的。除了具有Name属性外,User类型还定义了与Book类型的三种不同的关系:
public class User
{
public int UserId { get; set; }
public string Name { get; set; }
public List<Book> WrittenBooks { get; set; }
public List<Book> ReviewedBooks { get; set; }
public List<Book> EditedBooks { get; set; }
}
Chapter类与Book类有关系。然而,Chapter类与Book类不同,因为Chapter类还定义了一个属性来关联一个外键: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; }
}
模型类型之间的映射现在在BooksContext的OnModelCreating方法中定义。Book类型与多个Chapter对象相关联;这是使用HasMany和WithOne定义的。Chapter类与一个Book对象相关联;这是使用HasOne和WithMany定义的。因为在Chapter类中还有一个外键属性,所以使用HasForeignKey方法来指定这个键:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>(entity =>
{
entity.HasMany(b => b.Chapters)
.WithOne(c => c.Book);
entity.HasOne(b => b.Author)
.WithMany(w => w.WrittenBooks);
entity.HasOne(b => b.Reviewer)
.WithMany(r => r.ReviewedBooks);
entity.HasOne(b => b.Editor)
.WithMany(p => p.EditedBooks);
});
modelBuilder.Entity<Chapter>(entity =>
{
entity.HasOne(c => c.Book)
.WithMany(b => b.Chapters)
.HasForeignKey(c => c.BookId);
});
modelBuilder.Entity<User>(entity =>
{
entity.HasMany(u => u.WrittenBooks)
.WithOne(b => b.Author);
entity.HasMany(u => u.ReviewedBooks)
.WithOne(b => b.Reviewer);
entity.HasMany(u => u.EditedBooks)
.WithOne(b => b.Editor);
});
}
6. 根据约定的每个层次结构的表
本文地址:https://blog.csdn.net/qq_41708190/article/details/107322586