关系可以定义为一对一或一对多。对于多对多关系,在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