C#9 和 .NET5 高级教程(十二)

原文:Pro C# 9 with .NET 5

协议:CC BY-NC-SA 4.0

二十二、实体框架核心简介

前一章研究了 ADO.NET 的基本面。ADO.NET 促成了。NET 程序员使用关系数据(以一种相对简单的方式)。NET 平台。在 ADO.NET 的基础上,微软引入了 ADO.NET API 的一个新组件,名为实体框架(或简称为 EF )。NET 3.5 服务包 1。

EF 的首要目标是允许您使用直接映射到应用中业务对象(或域对象)的对象模型与关系数据库中的数据进行交互。例如,您可以对称为实体的强类型对象集合进行操作,而不是将一批数据视为行和列的集合。这些实体保存在支持 LINQ 的专用集合类中,支持使用 C# 代码进行数据访问操作。集合类使用你在第十三章中学到的相同的 LINQ 语法提供对数据存储的查询。

就像。NET 核心框架,实体框架核心是对实体框架 6 的完全重写。它建立在。NET Core 框架,使 EF Core 能够在多个平台上运行。重写 EF Core 使团队能够为 EF Core 添加新的特性和性能改进,这些在 EF 6 中无法合理实现。

从头开始重新创建一个完整的框架需要仔细考虑新框架将支持哪些特性,哪些特性将被抛弃。EF 6 的一个特性是对实体设计器的支持,这个特性不在 EF 核心中(也不太可能被添加)。EF Core 只支持代码优先的开发范式。如果您目前正在首先使用代码,您可以放心地忽略前面的句子。

Note

EF Core 可用于现有数据库以及空白和/或新数据库。这两种机制都被称为代码优先,这可能不是最好的名字。实体类和派生的DbContext可以从现有的数据库中搭建,数据库可以从实体类中创建和更新。在 EF 核心章节中,你会学到这两种方法。

在每个版本中,EF Core 都添加了更多 EF 6 中已有的功能,以及 EF 6 中从未有过的新功能。3.1 版本显著缩短了 EF 核心中缺少的基本特性列表(与 EF 6 相比),5.0 版本进一步缩小了差距。事实上,对于大多数项目来说,英孚核心拥有你所需要的一切。

本章和下一章将向您介绍使用实体框架核心的数据访问。您将了解如何创建域模型、将实体类和属性映射到数据库表和列、实现变更跟踪、使用 EF Core 命令行界面(CLI)进行搭建和迁移,以及DbContext类的角色。您还将学习将实体与导航属性、事务和并发检查相关联,这只是所探索的一些特性。

当您完成这些章节时,您将拥有我们的AutoLot数据库的数据访问层的最终版本。在我们进入 EF Core 之前,我们先来谈谈对象关系映射器。

Note

两章远不足以涵盖所有的实体框架核心,因为整本书(有些和这本书一样大)都是专门讨论 EF 核心的。这些章节的目的是为您提供实用知识,帮助您开始将 EF Core 用于您的业务线应用。

对象关系映射器

ADO.NET 为您提供了一个结构,允许您通过连接、命令和数据读取器来选择、插入、更新和删除数据。虽然这一切都很好,但 ADO.NET 的这些方面迫使您以与物理数据库模式紧密耦合的方式处理提取的数据。例如,回想一下,当从数据库中获取记录时,您打开一个连接,创建并执行一个命令对象,然后使用数据读取器通过数据库特定的列名迭代每条记录。

当您使用 ADO.NET 时,您必须时刻注意后端数据库的物理结构。您必须知道每个数据表的模式,编写潜在的复杂 SQL 查询以与数据表交互,跟踪对检索(或添加)数据的更改,等等。这可能会迫使您编写一些相当冗长的 C# 代码,因为 C# 本身并不直接使用数据库模式的语言。

更糟糕的是,物理数据库的构造方式通常完全集中在数据库构造上,如外键、视图、存储过程和数据规范化,而不是面向对象的编程。

应用开发人员关心的另一个问题是变更跟踪。从数据库获取数据是该过程的一个步骤,但是开发人员必须跟踪任何更改、添加和/或删除,以便可以将它们持久化回数据存储。

中的对象关系映射框架(通常称为 ORM)的可用性。NET 通过为开发人员管理大量的创建、读取、更新和删除(CRUD)数据访问任务,极大地增强了数据访问能力。开发人员创建。NET 对象和关系数据库,ORM 管理连接、查询生成、变更跟踪和持久化数据。这使得开发人员可以专注于应用的业务需求。

Note

重要的是要记住,ORM 不是骑在彩虹上的神奇独角兽。每一个决定都涉及到取舍。ORM 减少了开发人员创建数据访问层的工作量,但是如果使用不当,也会带来性能和伸缩问题。对 CRUD 操作使用 ORM,对基于集合的操作使用数据库的能力。

尽管不同的 ORM 在操作方式和使用方式上略有不同,但它们本质上都有相同的部分,并为相同的目标而努力——使数据访问操作更容易。实体是映射到数据库表的类。专用集合类型包含一个或多个实体。改变跟踪机制跟踪实体的状态以及对它们所做的任何改变、添加和/或删除,并且中央构造作为领头者控制操作。

理解实体框架核心的作用

在幕后,EF 核心使用你已经在前一章检查的 ADO.NET 基础设施。像任何与数据存储的 ADO.NET 交互一样,EF Core 使用 ADO.NET 数据提供者进行数据存储交互。在 EF Core 可以使用 ADO.NET 数据提供程序之前,必须对其进行更新,以与 EF Core 完全集成。由于这一新增功能,EF 核心数据提供商可能比 added 数据提供商更少。

使用 ADO.NET 数据库提供商模式的 EF Core 的好处是,它使您能够在同一个项目中结合 EF Core 和 ADO.NET 数据访问范例,增强您的能力。例如,使用 EF Core 为批量复制操作提供连接、模式和表名利用了 EF Core 的映射功能和内置于 ADO.NET 的 BCP 功能。这种混合方法使 EF Core 成为您工具箱中的又一个工具。

当您看到有多少基本的数据访问管道以方便和有效的方式为您处理时,EF Core 很可能会成为您的数据访问首选机制。

Note

许多第三方数据库(如 Oracle 和 MySQL)都提供支持 EF 的数据提供者。如果您没有使用 SQL Server,请咨询您的数据库供应商了解详细信息,或者导航至 https://docs.microsoft.com/en-us/ef/core/providers 获取可用的 EF 核心数据提供商列表。

EF Core 最适合数据之上的表单(或数据之上的 API)情况下的开发过程。使用工作单元模式对少量实体进行操作以确保一致性是 EF Core 的优势。它不太适合大规模数据操作,如提取-转换-加载(ETL)数据仓库应用或大型报告情况。

实体框架的构建块

EF 核心的主要组件是DbContextChangeTrackerDbSet专用集合类型、数据库提供者和应用的实体。要完成本部分,请创建一个名为 AutoLot 的新控制台应用。取样并添加Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.DesignMicrosoft.EntityFrameworkCore.SqlServer包。

dotnet new sln -n Chapter22_AllProjects
dotnet new console -lang c# -n AutoLot.Samples -o .\AutoLot.Samples -f net5.0
dotnet sln .\Chapter22_AllProjects.sln add .\AutoLot.Samples
dotnet add AutoLot.Samples package Microsoft.EntityFrameworkCore
dotnet add AutoLot.Samples package Microsoft.EntityFrameworkCore.Design
dotnet add AutoLot.Samples package Microsoft.EntityFrameworkCore.SqlServer

DbContext 类

DbContext是 EF 核心的首要组件,通过Database属性提供对数据库的访问。DbContext管理ChangeTracker实例,公开访问 Fluent API 的虚拟OnModelCreating方法,保存所有的DbSet<T>属性,并提供SaveChanges方法将数据保存到数据存储中。它不是直接使用的,而是通过一个继承了DbContext的自定义类。在这个类中放置了DbSet<T>属性。

表 22-1 显示了一些更常用的DbContext成员。

表 22-1。

DbContext的普通成员

|

DbContext的成员

|

生命的意义

|
| — | — |
| Database | 提供对数据库相关信息和功能的访问,包括 SQL 语句的执行。 |
| Model | 关于实体形状、它们之间的关系以及它们如何映射到数据库的元数据。**注意:**这个属性通常不直接交互。 |
| ChangeTracker | 提供对该DbContext正在跟踪的实体实例的信息和操作的访问。 |
| DbSet<T> | 不是真正的DbContext成员,而是添加到自定义派生类DbContext的属性。这些属性属于DbSet<T>类型,用于查询和保存应用实体的实例。针对DbSet<T>属性的 LINQ 查询被转换成 SQL 查询。 |
| Entry | 提供对实体的更改跟踪信息和操作的访问,例如显式加载相关实体或更改EntityState。也可以在未跟踪的实体上调用,以将状态更改为已跟踪。 |
| Set<TEntity> | 创建可用于查询和保存数据的DbSet<T>属性的实例。 |
| SaveChanges / SaveChangesAsync | 将所有实体更改保存到数据库,并返回受影响的记录数。在事务中执行(隐式或显式)。 |
| Add / AddRange``Update / UpdateRange``Remove / RemoveRange | 方法来添加、更新和移除实体实例。只有当SaveChanges成功执行时,更改才会被保存。异步版本也可用。**注意:**虽然在派生的DbContext上可用,但这些方法通常直接在DbSet<T>属性上调用。 |
| Find | 查找具有给定主键值的类型的实体。异步版本也可用。**注意:**虽然在派生的DbContext上可用,但这些方法通常直接在DbSet<T>属性上调用。 |
| Attach / AttachRange | 开始跟踪实体(或实体列表)。异步版本也可用。**注意:**虽然在派生的DbContext上可用,但这些方法通常直接在DbSet<T>属性上调用。 |
| SavingChanges | 在开始调用SaveChanges / SaveChangesAsync时触发事件。 |
| SavedChanges | 在对SaveChanges / SaveChangesAsync的调用结束时触发的事件。 |
| SaveChangesFailed | 对SaveChanges / SaveChangesAsync的调用失败时触发的事件。 |
| OnModelCreating | 当一个模型已经被初始化,但在它完成之前调用。来自 Fluent API 的方法被放置在该方法中,以最终确定模型的形状。 |
| OnConfiguring | 用于创建或修改DbContext选项的生成器。每次创建一个DbContext实例时执行。**注意:**建议不要使用这个,而是使用DbContextOptions在运行时配置DbContext实例,在设计时使用IDesignTimeDbContextFactory实例。 |

创建派生的 DbContext

EF Core 的第一步是创建一个从DbContext继承的自定义类。然后添加一个构造函数,该构造函数接受一个强类型的实例DbContextOptions(接下来将介绍)并将该实例传递给基类。

namespace AutoLot.Samples
{
  public class ApplicationDbContext : DbContext
  {
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
    {
    }
  }
}

这是一个用来访问数据库和处理实体、变更跟踪器以及 EF 核心的所有组件的类。

配置数据库上下文

使用DbContextOptions类的实例来配置DbContext实例。使用DbContextOptionsBuilder创建DbContextOptions实例,因为DbContextOptions类并不意味着直接在您的代码中构造。通过DbContextOptionsBuilder实例,选择数据库提供者(以及任何提供者特定的设置),并设置 EF Core DbContext通用选项(如日志记录)。然后在运行时将Options属性注入到基DbContext中。

这种动态配置功能支持在运行时更改设置,只需选择不同的选项(例如,MySQL 而不是 SQL Server provider)并创建派生的DbContext的新实例。

设计时 DbContext 工厂

设计时DbContext工厂是实现IDesignTimeDbContextFactory<T>接口的类,其中T是派生的DbContext类。该接口有一个方法CreateDbContext(),您必须实现它来创建您的派生DbContext的实例。

下面的ApplicationDbContextFactory类使用CreateDbContext()方法为ApplicationDbContext类创建一个强类型的DbContextOptionsBuilder,将数据库提供者设置为 SQL Server 提供者(使用第二十一章中的 Docker 实例连接字符串),然后创建并返回一个新的ApplicationDbContext实例:

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

namespace AutoLot.Samples
{
  public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
  {
    public ApplicationDbContext CreateDbContext(string[] args)
    {
      var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
      var connectionString = @"server=.,5433;Database=AutoLotSamples;User Id=sa;Password=P@ssw0rd;";
      optionsBuilder.UseSqlServer(connectionString);
      Console.WriteLine(connectionString);
      return new ApplicationDbContext(optionsBuilder.Options);
    }
  }
}

命令行界面使用上下文工厂来创建派生的DbContext类的实例,以执行诸如数据库迁移创建和应用之类的操作。因为它是设计时构造的,而不是在运行时使用的,所以开发数据库的连接字符串通常是硬编码的。

EF Core 5 中的新特性,参数可以从命令行传递给CreateDbContext()方法。在这一章的后面你会学到更多。

on model 创建

基本的DbContext类公开了OnModelCreating方法,该方法用于使用 Fluent API 来形成您的实体。这将在本章后面深入讨论,但是现在,将下面的代码添加到ApplicationDbContext类中:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  // Fluent API calls go here
  OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);

保存更改

为了触发DbContextChangeTracker来保持被跟踪实体中的任何变化,在派生的DbContext上调用SaveChanges()(或SaveChangesAsync())方法。

static void SampleSaveChanges()
{
  //The factory is not meant to be used like this, but it’s demo code :-)
    var context = new ApplicationDbContextFactory().CreateDbContext(null);
    //make some changes
    context.SaveChanges();
}

在本章(和本书)的剩余部分将会有很多保存更改的例子。

交易和保存点支持

EF Core 使用数据库的隔离级别将每个对SaveChanges / SaveChangesAsync的调用包装在一个隐式事务中。为了获得更多的控制,您还可以将派生的DbContext加入到显式事务中。要在显式事务中执行,使用派生的DbContextDatabase属性创建一个事务。照常执行操作,然后提交或回滚事务。下面是演示这一点的代码片段:

using var trans = context.Database.BeginTransaction();
try
{
  //Create, change, delete stuff
  context.SaveChanges();
  trans.Commit();
}
catch (Exception ex)
{
  trans.Rollback();
}

EF Core 5 中引入了 EF Core 交易的保存点。当调用SaveChanges() / SaveChangesAsync()时,事务已经在进行中,EF Core 在该事务中创建一个保存点。如果调用失败,事务将回滚到保存点,而不是事务的开始。保存点也可以通过调用事务上的CreateSavePoint()RollbackToSavepoint()以编程方式进行管理,如下所示:

using var trans = context.Database.BeginTransaction();
try
{
  //Create, change, delete stuff
  trans.CreateSavepoint("check point 1");
  context.SaveChanges();
  trans.Commit();
}
catch (Exception ex)
{
  trans. RollbackToSavepoint("check point 1");
}

交易和执行策略

当一个执行策略处于活动状态时(如在使用EnableRetryOnFailure()时),在创建一个显式事务之前,您必须获得一个对 EF Core 正在使用的当前执行策略的引用。然后调用策略上的Execute()方法来创建一个显式事务。

var strategy = context.Database.CreateExecutionStrategy();
strategy.Execute(() =>
{
  using var trans = context.Database.BeginTransaction();
  try
  {
    actionToExecute();
    trans.Commit();
  }
  catch (Exception ex)
  {
    trans.Rollback();
  }
});

保存/已保存的更改事件

EF Core 5 引入了三个由SaveChanges() / SaveChangesAsync()方法触发的新事件。当调用SaveChanges()时(但是在针对数据存储执行 SQL 语句之前)触发SavingChanges,在SaveChanges()完成后SavedChanges触发。以下(简单的)代码示例显示了事件及其处理程序的运行情况:

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
    : base(options)
{
  SavingChanges += (sender, args) =>
  {
    Console.WriteLine($"Saving changes for {((DbContext)sender).Database.GetConnectionString()}");
  };
  SavedChanges += (sender, args) =>
  {
    Console.WriteLine($"Saved {args.EntitiesSavedCount} entities");
  };
  SaveChangesFailed += (sender, args) =>
  {
    Console.WriteLine($"An exception occurred! {args.Exception.Message} entities");
  };
}

DbSet 类

对于对象模型中的每个实体,添加一个类型为DbSet<T>的属性。DbSet<T>类是一个专门的集合属性,用于与数据库提供者交互,以获取、添加、更新或删除数据库中的记录。每个DbSet<T>为数据库交互的每个集合提供许多核心服务。对DbSet<T>类执行的任何 LINQ 查询都被数据库提供者翻译成数据库查询。表 22-2 描述了一些DbSet<T>级的核心成员。

表 22-2。

DbSet<T>的公共成员和扩展方法

|

DbSet<T>的成员

|

生命的意义

|
| — | — |
| Add / AddRange | 开始跟踪处于Added状态的实体。当调用SaveChanges时,项目将被添加。异步版本也可用。 |
| AsAsyncEnumerable | 以IAsyncEnumerable<T>的形式返回集合。 |
| AsQueryable | 以IQueryable<T>的形式返回集合。 |
| Find | 通过主键在ChangeTracker中搜索实体。如果在变更跟踪器中找不到,则查询数据存储中的对象。异步版本也可用。 |
| Update / UpdateRange | 开始跟踪处于Modified状态的实体。调用SaveChanges时,项目将被更新。异步版本也可用。 |
| Remove / RemoveRange | 开始跟踪处于Deleted状态的实体。当调用SaveChanges时,项目将被移除。异步版本也可用。 |
| Attach / AttachRange | 开始跟踪实体。将数字主键定义为标识且值等于零的实体在添加时被跟踪。所有其他的被跟踪为未改变。异步版本也可用。 |
| FromSqlRaw / FromSqlInterpolated | 基于表示 SQL 查询的原始或插值字符串创建 LINQ 查询。可以与用于服务器端执行的附加 LINQ 语句结合使用。 |
| AsQueryable() | 从DbSet<T>返回一个IQueryable<T>实例。 |

DbSet<T>实现IQueryable<T>并且通常是实体查询的 LINQ 的目标。除了 EF Core 增加的扩展方法,DbSet<T>还支持你在第十三章中了解到的相同的扩展方法,比如ForEach()Select()All()

您将在“实体”部分向ApplicationDbContext添加DbSet<T>属性。

Note

表 22-2 中列出的许多方法的名称与表 22-1 中的方法相同。主要的区别在于,DbSet<T>方法已经知道要操作的类型,并且有实体列表。DbContext方法必须使用反射来决定做什么。使用DbSet<T>的方法比使用DbContext的方法更常见。

查询类型

查询类型是用于表示视图、SQL 语句或没有主键的表的DbSet<T>集合。以前版本的 EF 核心使用DbQuery<T>来处理这些,但是从 EF 核心 3.1 开始,DbQuery类型已经被淘汰。使用DbSet<T>属性将查询类型添加到派生的DbContext中,并配置为无键。

例如,CustomerOrderViewModel(您将在构建完整的AutoLot数据访问库时创建)配置有Keyless属性。

[Keyless]
public class CustomerOrderViewModel
{
...
}

其余的配置在 Fluent API 中进行。以下示例将实体设置为 keyless,并将查询类型映射到dbo.CustomerOrderView数据库视图(注意,如果Keyless数据注释在模型上,则HasNoKey() Fluent API 方法不是必需的,反之亦然,但为了完整起见,在本示例中显示了该方法):

modelBuilder.Entity<CustomerOrderViewModel>().HasNoKey().ToView("CustomerOrderView", "dbo");

查询类型也可以映射到 SQL 查询,如下所示:

modelBuilder.Entity<CustomerOrderViewModel>().HasNoKey().ToSqlQuery(
  @"SELECT c.FirstName, c.LastName, i.Color, i.PetName, m.Name AS Make
        FROM   dbo.Orders o
        INNER JOIN dbo.Customers c ON o.CustomerId = c.Id
        INNER JOIN dbo.Inventory  i ON o.CarId = i.Id
        INNER JOIN dbo.Makes m ON m.Id = i.MakeId");

查询类型可以使用的最后一种机制是FromSqlRaw()FromSqlInterpolated()方法。下面是一个使用FromSqlRaw()的相同查询的例子:

public IEnumerable<CustomerOrderViewModel> GetOrders()
{
  return CustomerOrderViewModels.FromSqlRaw(
    @"SELECT c.FirstName, c.LastName, i.Color, i.PetName, m.Name AS Make
          FROM   dbo.Orders o
          INNER JOIN dbo.Customers c ON o.CustomerId = c.Id
          INNER JOIN dbo.Inventory  i ON o.CarId = i.Id
          INNER JOIN dbo.Makes m ON m.Id = i.MakeId");
}

灵活的查询/表映射

EF Core 5 引入了将同一个类映射到多个数据库对象的能力。这些对象可以是表、视图或函数。例如,来自章节 21 的CarViewModel可以映射到一个视图,该视图返回带有Car数据和Inventory表的品牌名称。然后,EF Core 将从视图中查询,并将更新发送到表中。

modelBuilder.Entity<CarViewModel>()
  .ToTable("Inventory")
  .ToView("InventoryWithMakesView");

变化跟踪者

在一个DbContext实例中,ChangeTracker实例跟踪加载到DbSet<T>中的对象的状态。表 22-3 列出了对象状态的可能值。

表 22-3。

实体状态枚举值

|

价值

|

生命的意义

|
| — | — |
| Added | 正在跟踪该实体,但它尚不存在于数据库中。 |
| Deleted | 该实体正在被跟踪,并被标记为从数据库中删除。 |
| Detached | 变更跟踪器没有跟踪该实体。 |
| Modified | 该条目正在被跟踪并已被更改。 |
| Unchanged | 该实体正在被跟踪,存在于数据库中,并且尚未被修改。 |

如果需要检查对象的状态,请使用以下代码:

EntityState state = context.Entry(entity).State;

您还可以使用相同的机制以编程方式更改对象的状态。要将状态更改为Deleted(例如),请使用以下代码:

context.Entry(entity).State = EntityState.Deleted;

ChangeTracker 事件

有两个事件可以由ChangeTracker引发。第一个是StateChanged,第二个是Tracked。当一个实体的状态改变时,触发StateChanged事件。当第一次跟踪实体时,它不会触发。当一个实体开始被跟踪时,触发Tracked事件,无论是通过编程添加到一个DbSet<T>实例,还是从一个查询返回。

重置 DbContext 状态

EF Core 5 的新功能是重置一个DbContextChangeTracker.Clear()方法通过将实体的状态设置为 detached 来清除DbSet<T>属性中的所有实体。

实体

映射到数据库表的强类型类被正式称为实体。应用中的实体集合包括物理数据库的概念模型。正式来说,这个模型被称为实体数据模型 (EDM),通常简称为模型。模型被映射到应用/业务领域。实体及其属性使用实体框架核心约定、配置和 Fluent API(代码)映射到表和列。实体不需要直接映射到数据库模式。您可以自由地构建实体类来满足您的应用需求,然后将您唯一的实体映射到您的数据库模式。

数据库和实体之间的这种松散耦合意味着您可以独立于数据库设计和结构来塑造实体以匹配您的业务领域。例如,从上一章的AutoLot数据库中的简单的Inventory表和Car实体类。名称不同,但是Car实体映射到了Inventory表。EF Core 检查模型中实体的配置,将客户端表示的Inventory表(在我们的例子中是Car类)映射到Inventory表的正确列。

接下来的几个部分详细介绍了 EF 核心约定、数据注释和代码(使用 Fluent API)如何将模式中的实体、属性和实体之间的关系映射到数据库中的表、列和外键关系。

将属性映射到列

当使用关系数据存储时,EF 核心约定将所有读写公共属性映射到实体所映射到的表中的列。如果属性是自动属性,EF Core 通过 getter 和 setter 进行读写。如果属性有支持字段,EF Core 将读写支持字段而不是公共属性,即使支持字段是私有的。虽然 EF Core 可以对私有字段进行读写,但是仍然必须有一个封装了后台字段的公共读写属性。

后台字段支持有两种优势,一种是在 Windows Presentation Foundation(WPF)应用中使用INotifyPropertyChanged模式,另一种是数据库默认值与。核心默认值净值。在第二十八章中介绍了使用 EF 内核和 WPF,数据库默认值将在本章稍后介绍。

列的名称、数据类型和可空性是通过约定、数据注释和/或 Fluent API 配置的。本章稍后将深入讨论这些主题。

将类映射到表

EF Core 中有两种可用的类到表的映射方案:每层次表(TPH)每类型表(TPT) 。TPH 映射是默认的,它将继承层次结构映射到单个表。TPT 是 EF Core 5 中的新特性,它将层次结构中的每个类映射到自己的表中。

Note

类也可以映射到视图和原始 SQL 查询。这些被称为查询类型,将在本章后面介绍。

每层次表映射(TPH)

考虑下面的例子,它显示了从第二十一章到的Car类被分成两个类:一个基类用于IdTimeStamp属性,其余的属性留在Car类中。这两个类都应该创建在自动程序的Models目录中。示例项目。

using System.Collections.Generic;

namespace AutoLot.Samples.Models
{
  public abstract class BaseEntity
  {
    public int Id { get; set; }
    public byte[] TimeStamp { get; set; }
  }
}

using System.Collections.Generic;
namespace AutoLot.Samples.Models
{
  public class Car : BaseEntity
  {
    public string Color { get; set; }
    public string PetName { get; set; }
    public int MakeId { get; set; }
  }
}

为了让 EF Core 知道实体类是对象模型的一部分,为实体添加一个DbSet<T>属性。将以下using语句添加到ApplicationDbContext类中:

using AutoLot.Samples.Models;

将以下代码添加到构造函数和OnModelCreating()方法之间的ApplicationDbContext类中:

public DbSet<Car> Cars { get; set; }

注意,基类是作为DbSet<T>实例添加的而不是。尽管移植细节将在本章后面介绍,但让我们创建数据库和Cars表。在与 AutoLot 相同的目录中打开命令提示符。示例项目,并运行以下命令(全部在一行中):

dotnet tool install s--global dotnet-ef --version 5.0.1
dotnet ef migrations add TPH -o Migrations -c AutoLot.Samples.ApplicationDbContext
dotnet ef database update TPH  -c AutoLot.Samples.ApplicationDbContext

第一个命令将 EF 核心命令行工具安装为一个全局工具。这只需要在您的机器上进行一次。第二个命令使用AutoLot.Samples名称空间中的ApplicationDbContextMigrations目录中创建了一个名为 TPH 的迁移。第三个命令从 TPH 迁移中更新数据库。

当使用 EF Core 在数据库中创建这个表时,继承的BaseEntity类被合并到Car类中,并且创建了一个表,如下所示:

CREATE TABLE [dbo].Cars NOT NULL,
  [MakeId] [int] NOT NULL,
  [Color] nvarchar NULL,
  [PetName] nvarchar NULL,
  [TimeStamp] varbinary NULL,
 CONSTRAINT [PK_Cars] PRIMARY KEY CLUSTERED
(
  [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

前一个例子依赖于 EF 核心约定(很快会介绍)来创建表和列属性。

每类型表映射(TPT)

为了探索 TPT 映射模式,可以使用前面的相同实体,即使基类被标记为抽象。因为 TPH 是默认的,所以必须指示 EF Core 将每个类映射到一个表。这可以通过数据注释或 Fluent API 来完成。将以下代码添加到ApplicationDbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<BaseEntity>().ToTable("BaseEntities");
  modelBuilder.Entity<Car>().ToTable("Cars");
  OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);

要“重置”数据库和项目,请删除Migrations文件夹和数据库。要使用 CLI 强制删除数据库,请输入以下内容:

dotnet ef database drop -f -c AutoLot.Samples.ApplicationDbContext

现在为 TPT 模式创建并应用迁移。

dotnet ef migrations add TPT -o Migrations -c AutoLot.Samples.ApplicationDbContext
dotnet ef database update TPT  -c AutoLot.Samples.ApplicationDbContext

EF 核心将在更新数据库时创建以下表格。索引还显示这些表有一对一的映射。

CREATE TABLE [dbo].BaseEntities NOT NULL,
  [TimeStamp] varbinary NULL,
 CONSTRAINT [PK_BaseEntities] PRIMARY KEY CLUSTERED
(
  [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

CREATE TABLE [dbo].Inventory NULL,
  [PetName] nvarchar NULL,
 CONSTRAINT [PK_Inventory] PRIMARY KEY CLUSTERED
(
  [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

ALTER TABLE [dbo].[Inventory]  WITH CHECK ADD  CONSTRAINT [FK_Inventory_BaseEntities_Id] FOREIGN KEY([Id])
REFERENCES [dbo].[BaseEntities] ([Id])
GO
ALTER TABLE [dbo].[Inventory] CHECK CONSTRAINT [FK_Inventory_BaseEntities_Id]
GO

Note

每种类型的表映射具有显著的性能影响,在使用这种映射方案之前应该考虑到这一点。更多信息请参考文档: https://docs.microsoft.com/en-us/ef/core/performance/modeling-for-performance#inheritance-mapping

为了“重置”数据库和项目以准备下一组示例,注释掉OnModelCreating()方法中的代码,并再次删除Migrations文件夹和数据库。

dotnet ef database drop -f -c AutoLot.Samples.ApplicationDbContext

导航属性和外键

导航属性表示实体类如何相互关联,并使代码能够从一个实体实例遍历到另一个实体实例。根据定义,导航属性是映射到数据库提供程序定义的非标量类型的任何属性。实际上,导航属性映射到另一个实体(称为引用导航属性)或者另一个实体的集合(称为集合导航属性)。在数据库端,导航属性被转换成表之间的外键关系。在 EF Core 中直接支持一对一、一对多和(EF Core 5 中新增的)多对多关系。实体类也可以有自己的导航属性,表示自引用表。

Note

我发现将具有导航属性的对象视为链表是很有帮助的,如果导航属性是双向的,那么对象的行为就像双向链表。

在详细介绍导航属性和实体关系模式之前,请参考表 22-4 。这三种关系模式中都用到这些术语。

表 22-4。

用于描述导航属性和关系的术语

|

学期

|

生命的意义

|
| — | — |
| 主要实体 | 关系的父级。 |
| 从属实体 | 关系的孩子。 |
| 主键 | 用于定义主体实体的属性。可以是主键或备用键。可以使用单个属性或多个属性来配置密钥。 |
| 外键 | 子实体保存的用于存储主键的一个或多个属性。 |
| 所需的关系 | 需要外键值的关系(不可为空)。 |
| 可选关系 | 外键值不可为空的关系。 |

缺少外键属性

如果具有引用导航属性的实体没有用于外键值的属性,EF Core 将在实体上创建必要的属性。这些被称为影子外键属性,并以<navigation property name><principal key property name><principal entity name><principal key property name>的格式命名。这适用于所有的关系类型(一对多、一对一、多对多)。与让 EF Core 为您创建实体相比,使用显式外键属性来构建您的实体是一种更为干净的方法。

一对多关系

为了创建一对多关系,一方(主体)的实体类添加多方(从属)的实体类的集合属性。依赖实体还应该拥有主体外键的属性。如果没有,EF Core 将创建影子外键属性,如前所述。

例如,在第二十一章创建的数据库中,Makes表(由Make实体类表示)和Inventory表(由Car实体类表示)是一对多的关系。为了使这些例子简单起见,Car实体将映射到Cars表。以下代码显示了表示这种关系的双向导航属性:

using System.Collections.Generic;
namespace AutoLot.Samples.Models
{
    public class Make : BaseEntity
    {
       public string Name { get; set; }
       public IEnumerable<Car> Cars { get; set; } = new List<Car>();
    }
}

using System.Collections.Generic;
namespace AutoLot.Samples.Models
{
  public class Car : BaseEntity
  {
    public string Color { get; set; }
    public string PetName { get; set; }
    public int MakeId { get; set; }
    public Make MakeNavigation { get; set; }
  }
}

Note

当搭建现有数据库时,EF 核心名称引用与属性类型名称相同的导航属性(例如,public Make {get; set;})。这可能会导致导航和智能感知方面的问题,更不用说使代码难以处理了。为了清楚起见,我更喜欢在引用导航属性时添加后缀Navigation,如前面的例子所示。

Car / Make的例子中,Car实体是从属实体(一对多的),而Make实体是主体实体(一对多的)。

DbSet<Make>实例添加到ApplicationDbContext,如下图所示:

public DbSet<Car> Cars { get; set; }
public DbSet<Make> Makes { get; set; }

使用以下命令创建迁移并更新数据库:

dotnet ef migrations add One2Many -o Migrations -c AutoLot.Samples.ApplicationDbContext
dotnet ef database update One2Many  -c AutoLot.Samples.ApplicationDbContext

使用 EF 核心迁移更新数据库时,会创建以下表格:

CREATE TABLE [dbo].Makes NOT NULL,
  [Name] nvarchar NULL,
  [TimeStamp] varbinary NULL,
 CONSTRAINT [PK_Makes] PRIMARY KEY CLUSTERED
(
  [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

CREATE TABLE [dbo].Cars NOT NULL,
  [Color] nvarchar NULL,
  [PetName] nvarchar NULL,
  [TimeStamp] varbinary NULL,
  [MakeId] [int] NOT NULL,
 CONSTRAINT [PK_Cars] PRIMARY KEY CLUSTERED
(
  [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

ALTER TABLE [dbo].[Cars]  WITH CHECK ADD  CONSTRAINT [FK_Cars_Makes_MakeId] FOREIGN KEY([MakeId])
REFERENCES [dbo].[Makes] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[Cars] CHECK CONSTRAINT [FK_Cars_Makes_MakeId]
GO

注意在 dependent ( Cars)表上创建的外键和检查约束。

一对一的关系

在一对一关系中,两个实体都具有对另一个实体的引用导航属性。虽然一对多关系明确表示主体和从属实体,但在建立一对一关系时,必须通过明确定义主体实体的外键或通过使用 Fluent API 指示主体来告知 EF Core 哪一方是主体。如果 EF Core 没有得到通知,它将根据自己检测外键的能力选择一个。实际上,您应该通过添加外键属性来明确定义依赖项。

namespace AutoLot.Samples.Models
{
  public class Car : BaseEntity
  {
    public string Color { get; set; }
    public string PetName { get; set; }
    public int MakeId { get; set; }
    public Make MakeNavigation { get; set; }
    public Radio RadioNavigation { get; set; }
  }
}

namespace AutoLot.Samples.Models
{
  public class Radio : BaseEntity
  {
    public bool HasTweeters { get; set; }
    public bool HasSubWoofers { get; set; }
    public string RadioId { get; set; }
    public int CarId { get; set; }
    public Car CarNavigation { get; set; }
  }
}

由于Radio有一个到Car类的外键(基于约定,稍后将介绍),Radio是依赖实体,Car是主体实体。EF Core 隐式地在依赖实体的外键属性上创建所需的唯一索引。如果您想要更改索引的名称,可以使用数据注释或 Fluent API 来完成。

DbSet<Radio>加到ApplicationDbContext上。

public virtual DbSet<Car> Cars { get; set; }
public virtual DbSet<Make> Makes { get; set; }
public virtual DbSet<Radio> Radios { get; set; }

使用以下命令创建迁移并更新数据库:

dotnet ef migrations add One2One -o Migrations -c AutoLot.Samples.ApplicationDbContext
dotnet ef database update One2One  -c AutoLot.Samples.ApplicationDbContext

当使用 EF 核心迁移更新数据库时,Cars表保持不变,并创建以下Radios表:

CREATE TABLE [dbo].Radios NOT NULL,
  [HasTweeters] [bit] NOT NULL,
  [HasSubWoofers] [bit] NOT NULL,
  [RadioId] nvarchar NULL,
  [TimeStamp] varbinary NULL,
  [CarId] [int] NOT NULL,
 CONSTRAINT [PK_Radios] PRIMARY KEY CLUSTERED
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE [dbo].[Radios]  WITH CHECK ADD  CONSTRAINT [FK_Radios_Cars_CarId] FOREIGN KEY([CarId])
REFERENCES [dbo].[Cars] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[Radios] CHECK CONSTRAINT [FK_Radios_Cars_CarId]
GO

注意在 dependent ( Radios)表上创建的外键和检查约束。

多对多关系(新 EF Core 5)

在多对多关系中,两个实体都有指向另一个实体的集合导航属性。这是在数据存储中实现的,在两个实体表之间有一个连接表。这个连接表是以使用<Entity1Entity2>的两个表命名的。可以通过 Fluent API 以编程方式更改该名称。连接实体与每个实体表都有一对多的关系。

namespace AutoLot.Samples.Models
{
  public class Car : BaseEntity
  {
    public string Color { get; set; }
    public string PetName { get; set; }
    public int MakeId { get; set; }
    public Make MakeNavigation { get; set; }
    public Radio RadioNavigation { get; set; }
    public IEnumerable<Driver> Drivers { get; set; } = new List<Driver>();
  }
}

namespace AutoLot.Samples.Models
{
  public class Driver : BaseEntity
  {
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public IEnumerable<Car> Cars { get; set; } = new List<Car>();
  }
}

等效的方法可以通过显式创建三个表来完成,这也是在 EF Core 5 之前的 EF Core 版本中必须完成的工作。下面是一个简短的例子:

public class Driver
{
...
  public IEnumerable<CarDriver> CarDrivers { get; set; }
}

public class Car
{
...
  public IEnumerable<CarDriver> CarDrivers { get; set; }
}
public class CarDriver
{
  public int CarId {get;set;}
  public Car CarNavigation {get;set;}
  public int DriverId {get;set;}
  public Driver DriverNavigation {get;set;}
}

DbSet<Driver>加到ApplicationDbContext上。

public virtual DbSet<Car> Cars { get; set; }
public virtual DbSet<Make> Makes { get; set; }
public virtual DbSet<Radio> Radios { get; set; }
public virtual DbSet<Driver> Drivers { get; set; }

使用以下命令创建迁移并更新数据库:

dotnet ef migrations add Many2Many -o Migrations -c AutoLot.Samples.ApplicationDbContext
dotnet ef database update many2Many  -c AutoLot.Samples.ApplicationDbContext

当使用 EF 核心迁移更新数据库时,Cars表保持不变,而DriversCarDriver表被创建。

CREATE TABLE [dbo].Drivers NOT NULL,
  [FirstName] NVARCHAR NULL,
  [LastName] NVARCHAR NULL,
  [TimeStamp] VARBINARY NULL,
 CONSTRAINT [PK_Drivers] PRIMARY KEY CLUSTERED
(
  [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

CREATE TABLE [dbo].CarDriverWITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[CarDriver]  WITH CHECK ADD  CONSTRAINT [FK_CarDriver_Cars_CarsId] FOREIGN KEY([CarsId])
REFERENCES [dbo].[Cars] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[CarDriver] CHECK CONSTRAINT [FK_CarDriver_Cars_CarsId]
GO
ALTER TABLE [dbo].[CarDriver]  WITH CHECK ADD  CONSTRAINT [FK_CarDriver_Drivers_DriversId] FOREIGN KEY([DriversId])
REFERENCES [dbo].[Drivers] ([Id])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[CarDriver] CHECK CONSTRAINT [FK_CarDriver_Drivers_DriversId]
GO

请注意,复合主键、检查约束(外键)和级联行为都是由 EF Core 创建的,以确保将CarDriver表配置为正确的连接表。

Note

在撰写本文时,还不支持搭建多对多关系。多对多关系是基于表结构搭建的,如第二个示例中的CarDriver实体。这里正在跟踪的问题: https://github.com/dotnet/efcore/issues/22475

级联行为

大多数数据存储(如 SQL Server)都有规则来控制删除行时的行为。如果相关(从属)记录也被删除,这被称为级联删除。在 EF Core 中,当删除主体实体(内存中加载了相关实体)时,会发生三种操作。

  • 相关记录被删除。

  • 相关外键被设置为 null。

  • 从属实体保持不变。

可选关系和必需关系的默认行为是不同的。行为也可以配置为七个值中的一个,尽管只推荐使用五个。使用 Fluent API 通过DeleteBehavior枚举配置行为。枚举中可用的选项如下所示:

  • Cascade

  • ClientCascade

  • ClientNoAction(不推荐使用)

  • ClientSetNull

  • NoAction(不推荐使用)

  • SetNull

  • Restrict

在 EF Core 中,只有在删除了一个实体并且在派生的DbContext上调用了SaveChanges()之后,才会触发指定的行为。有关 EF Core 何时与数据存储交互的更多详细信息,请参见“查询执行”一节。

可选关系

回想一下表 22-4 中的可选关系,依赖实体可以将外键值设置为空。对于可选关系,默认行为是ClientSetNull。表 22-5 显示了使用 SQL Server 时依赖实体的级联行为以及对数据库记录的影响。

表 22-5。

具有可选关系的级联行为

|

删除行为

|

对受抚养人的影响(在内存中)

|

对受抚养人的影响(在数据库中)

|
| — | — | — |
| Cascade | 实体被删除。 | 实体被数据库删除。 |
| ClientCascade | 实体被删除。 | 对于不支持级联删除的数据库,EF Core 会删除实体。 |
| ClientSetNull(默认) | 外键属性设置为空。 | 没有。 |
| SetNull | 外键属性设置为空。 | 外键属性设置为空。 |
| Restrict | 没有。 | 没有。 |

所需的关系

所需的关系是依赖实体不能将外键值设置为空。对于必需的关系,默认行为是Cascade。表 22-6 显示了使用 SQL Server 时依赖实体的级联行为以及对数据库记录的影响。

表 22-6。

具有所需关系的级联行为

|

删除行为

|

对受抚养人的影响(在内存中)

|

对受抚养人的影响(在数据库中)

|
| — | — | — |
| Cascade(默认) | 实体被删除。 | 实体被删除。 |
| ClientCascade | 实体被删除。 | 对于不支持级联删除的数据库,EF Core 会删除实体。 |
| ClientSetNull | SaveChanges抛出异常。 | 没有。 |
| SetNull | SaveChanges抛出异常。 | SaveChanges抛出异常。 |
| Restrict | 没有。 | 没有。 |

实体约定

EF Core 使用许多约定来定义一个实体以及它与数据存储的关系。除非被 Fluent API 中的数据注释或代码否决,否则这些约定将始终启用。表 22-7 列出了一些更重要的 EF 核心惯例。

表 22-7。

EF 的一些核心惯例

|

惯例

|

生命的意义

|
| — | — |
| 包含的表格 | 所有具有DbSet属性的类和所有能够被DbSet类访问(通过导航属性)的类都在数据库中创建。 |
| 包含的列 | 所有带有 getter 和 setter 的公共属性(包括自动属性)都被映射到列。 |
| 表名 | 映射到派生的DbContext中的DbSet属性名。如果不存在DbSet,则使用类名。 |
| 计划 | 表是在数据存储的默认模式中创建的(在 SQL Server 上为dbo)。 |
| 列名 | 列名映射到类的属性名。 |
| 列数据类型 | 数据类型是根据。NET 核心数据类型,并由数据库提供程序(SQL Server)转换。DateTime映射到datetime2(7),string映射到nvarchar(max)。字符串作为主键的一部分映射到nvarchar(450)。 |
| 列为空性 | 不可为 Null 的数据类型被创建为 Not Null 持久性列。EF 核心支持 C# 8 可空性。 |
| 主关键字 | 名为Id<EntityTypeName>Id的属性将被配置为主键。类型为shortintlongGuid的键具有由数据存储器控制的值。数值被创建为标识列(SQL Server)。 |
| 关系 | 当两个实体类之间有导航属性时,就创建了表之间的关系。 |
| 外键 | 名为<OtherClassName>Id的属性是类型为<OtherClassName>的导航属性的外键。 |

前面的导航属性示例都利用 EF 核心约定来构建表之间的关系。

将属性映射到列

按照约定,公共读写属性映射到同名的列。数据类型与属性的 CLR 数据类型的数据存储区等效项相匹配。不可为空的属性在数据存储中设置为 not null,可为空的属性设置为允许 null。EF 核心支持在 C# 8 中引入的可空引用类型。

对于支持字段,EF Core 希望使用以下约定之一来命名支持字段(按优先顺序排列):

  • _<camel-cased property name>

  • _<property name>

  • m_<camel-cased property name>

  • m_<property name>

如果Car类的Color属性被更新为使用后备字段,则(按照惯例)需要将其命名为_color_Colorm_colorm_Color之一,如下所示:

private string _color = "Gold";
public string Color
{
  get => _color;
  set => _color = value;
}

实体框架数据注释

数据注释是 C# 属性,用于进一步塑造您的实体。表 22-8 列出了一些最常用的数据注释,用于定义实体类和属性如何映射到数据库表和字段。数据注释会覆盖任何冲突的约定。正如您将在本章和本书的其余部分中看到的,您可以使用更多的注释来细化模型中的实体。

表 22-8。

实体框架核心支持的一些数据注释(* EF Core 5 中的新属性)

|

数据注释

|

生命的意义

|
| — | — |
| Table | 定义实体的模式和表名。 |
| Keyless* | 指示实体没有键(例如,表示数据库视图)。 |
| Column | 定义实体属性的列名。 |
| BackingField* | 指定属性的 C# 支持字段。 |
| Key | 定义实体的主键。关键字段也是隐式的[Required]。 |
| Index* | 放置在类上以指定单列或多列索引。允许指定索引是唯一的。 |
| Owned | 声明该类将由另一个实体类拥有。 |
| Required | 将属性声明为在数据库中不可为空。 |
| ForeignKey | 声明一个用作导航属性的外键的属性。 |
| InverseProperty | 在关系的另一端声明导航属性。 |
| StringLength | 指定字符串属性的最大长度。 |
| TimeStamp | 在 SQL Server 中将类型声明为rowversion,并向涉及实体的数据库操作添加并发检查。 |
| ConcurrencyCheck | 执行更新和删除时用于并发检查的标志字段。 |
| DatabaseGenerated | 指定字段是否由数据库生成。取ComputedIdentityNoneDatabaseGeneratedOption值。 |
| DataType | 提供比内部数据类型更具体的字段定义。 |
| NotMapped | 排除与数据库字段和表相关的属性或类。 |

下面的代码显示了带有注释的BaseEntity类,该注释将Id字段声明为主键。属性Id上的第二个数据注释表明它是 SQL Server 中的一个标识列。TimeStamp属性将是一个 SQL Server timestamp / rowversion属性(用于并发检查,将在本章后面介绍)。

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public abstract class BaseEntity
{
  [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
  public int Id { get; set; }
  [Timestamp]
  public byte[] TimeStamp { get; set; }
}

下面是数据库中的Car类和塑造它的数据注释:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

[Table("Inventory", Schema="dbo")]
[Index(nameof(MakeId), Name = "IX_Inventory_MakeId")]
public class Car : BaseEntity
{
  [Required, StringLength(50)]
  public string Color { get; set; }
  [Required, StringLength(50)]
  public string PetName { get; set; }
  public int MakeId { get; set; }
  [ForeignKey(nameof(MakeId))]
  public Make MakeNavigation { get; set; }
  [InverseProperty(nameof(Driver.Cars))]
  public IEnumerable<Driver> Drivers { get; set; }
}

Table属性将Car类映射到dbo模式中的Inventory表(Column属性用于更改列名或数据类型)。属性在外键MakeId上创建一个索引。两个文本字段被设置为Required和最多 50 个字符的StringLength。下一节将解释InversePropertyForeignKey属性。

EF 核心惯例的变化如下:

  • 将表格从Cars重命名为Inventory

  • TimeStamp列从varbinary(max)更改为 SQL Server 时间戳数据类型

  • ColorPetName列的数据类型和可空性从nvarchar(max) /null 设置为nvarchar(50) /not null

  • 重命名MakeId上的索引

使用的其余注释与 EF 核心约定定义的配置相匹配。

如果您要创建迁移并尝试应用它,迁移将会失败。SQL Server 不允许将现有列从另一种数据类型更改为 timestamp 数据类型。必须删除并重新创建该列。不幸的是,迁移基础设施不能丢弃和重新创建。它试图改变列。

解决这个问题最简单的方法是注释掉基本实体上的TimeStamp属性,创建并应用一个迁移,然后取消对TimeStamp的注释,创建并应用另一个迁移。

注释掉TimeStamp属性和数据注释,并执行以下命令:

dotnet ef migrations add RemoveTimeStamp -o Migrations -c AutoLot.Samples.ApplicationDbContext
dotnet ef database update RemoveTimeStamp  -c AutoLot.Samples.ApplicationDbContext

取消对TimeStamp属性和数据注释的注释,并运行这些命令将属性作为timestamp列添加到每个表中:

dotnet ef migrations add ReplaceTimeStamp -o Migrations -c AutoLot.Samples.ApplicationDbContext
dotnet ef database update ReplaceTimeStamp  -c AutoLot.Samples.ApplicationDbContext

现在,您的数据库与您的模型相匹配。

注释和导航属性

ForeignKey注释让 EF Core 知道哪个属性是导航属性的支持字段。按照惯例,<TypeName>Id将被自动设置为外键属性,但在前面的示例中,它是显式设置的。这支持不同的命名风格,以及同一个表有多个外键。它也(在我看来)增加了代码的可读性。

InverseProperty通过指示导航回该实体的其他实体的导航属性,告知 EF Core 这些表是如何关联的。当一个实体不止一次地与另一个实体相关时,需要使用InverseProperty,这也(再次,以我的诚实观点)使代码更可读。

流畅的 API

Fluent API 通过 C# 代码配置应用实体。这些方法由在DbContext OnModelCreating()方法中可用的ModelBuilder实例公开。Fluent API 是最强大的配置方法,可以覆盖任何冲突的约定或数据注释。一些配置选项仅在使用 Fluent API 时可用,例如为导航属性设置默认值和级联行为。

类别和属性映射

下面的代码显示了前面的Car示例,其中 Fluent API 相当于所使用的数据注释(省略了导航属性,这将在接下来讨论)。

modelBuilder.Entity<Car>(entity =>
{
  entity.ToTable("Inventory","dbo");
  entity.HasKey(e=>e.Id);
  entity.HasIndex(e => e.MakeId, "IX_Inventory_MakeId");
  entity.Property(e => e.Color)
    .IsRequired()
    .HasMaxLength(50);
  entity.Property(e => e.PetName)
    .IsRequired()
    .HasMaxLength(50);
  entity.Property(e => e.TimeStamp)
    .IsRowVersion()
    .IsConcurrencyToken();
});

如果您现在创建并运行迁移,您会发现没有任何变化,因为 Fluent API 中的命令与约定和数据注释定义的当前配置相匹配。

默认值

Fluent API 提供了为列设置默认值的方法。默认值可以是值类型或 SQL 字符串。例如,要将新Car的默认Color设置为Black,请使用以下命令:

modelBuilder.Entity<Car>(entity =>
{
...
  entity.Property(e => e.Color)
  .HasColumnName("CarColor")
  .IsRequired()
  .HasMaxLength(50)
  .HasDefaultValue("Black");
});

要将值设置为数据库函数(如getdate()),请使用HasDefaultValueSql()方法。假设一个名为DateBuiltDateTime属性已经被添加到Car类中,默认值应该是使用 SQL Server getdate()方法的当前日期。列的配置如下:

modelBuilder.Entity<Car>(entity =>
{
...
  entity.Property(e => e.DateBuilt)
  .HasDefaultValueSql("getdate()");
});

就像使用 SQL 插入记录一样,如果在 EF Core 插入记录时映射到具有默认值的列的属性具有值,则使用该属性的值而不是默认值。如果属性值为 null,则使用列的默认值。

当属性的数据类型有默认值时,就会出现问题。回想一下,数字默认为零,布尔默认为 false。如果您将数字属性的值设置为零或将布尔属性的值设置为 false,然后插入该实体,EF Core 会将该属性视为没有设置值的*。如果该属性映射到具有默认值的列,则使用列定义中的默认值。*

例如,向Car类添加一个名为IsDrivablebool属性。将属性的列映射的默认值设置为true

//Car.cs
public class Car : BaseEntity
{
...
  public bool IsDrivable { get; set; }
}

//ApplicationDbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<Car>(entity =>
  {
...
  entity.Property(e => e.IsDrivable).HasDefaultValue(true);
});

当用IsDrivable = false保存新的 a 记录时,该值将被忽略(因为它是布尔值的默认值),将使用数据库默认值。这意味着IsDrivable的值将始终为真!对此的一个解决方案是使您的公共属性(以及列)可为空,但是这可能不符合业务需求。

另一个解决方案是由 EF Core 及其对后台字段的支持提供的。回想一下前面的内容,如果支持字段存在(并且通过约定、数据注释或 Fluent API 被标识为属性的 backfield),那么 EF Core 将使用支持字段进行读写操作,而不是公共属性。

如果你更新IsDrivable使用一个可空的后备字段(但是保持属性不可空),ER Core 将从后备字段而不是属性中读写。可空布尔值的默认值是 null,而不是 false。这一更改现在使属性按预期工作。

public class Car
{
...
private bool? _isDrivable;
public bool IsDrivable
{
  get => _isDrivable ?? true;
  set => _isDrivable = value;
}

Fluent API 用于通知 EF 核心支持字段。

modelBuilder.Entity<Car>(entity =>
{
  entity.Property(p => p.IsDrivable)
    .HasField("_isDrivable")
    .HasDefaultValue(true);
});

Note

在这个例子中,HasField()方法不是必需的,因为支持字段的名称遵循命名约定。我把它包括进来是为了展示如何使用 Fluent API 来设置它。

EF Core 将该字段转换为以下 SQL 定义:

CREATE TABLE [dbo].Inventory)) FOR [IsDrivable]
GO

计算列

还可以将列设置为基于数据存储的功能进行计算。对于 SQL Server,有两种选择:根据同一记录中其他字段的值计算值,或者使用标量函数。例如,要在Inventory表上创建一个计算列,该列组合了PetNameColor值以创建一个DisplayName,请使用HasComputedColumnSql()函数。

modelBuilder.Entity<Car>(entity =>
{
  entity.Property(p => p.FullName)
    .HasComputedColumnSql("[PetName] + ' (' + [Color] + ')'");
});

EF Core 5 中的新特性是,计算出的值可以持久化,因此该值只在创建或更新行时计算。虽然 SQL Server 支持这一点,但并非所有数据存储都支持,因此请查阅数据库提供商的文档。

modelBuilder.Entity<Car>(entity =>
{
  entity.Property(p => p.FullName)
    .HasComputedColumnSql("[PetName] + ' (' + [Color] + ')'", stored:true);

});

一对多关系

要使用 Fluent API 来定义一对多关系,选择要更新的实体中的一个。导航链的两端都在一个代码块中设置。

modelBuilder.Entity<Car>(entity =>
{
...
  entity.HasOne(d => d.MakeNavigation)
    .WithMany(p => p.Cars)
    .HasForeignKey(d => d.MakeId)
    .OnDelete(DeleteBehavior.ClientSetNull)
    .HasConstraintName("FK_Inventory_Makes_MakeId");
});

如果选择主体实体作为导航属性配置的基础,则代码如下所示:

modelBuilder.Entity<Make>(entity =>
{
...
  entity.HasMany(e=>e.Cars)
    .WithOne(c=>c.MakeNavigation)
    .HasForeignKey(c=>c.MakeId)
    .OnDelete(DeleteBehavior.ClientSetNull)
    .HasConstraintName("FK_Inventory_Makes_MakeId");
 });

一对一的关系

一对一关系的配置方式相同,只是使用了WithOne() Fluent API 方法而不是WithMany()。向依赖实体添加唯一索引。下面是使用依赖实体(Radio)的CarRadio实体之间的关系代码:

modelBuilder.Entity<Radio>(entity =>
{
  entity.HasIndex(e => e.CarId, "IX_Radios_CarId")
    .IsUnique();

  entity.HasOne(d => d.CarNavigation)
    .WithOne(p => p.RadioNavigation)
    .HasForeignKey<Radio>(d => d.CarId);
});

如果关系是在主体实体上定义的,则唯一索引仍会添加到依赖实体中。下面是使用关系的主体实体的CarRadio实体之间的关系代码:

modelBuilder.Entity<Radio>(entity =>
{
  entity.HasIndex(e => e.CarId, "IX_Radios_CarId")
    .IsUnique();
});

modelBuilder.Entity<Car>(entity =>
{
  entity.HasOne(d => d.RadioNavigation)
    .WithOne(p => p.CarNavigation)
    .HasForeignKey<Radio>(d => d.CarId);
});

多对多关系

使用 Fluent API 可以更好地定制多对多关系。外键字段名称、索引名称和级联行为都可以在定义关系的语句中设置。下面是前面使用 Fluent API 复制的多对多关系示例(更改了键和列名以使它们更具可读性):

modelBuilder.Entity<Car>()
  .HasMany(p => p.Drivers)
  .WithMany(p => p.Cars)
  .UsingEntity<Dictionary<string, object>>(
    "CarDriver",
    j => j
      .HasOne<Driver>()
      .WithMany()
      .HasForeignKey("DriverId")
      .HasConstraintName("FK_CarDriver_Drivers_DriverId")
      .OnDelete(DeleteBehavior.Cascade),
    j => j
      .HasOne<Car>()
      .WithMany()
      .HasForeignKey("CarId")
      .HasConstraintName("FK_CarDriver_Cars_CarId")
      .OnDelete(DeleteBehavior.ClientCascade));

约定、注释和流畅的 API,天哪!

在本章的这一点上,您可能想知道使用三个选项中的哪一个来塑造您的实体以及它们彼此之间和与数据存储的关系。答案是三者皆有。这些约定总是有效的(除非您用数据注释或 Fluent API 覆盖它们)。数据注释几乎可以做 Fluent API 方法可以做的所有事情,并将信息保存在实体类本身中,这可以增加代码的可读性和支持。Fluent API 是所有三个 API 中最强大的,但是代码隐藏在DbContext类中。无论您使用数据注释还是 Fluent API,都要知道数据注释否决了内置约定,而 Fluent API 的方法否决了一切。

查询执行

数据检索查询是用针对DbSet<T>属性编写的 LINQ 查询创建的。数据库提供商的 LINQ 翻译引擎将 LINQ 查询转换为特定于数据库的语言(例如,T-SQL ),并在服务器端执行。多记录(或潜在的多记录)LINQ 查询直到该查询被迭代(例如,使用foreach)或被绑定到用于显示的控件(像数据网格)时才被执行。这种延迟执行允许在代码中构建查询,而不会因为与数据库的对话而出现性能问题。

例如,要从数据库中获取所有黄色的Car记录,请执行以下查询:

var cars = Context.Cars.Where(x=>x.Color == "Yellow");

对于延迟执行,在结果被迭代之前,不会真正查询该数据库。要立即执行查询,请使用ToList()

var cars = Context.Cars.Where(x=>x.Color == "Yellow").ToList();

因为查询在被触发之前不会被执行,所以它们可以在多行代码中构建。下面的代码示例与前面的示例执行相同:

var query = Context.Cars.AsQueryable();
query = query.Where(x=>x.Color == "Yellow");
var cars = query.ToList();

单记录查询(如使用First() / FirstOrDefault()时)在调用动作(如FirstOrDefault())时立即执行,create、update 和 delete 语句在执行DbContext.SaveChanges()方法时立即执行。

混合客户端-服务器评估

EF Core 的早期版本引入了混合服务器端和客户端执行的能力。这意味着 C# 函数可以用在 LINQ 语句的中间,从本质上否定我在上一段中描述的内容。直到 C# 函数的部分将在服务器端执行,但是所有的结果(在查询时)将被带回客户端,然后查询的其余部分将作为对象的 LINQ 执行。这最终导致了比它所解决的更多的问题,随着 EF Core 3.1 的发布,这个功能被改变了。现在,只有 LINQ 语句的最后一个节点可以在客户端执行。

跟踪与非跟踪查询

当数据从数据库读入一个DbSet<T>实例时,实体(默认情况下)被变更跟踪器跟踪。这通常是您在应用中想要的。一旦实例被更改跟踪器跟踪,对同一项(基于主键)的数据库的任何进一步调用将导致该项的更新,而不是重复。

然而,有时您可能需要从数据库中获取一些数据,但是您不希望它被更改跟踪器跟踪。原因可能是性能(跟踪大量记录的原始值和当前值会增加内存压力),也可能是您知道那些记录永远不会被需要数据的应用部分更改。

要将数据加载到一个DbSet<T>实例中而不将数据添加到ChangeTracker中,请将AsNoTracking()添加到 LINQ 语句中。这向 EF 内核发出信号以检索数据,而不将其添加到ChangeTracker中。例如,要加载一条Car记录而不将其添加到ChangeTracker中,执行以下命令:

public virtual Car? FindAsNoTracking(int id)
  => Table.AsNoTracking().FirstOrDefault(x => x.Id == id);

这样做的好处是不会增加潜在的内存压力,但也有潜在的缺点:检索同一个Car的额外调用会创建记录的额外副本。以使用更多内存和稍慢的执行时间为代价,可以修改查询以确保只有一个未映射的Car实例。

public virtual Car? FindAsNoTracking(int id)
  => Table.AsNoTrackingWithIdentityResolution().FirstOrDefault(x => x.Id == id);

英孚的显著核心特征

EF 6 的许多特性在 EF Core 中得到了复制,并且在每个版本中都增加了更多的特性。EF Core 中的许多功能在功能和性能上都有了巨大的改进。除了复制 EF 6 的功能之外,EF Core 还有许多新功能,是以前版本中没有的。以下是 EF Core 中一些比较值得注意的特性(排名不分先后)。

Note

本节中的代码样本直接来自您将在下一章构建的完整的AutoLot数据访问库。

处理数据库生成的值

除了变更跟踪和从 LINQ 生成 SQL 查询之外,与原始 ADO.NET 相比,使用 EF Core 的一个显著优势是无缝处理数据库生成的值。添加或更新实体后,EF Core 会查询任何数据库生成的数据,并自动使用正确的值更新实体。在原始的 ADO.NET,你需要自己去做。

例如,Inventory表有一个在 SQL Server 中定义为标识列的整数主键。当添加记录时,标识列由 SQL Server 使用唯一的编号(来自序列)填充,并且在正常更新期间不允许更新(不包括启用identity insert的特殊情况)。另外,Inventory表有一个用于并发检查的Timestamp列。接下来将讨论并发检查,但是现在只需要知道Timestamp列是由 SQL Server 维护的,并在任何添加或编辑操作时更新。

例如,向Inventory表中添加一个新的Car。下面的代码创建一个新的Car实例,将其添加到派生的DbContext上的DbSet<Car>实例中,并调用SaveChanges()来保存数据:

var car = new Car
{
  Color = "Yellow",
  MakeId = 1,
  PetName = "Herbie"
};
Context.Cars.Add(car);
Context.SaveChanges();

当执行SaveChanges时,新记录被插入到表中,然后IdTimestamp值从表中返回到 EF Core,实体的属性相应地被更新。

INSERT INTO [Dbo].[Inventory] ([Color], [MakeId], [PetName])
VALUES (N'Yellow', 1, N'Herbie');
SELECT [Id], [TimeStamp]
FROM [Dbo].[Inventory]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

Note

EF Core 实际上执行参数化查询,但是为了可读性,我简化了所有的例子。

这也适用于向数据库中添加多个项目的情况。EF 核心知道如何将价值与正确的实体联系起来。当更新记录时,主键值是已知的,所以在我们的Car示例中,只查询并返回更新后的Timestamp值。

并发检查

当两个独立的进程(用户或系统)试图几乎同时更新同一条记录时,就会出现并发问题。例如,用户 1 和用户 2 都获得了客户 a 的数据。用户 1 更新了地址并保存了更改。用户 2 更新信用评级并试图保存相同的记录。如果用户 2 的保存起作用,来自用户 1 的更改将被恢复,因为地址是在用户 2 检索到记录后更改的。另一个选项是对用户 2 的保存失败,在这种情况下,用户 1 的更改会被持久化,但用户 2 的更改不会。

如何处理这种情况取决于应用的需求。解决方案从什么都不做(第二次更新覆盖第一次更新)到使用开放式并发(第二次更新失败)到更复杂的解决方案,如检查单个字段。除了选择什么都不做(普遍认为这是一个糟糕的编程想法),开发人员需要知道并发问题何时出现,以便可以适当地处理它们。

幸运的是,许多现代数据库都有工具来帮助开发团队处理并发问题。SQL Server 有一个名为timestamp的内置数据类型,是rowversion的同义词。如果一个列被定义为数据类型为timestamp,当一条记录被添加到数据库时,该列的值由 SQL Server 创建,当一条记录被更新时,该列的值也被更新。该值实际上保证是唯一的,并由 SQL Server 控制。

EF Core 可以通过在实体上实现一个Timestamp属性(在 C# 中表示为byte[])来利用 SQL Server 时间戳数据类型。当更新或删除记录时,用Timestamp属性或 Fluent API 名称定义的实体属性被添加到where子句中。生成的 SQL 并不仅仅使用主键值,而是将时间戳属性的值添加到where子句中。这将结果限制为主键和时间戳值匹配的那些记录。如果另一个用户(或系统)更新了记录,时间戳值将不匹配,并且updatedelete语句不会更新记录。下面是一个使用Timestamp列的更新查询示例:

UPDATE [Dbo].[Inventory] SET [Color] = N'Yellow'
WHERE [Id] = 1 AND [TimeStamp] = 0x000000000000081F;

当数据存储报告受影响的记录数量不同于ChangeTracker预期要更改的记录数量时,EF Core 抛出一个DbUpdateConcurrencyException并回滚整个事务。DbUpdateConcurrencyException包含所有没有保存的记录的信息,包括原始值(从数据库加载实体时)和当前值(用户/系统更新它们时)。还有一个获取当前数据库值的方法(这需要再次调用服务器)。有了这些丰富的信息,开发人员就可以按照应用的要求处理并发错误。下面的代码展示了这一点:

try
{
  //Get a car record (doesn’t matter which one)
  var car = Context.Cars.First();
  //Update the database outside of the context
  Context.Database.ExecuteSqlInterpolated($"Update dbo.Inventory set Color="Pink" where Id = {car.Id}");
  //update the car record in the change tracker and then try and save changes
  car.Color = "Yellow";
  Context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
  //Get the entity that failed to update
  var entry = ex.Entries[0];
  //Get the original values (when the entity was loaded)
  PropertyValues originalProps = entry.OriginalValues;
  //Get the current values (updated by this code path)
  PropertyValues currentProps = entry.CurrentValues;
  //get the current values from the data store –
  //Note: This needs another database call
  //PropertyValues databaseProps = entry.GetDatabaseValues();
}

连接弹性

暂时性错误很难调试,更难复制。幸运的是,许多数据库提供商有一个内置的重试机制,可以处理数据库系统中的故障(tempdb问题、用户限制等)。)可以被 EF Core 利用。对于 SQL Server,SqlServerRetryingExecutionStrategy捕获暂时的错误(由 SQL Server 团队定义),如果在派生的DbContextDbContextOptions上启用,EF Core 会自动重试操作,直到达到最大重试限制。

对于 SQL Server,有一个快捷方法可以用来启用所有默认的SqlServerRetryingExecutionStrategy。与SqlServerOptions一起使用的方法是EnableRetryOnFailure(),此处演示:

public ApplicationDbContext CreateDbContext(string[] args)
{
  var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
  var connectionString = @"server=.,5433;Database=AutoLot50;User Id=sa;Password=P@ssw0rd;";
  optionsBuilder.UseSqlServer(connectionString, options => options.EnableRetryOnFailure());
  return new ApplicationDbContext(optionsBuilder.Options);
}

最大重试次数和重试之间的时间限制可以根据应用的要求进行配置。如果在操作没有完成的情况下达到重试限制,EF Core 将通过抛出RetryLimitExceededException来通知应用连接问题。当由开发人员处理时,该异常可以将相关信息传递给用户,从而提供更好的体验。

try
{
  Context.SaveChanges();
}
catch (RetryLimitExceededException ex)
{
  //A retry limit error occurred
  //Should handle intelligently
  Console.WriteLine($"Retry limit exceeded! {ex.Message}");
}

对于不提供内置执行策略的数据库提供者,也可以创建定制的执行策略。更多信息请参考 EF 核心文档: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency

相关资料

实体导航属性用于加载实体的相关数据。相关数据可以被急切地加载(一个 LINQ 语句,一个 SQL 查询),急切地使用拆分查询(一个 LINQ 语句,多个 SQL 查询),显式地加载(多个 LINQ 调用,多个 SQL 查询),或者懒惰地加载(一个 LINQ 语句,多个按需 SQL 查询)。

除了使用导航属性加载相关数据的能力之外,EF Core 还将在实体被加载到变更跟踪器时自动修复实体。例如,假设所有的Make记录都被加载到DbSet<Make>中。接下来,所有的Car记录都被加载到DbSet<Car>中。尽管这些记录是分别加载的,但是它们可以通过导航属性相互访问。

急切装载

急切加载是在一次数据库调用中从多个表中加载相关记录的术语。这类似于在 T-SQL 中创建一个用连接链接两个或多个表的查询。当实体具有导航属性并且这些属性在 LINQ 查询中使用时,翻译引擎使用联接从相关表中获取数据并加载相应的实体。这通常比执行一个查询从一个表中获取数据,然后对每个相关的表运行额外的查询要有效得多。对于那些使用一个查询效率较低的时候,EF Core 5 引入了查询拆分,这将在下一篇文章中介绍。

Include()ThenInclude()(用于后续导航属性)方法用于遍历 LINQ 查询中的导航属性。如果需要该关系,LINQ 翻译引擎将创建一个内部连接。如果关系是可选的,翻译引擎将创建左连接。

例如,要加载所有的Car记录及其相关的Make信息,请执行以下 LINQ 查询:

var queryable = Context.Cars.IgnoreQueryFilters().Include(c => c.MakeNavigation).ToList();

前面的 LINQ 对数据库执行以下查询:

SELECT [i].[Id], [i].[Color], [i].[MakeId], [i].[PetName], [i].[TimeStamp],
  [m].[Id], [m].[Name], [m].[TimeStamp]
FROM [Dbo].[Inventory] AS [i]
INNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id]

可以在同一个查询中使用多个Include()语句将多个实体连接到原始实体。要向下操作导航属性树,在Include()后使用ThenInclude()。例如,要获得所有的Cars记录及其相关的MakeOrder信息以及与Order相关的Customer信息,使用以下语句:

var cars = Context.Cars.Where(c => c.Orders.Any())
  .Include(c => c.MakeNavigation)
  .Include(c => c.Orders).ThenInclude(o => o.CustomerNavigation).ToList();

过滤包括

EF Core 5 中的新功能是可以对包含的数据进行过滤和排序。收藏导航允许的操作有Where()OrderBy()OrderByDescending()ThenBy()ThenByDescending()Skip()Take()。例如,如果您想要获取所有的Make记录,但是只获取颜色为黄色的相关Car记录,您可以在 lambda 表达式中过滤 navigation 属性,如下所示:

var query = Context.Makes
    .Include(x => x.Cars.Where(x=>x.Color == "Yellow")).ToList();

执行的查询如下:

SELECT [m].[Id], [m].[Name], [m].[TimeStamp], [t].[Id], [t].[Color],
              [t].[MakeId], [t].[PetName], [t].[TimeStamp]
FROM [dbo].[Makes] AS [m]
LEFT JOIN (
        SELECT [i].[Id], [i].[Color], [i].[MakeId], [i].[PetName], [i].[TimeStamp]
        FROM [Dbo].[Inventory] AS [i]
        WHERE [i].[Color] = N'Yellow') AS [t] ON [m].[Id] = [t].[MakeId]
ORDER BY [m].[Id], [t].[Id]

使用拆分查询进行快速加载

当 LINQ 查询包含大量包含时,可能会对性能产生负面影响。为了解决这种情况,EF Core 5 引入了拆分查询。EF 核心不是执行单个查询,而是将 LINQ 查询拆分成多个 SQL 查询,然后连接所有相关数据。例如,通过将AsSplitQuery()添加到 LINQ 查询中,可以将前面的查询预期为多个 SQL 查询,如下所示:

var query = Context.Makes.AsSplitQuery()
  .Include(x => x.Cars.Where(x=>x.Color == "Yellow")).ToList();

执行的查询如下所示:

SELECT [m].[Id], [m].[Name], [m].[TimeStamp]
FROM [dbo].[Makes] AS [m]
ORDER BY [m].[Id]

SELECT [t].[Id], [t].[Color], [t].[MakeId], [t].[PetName], [t].[TimeStamp], [m].[Id]
FROM [dbo].[Makes] AS [m]
INNER JOIN (
    SELECT [i].[Id], [i].[Color], [i].[MakeId], [i].[PetName], [i].[TimeStamp]
    FROM [Dbo].[Inventory] AS [i]
    WHERE [i].[Color] = N'Yellow'
) AS [t] ON [m].[Id] = [t].[MakeId]
ORDER BY [m].[Id]

使用拆分查询有一个缺点:如果在执行查询之间数据发生了变化,那么返回的数据将会不一致。

显式加载

显式加载是在已经加载了核心对象之后沿着导航属性加载数据。这个过程包括执行一个额外的数据库调用来获取相关数据。如果您的应用需要有选择地获取相关记录,而不是基于某些用户操作提取所有相关记录,这可能会很有用。

这个过程从一个已经加载的实体开始,并在派生的DbContext上使用Entry()方法。当查询参考导航属性时(例如,获取汽车的Make信息),使用Reference()方法。当查询集合导航属性时,使用Collection()方法。查询被推迟,直到执行Load()ToList()或聚合函数(例如Count()Max())。

以下示例显示了如何获取相关的Make数据以及Car记录的任何Orders:

//Get the Car record
var car = Context.Cars.First(x => x.Id == 1);
//Get the Make information
Context.Entry(car).Reference(c => c.MakeNavigation).Load();
//Get any orders the Car is related to
Context.Entry(car).Collection(c => c.Orders).Query().IgnoreQueryFilters().Load();

惰性装载

当导航属性用于访问尚未加载到内存中的相关记录时,延迟加载是按需加载记录。延迟加载是 EF 6 的一个特性,在 2.1 版中被添加到 EF Core 中。虽然打开它听起来是个好主意,但是启用延迟加载会导致潜在的不必要的数据库往返,从而导致应用的性能问题。因此,在 EF 内核中,延迟加载是默认关闭的(在 EF 6 中,它是默认启用的)。

延迟加载在智能客户端(WPF、WinForms)应用中很有用,但建议不要在 web 或服务应用中使用。因此,我不打算在本文中讨论延迟加载。如果你想了解更多关于延迟加载的知识,以及如何在 EF Core 中使用它,请参考这里的文档: https://docs.microsoft.com/en-us/ef/core/querying/related-data/lazy

全局查询过滤器

全局查询过滤器允许将一个where子句添加到特定实体的所有 LINQ 查询中。例如,一种常见的数据库设计模式是使用软删除而不是硬删除。表中会添加一个字段来指示记录的删除状态。如果记录被“删除”,则该值被设置为 true(或 1),但不会从数据库中删除。这叫做软删除。为了从正常操作中过滤出被软删除的记录,每个where子句都必须检查该字段的值。如果没有问题的话,记住在每个查询中包含这个过滤器是很费时间的。

EF Core 支持向实体添加一个全局查询过滤器,然后应用于涉及该实体的每个查询。对于前面描述的软删除示例,您在实体类上设置了一个过滤器来排除被软删除的记录。EF 核心创建的任何涉及具有全局查询过滤器的实体的查询都将应用其过滤器。您不再需要记住在每个查询中包含where子句。

与本书的Car主题保持一致,假设所有不可驱动的Car记录都应该从正常查询中过滤掉。使用 Fluent API,您可以像这样添加一个全局查询过滤器:

modelBuilder.Entity<Car>(entity =>
{
  entity.HasQueryFilter(c => c.IsDrivable == true);
  entity.Property(p => p.IsDrivable).HasField("_isDrivable").HasDefaultValue(true);
});

有了全局查询过滤器,涉及Car实体的查询将自动过滤掉不可驾驶的汽车。例如,执行以下 LINQ 查询:

var cars = Context.Cars.ToList();

执行以下 SQL:

SELECT [i].[Id], [i].[Color], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].[TimeStamp]
FROM [Dbo].[Inventory] AS [i]
WHERE [i].[IsDrivable] = CAST(1 AS bit)

如果需要查看过滤后的记录,将IgnoreQueryFilters()添加到 LINQ 查询中,这将禁用 LINQ 查询中每个实体的全局查询过滤器。执行以下 LINQ 查询:

var cars = Context.Cars.IgnoreQueryFilters().ToList();

执行以下 SQL:

SELECT [i].[Id], [i].[Color], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].[TimeStamp]
FROM [Dbo].[Inventory] AS [i]

值得注意的是,调用IgnoreQueryFilters()会删除 LINQ 查询中每个实体的查询过滤器,包括任何涉及Include()ThenInclude()语句的实体。

导航属性上的全局查询过滤器

还可以在导航属性上设置全局查询过滤器。假设您想要过滤掉任何包含不可驾驶的Car的订单。查询过滤器在Order实体的CarNavigation导航属性上创建,如下所示:

modelBuilder.Entity<Order>().HasQueryFilter(e => e.CarNavigation.IsDrivable);

执行标准 LINQ 查询时,任何包含不可驾驶汽车的订单都将从结果中排除。下面是 LINQ 语句和生成的 SQL 语句:

//C# Code
var orders = Context.Orders.ToList();

/* Generated SQL query */
SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp]
FROM [Dbo].[Orders] AS [o]
INNER JOIN (SELECT [i].[Id], [i].[IsDrivable]
                       FROM [Dbo].[Inventory] AS [i]
                       WHERE [i].[IsDrivable] = CAST(1 AS bit)) AS [t]
         ON [o].[CarId] = [t].[Id]
WHERE [t].[IsDrivable] = CAST(1 AS bit)

要删除查询过滤器,请使用IgnoreQueryFilters()。以下是更新后的 LINQ 语句和后续生成的 SQL:

//C# Code
var orders = Context.Orders.IgnoreQueryFilters().ToList();

/* Generated SQL query */
SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp]
FROM [Dbo].[Orders] AS [o]

这里需要注意的是:EF Core 不检测循环的全局查询过滤器,所以在向导航属性添加查询过滤器时要小心。

使用全局查询过滤器进行显式加载

当显式加载相关数据时,全局查询过滤器也是有效的。例如,如果你想加载一个MakeCar记录,IsDrivable过滤器将阻止不可驾驶的汽车被加载到内存中。以下面的代码片段为例:

var make = Context.Makes.First(x => x.Id == makeId);
Context.Entry(make).Collection(c=>c.Cars).Load();

到目前为止,生成的 SQL 查询包括不可驾驶汽车的过滤器就不足为奇了。

SELECT [i].[Id], [i].[Color], [i].[IsDrivable],
              [i].[MakeId], [i].[PetName], [i].[TimeStamp]
FROM [Dbo].[Inventory] AS [i]
WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] = 1

显式加载数据时忽略查询过滤器有一个小问题。由Collection()方法返回的类型是CollectionEntry<Make,Car>,并且没有显式实现IQueryable<T>接口。要调用IgnoreQueryFilters(),必须先调用Query(),它返回一个IQueryable<Car>

var make = Context.Makes.First(x => x.Id == makeId);
Context.Entry(make).Collection(c=>c.Cars).Query().IgnoreQueryFilters().Load();

当使用Reference()方法从引用导航属性中检索数据时,同样的过程也适用。

使用 LINQ 的原始 SQL 查询

有时,为复杂的查询获取正确的 LINQ 语句可能比直接编写 SQL 语句更难。幸运的是,EF Core 有一种机制允许在DbSet<T>上执行原始 SQL 语句。FromSqlRaw()FromSqlRawInterpolated()方法接受一个字符串,该字符串成为 LINQ 查询的基础。这个查询在服务器端执行。

如果原始 SQL 语句是非终止的(例如,既不是存储过程,也不是用户定义的函数,也不是使用公用表表达式或以分号结束的语句),则可以向查询中添加附加的 LINQ 语句。额外的 LINQ 语句,如Include()OrderBy()Where()子句,将与原始的原始 SQL 调用和任何全局查询过滤器相结合,整个查询在服务器端执行。

当使用其中一个FromSql变量时,必须使用数据存储模式和表名而不是实体名来编写查询。FromSqlRaw()将按原样发送字符串。FromSqlInterpolated()使用 C# 字符串插值,每个插值后的字符串在 SQL 参数中进行翻译。每当使用变量来增加参数化查询中固有的保护时,都应该使用插值版本。

假设在Car实体上设置了全局查询过滤器,下面的 LINQ 语句将获得第一条库存记录,其中Id为 1,包括相关的Make数据,并过滤掉不可驾驶的汽车:

var car = Context.Cars
  .FromSqlInterpolated($"Select * from dbo.Inventory where Id = {carId}")
  .Include(x => x.MakeNavigation)
  .First();

LINQ 到 SQL 转换引擎将原始 SQL 语句与 LINQ 语句的其余部分结合起来,并执行以下查询:

SELECT TOP(1) [c].[Id], [c].[Color], [c].[IsDrivable], [c].[MakeId],
                           [c].[PetName], [c].[TimeStamp],
                           [m].[Id], [m].[Name], [m].[TimeStamp]
FROM (Select * from dbo.Inventory where Id = 1) AS [c]
INNER JOIN [dbo].[Makes] AS [m] ON [c].[MakeId] = [m].[Id]
WHERE [c].[IsDrivable] = CAST(1 AS bit)

要知道,在 LINQ 中使用原始 SQL 时,有一些规则必须遵守。

  • SQL 查询必须返回实体类型的所有属性的数据。

  • 列名必须与它们被映射到的属性相匹配(这是对 EF 6 的一个改进,在 EF 6 中映射被忽略了)。

  • SQL 查询不能包含相关数据。

语句的批处理

EF Core 通过在一个或多个批处理中执行语句来保存对数据库的更改,从而显著提高了性能。这减少了应用和数据库之间的往返,提高了性能并潜在地降低了成本(例如,对于对事务收费的云数据库)。

EF 核心使用表值参数对 create、update 和 delete 语句进行批处理。EF 批处理的语句数量取决于数据库提供者。例如,对于 SQL Server,低于 4 条语句和高于 40 条语句时,批处理效率很低。不管批处理的数量是多少,所有语句仍然在一个事务中执行。批量大小也可以通过DbContextOptions配置,但是建议让 EF Core 计算大多数(如果不是全部)情况下的批量大小。

如果您要像这样在一次交易中插入四辆汽车:

var cars = new List<Car>
{
  new Car { Color = "Yellow", MakeId = 1, PetName = "Herbie" },
  new Car { Color = "White", MakeId = 2, PetName = "Mach 5" },
  new Car { Color = "Pink", MakeId = 3, PetName = "Avon" },
  new Car { Color = "Blue", MakeId = 4, PetName = "Blueberry" },
};
Context.Cars.AddRange(cars);
Context.SaveChanges();

EF Core 会在一次调用中批量处理这些语句。生成的查询如下所示:

exec sp_executesql N'SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
MERGE [Dbo].[Inventory] USING (
VALUES (@p0, @p1, @p2, 0),
(@p3, @p4, @p5, 1),
(@p6, @p7, @p8, 2),
(@p9, @p10, @p11, 3)) AS i ([Color], [MakeId], [PetName], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Color], [MakeId], [PetName])
VALUES (i.[Color], i.[MakeId], i.[PetName])
OUTPUT INSERTED.[Id], i._Position
INTO @inserted0;

SELECT [t].[Id], [t].[IsDrivable], [t].[TimeStamp] FROM [Dbo].[Inventory] t
INNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id])
ORDER BY [i].[_Position];

',N'@p0 nvarchar(50),@p1 int,@p2 nvarchar(50),@p3 nvarchar(50),@p4 int,@p5 nvarchar(50),@p6 nvarchar(50),@p7 int,@p8 nvarchar(50),@p9 nvarchar(50),@p10 int,@p11 nvarchar(50)',@p0=N'Yellow',@p1=1,@p2=N'Herbie',@p3=N'White',@p4=2,@p5=N'Mach 5',@p6=N'Pink',@p7=3,@p8=N'Avon',@p9=N'Blue',@p10=4,@p11=N'Blueberry'

拥有的实体类型

使用 C# 类作为一个实体的属性来定义另一个实体的属性集合是在 2.0 版本中首次引入的,并在不断更新。当用[Owned]属性标记的类型(或用 Fluent API 配置的类型)被添加为实体的属性时,EF Core 会将来自[Owned]实体类的所有属性添加到拥有实体中。这增加了 C# 代码重用的可能性。

在幕后,EF Core 认为这是一对一的关系。拥有的类是依赖实体,拥有的类是主体实体。拥有的类,即使被认为是一个实体,如果没有拥有的实体就不能存在。所拥有类型的默认列名将被格式化为NavigationPropertyName_OwnedEntityPropertyName(例如PersonalNavigation_FirstName)。可以使用 Fluent API 更改默认名称。

以这个Person类为例(注意Owned属性):

[Owned]
public class Person
{
  [Required, StringLength(50)]
  public string FirstName { get; set; } = "New";
  [Required, StringLength(50)]
  public string LastName { get; set; } = "Customer";
}

这由Customer类使用:

[Table("Customers", Schema = "Dbo")]
public partial class Customer : BaseEntity
{
  public Person PersonalInformation { get; set; } = new Person();
  [JsonIgnore]
  [InverseProperty(nameof(CreditRisk.CustomerNavigation))]
  public IEnumerable<CreditRisk> CreditRisks { get; set; } = new List<CreditRisk>();
  [JsonIgnore]
  [InverseProperty(nameof(Order.CustomerNavigation))]
  public IEnumerable<Order> Orders { get; set; } = new List<Order>();
}

默认情况下,两个Person属性被映射到名为PersonalInformation_FirstNamePersonalInformation_LastName的列。为了改变这一点,将下面的 Fluent API 代码添加到OnConfiguring()方法中:

modelBuilder.Entity<Customer>(entity =>
{
  entity.OwnsOne(o => o.PersonalInformation,
      pd =>
      {
        pd.Property<string>(nameof(Person.FirstName))
             .HasColumnName(nameof(Person.FirstName))
             .HasColumnType("nvarchar(50)");
        pd.Property<string>(nameof(Person.LastName))
             .HasColumnName(nameof(Person.LastName))
             .HasColumnType("nvarchar(50)");
      });
});

生成的表是这样创建的(注意,FirstNameLastName列的可空性与Person拥有的实体上的数据注释不匹配):

CREATE TABLE [dbo].Customers NOT NULL,
  [FirstName] nvarchar NULL,
  [LastName] nvarchar NULL,
  [TimeStamp] [timestamp] NULL,
  [FullName]  AS (([LastName]+', ')+[FirstName]),
CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED
(
  [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

EF Core 5 解决了一个拥有实体的问题,这个问题可能不会出现在你面前,但可能是一个重大问题。注意,Person类的两个属性上都有Required数据注释,但是 SQL Server 列都被设置为NULL。这是由于当所拥有的实体用于可选关系时,迁移系统如何转换所拥有的实体的问题。解决方法是建立必要的关系。

要纠正这一点,有几个选择。第一个是启用 C# 可空性(在项目级别或在类中)。这使得PersonalInformation导航属性不可为空,EF Core 支持这一点,然后 EF Core 相应地配置所拥有的实体中的列。另一个选项是添加一个流畅的 API 语句,使导航属性成为必需的。

modelBuilder.Entity<Customer>(entity =>
{
  entity.OwnsOne(o => o.PersonalInformation,
      pd =>
      {
        pd.Property<string>(nameof(Person.FirstName))
             .HasColumnName(nameof(Person.FirstName))
             .HasColumnType("nvarchar(50)");
        pd.Property<string>(nameof(Person.LastName))
             .HasColumnName(nameof(Person.LastName))
             .HasColumnType("nvarchar(50)");
      });
  entity.Navigation(c => c.PersonalInformation).IsRequired(true);
});

对于拥有的实体,还有其他选项可以探索,包括集合、表拆分和嵌套。这些都超出了本书的范围。如需了解更多信息,请在此处查阅关于所有实体的 EF 核心文件: https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities

数据库功能映射

SQL Server 函数可以映射到 C# 方法,并包含在 LINQ 语句中。C# 方法只是一个占位符,因为服务器函数被合并到为查询生成的 SQL 中。在 EF Core 中,对表值函数映射的支持已经添加到对标量函数映射的支持中。关于数据库函数映射的更多信息,请查阅文档: https://docs.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping

EF 核心全球工具 CLI 命令

dotnet-ef global CLI tool EF 核心工具包含将现有数据库移植到代码中、创建/删除数据库迁移以及对数据库进行操作(更新、删除等)所需的命令。).在您可以使用dotnet-ef全局工具之前,必须使用以下命令安装它(如果您已经按照本章前面的内容进行了安装,那么您已经完成了):

dotnet tool install --global dotnet-ef --version 5.0.1

Note

因为 EF Core 5 不是一个长期支持的版本,要使用 EF Core 5 全球工具,您必须指定一个版本。

要测试安装,请打开命令提示符并输入以下命令:

dotnet ef

如果工具安装成功,您将获得 EF 核心独角兽(团队的吉祥物)和可用命令列表,如下所示(独角兽在屏幕上更好看):

               _/\__
         ---==/     \\
  ___ ___     |.      \|\
 |__||__| |   )     \\\
 |_||_|   \_/ |   //|\\
 |__ ||_|     /    \\\/\\

Entity Framework Core .NET Command-line Tools 5.0.1

Usage: dotnet ef [options] [command]

Options:
  --version        Show version information
  -h|--help        Show help information
  -v|--verbose     Show verbose output.
  --no-color       Don't colorize output.
  --prefix-output  Prefix output with level.

Commands:
  database    Commands to manage the database.
  dbcontext   Commands to manage DbContext types.
  migrations  Commands to manage migrations.

Use "dotnet ef [command] --help" for more information about a command.

表 22-9 描述了 EF 核心全局工具中的三个主要命令。每个主命令都有附加的子命令。就像所有的。NET 核心命令,每个命令都有丰富的帮助系统,可以通过随命令输入-h来访问。

表 22-9。

EF 核心工具命令

|

命令

|

生命的意义

|
| — | — |
| Database | 管理数据库的命令。子命令包括dropupdate。 |
| DbContext | 管理DbContext类型的命令。子命令包括scaffoldlistinfo。 |
| Migrations | 管理迁移的命令。子命令包括addlistremovescript。 |

EF 核心命令在上执行。NET 核心项目文件(而不是解决方案文件)。目标项目需要引用 EF 核心工具 NuGet 包Microsoft.EntityFrameworkCore.Design。这些命令对位于运行命令的同一目录中的项目文件进行操作,或者对通过命令行选项引用的另一个目录中的项目文件进行操作。

对于需要派生的DbContext类(DatabaseMigrations)的实例的 EF Core CLI 命令,如果项目中只有一个实例,将使用那个实例。如果有多个,那么需要在命令行选项中指定DbContext。派生的DbContext类将使用实现IDesignTimeDbContextFactory<TContext>接口的类的实例进行实例化,如果可以找到的话。如果工具找不到,那么派生的DbContext将使用无参数构造函数进行实例化。如果两者都不存在,该命令将失败。注意,无参数构造函数选项要求存在OnConfiguring覆盖,这不是一个好的实践。最好的(也是唯一的)选择是始终为应用中的每个派生的DbContext创建一个IDesignTimeDbContextFactory<TContext>

EF 核心命令有常用选项,如表 22-10 所示。许多命令都有额外的选项或参数。

表 22-10。

EF 核心命令选项

|

选项(速记||手写)

|

生命的意义

|
| — | — |
| --c &#124;&#124; --context <DBCONTEXT> | 要使用的完全限定的派生类DbContext。如果项目中存在多个派生的DbContext,这是一个必需选项。 |
| -p &#124;&#124; --project <PROJECT> | 要使用的项目(放置文件的位置)。默认为当前工作目录。 |
| -s &#124;&#124; --startup-project <PROJECT> | 要使用的启动项目(包含派生的DbContext)。默认为当前工作目录。 |
| -h &#124;&#124; --help | 显示帮助和所有选项。 |
| -v || -详细 | 显示详细输出。 |

要列出命令的所有参数和选项,请在命令窗口中输入dotnet ef <command> -h,如下所示:

dotnet ef migrations add -h

Note

需要注意的是,CLI 命令不是 C# 命令,因此转义斜杠和引号的规则不适用。

迁移命令

migrations命令用于添加、删除、列出和编写迁移脚本。当迁移应用于一个基础时,在__EFMigrationsHistory表中创建一个记录。表 22-11 描述了这些命令。以下部分详细解释了这些命令。

表 22-11。

EF 核心迁移命令

|

命令

|

生命的意义

|
| — | — |
| Add | 基于上一次迁移的更改创建新的迁移 |
| Remove | 检查项目中的最后一次迁移是否已应用于数据库,如果没有,则删除迁移文件(及其设计器),然后将快照类回滚到上一次迁移 |
| List | 列出派生DbContext的所有迁移及其状态(已应用或待定) |
| Script | 为所有、一个或一系列迁移创建 SQL 脚本 |

添加命令

add命令基于当前对象模型创建一个新的数据库迁移。该过程检查派生的DbContext上具有DbSet<T>属性的每个实体(以及使用导航属性可以从这些实体到达的每个实体),并确定是否有任何需要应用到数据库的更改。如果有更改,将生成适当的代码来更新数据库。稍后您将了解到更多相关信息。

Add命令需要一个name参数,用于命名迁移的创建类和文件。除了通用选项之外,选项-o <PATH>–output-dir <PATH>指示迁移文件应该放在哪里。相对于当前路径,默认目录被命名为Migrations

添加的每个迁移都会创建两个属于同一类的文件。这两个文件都以时间戳和迁移名称作为名称的开头,用作add命令的参数。第一个文件命名为<YYYYMMDDHHMMSS>_<MigrationName>.cs,第二个命名为<YYYYMMDDHHMMSS>_<MigrationName>.Designer.cs。时间戳基于文件的创建时间,两个文件的时间戳将完全匹配。第一个文件表示在这个迁移中为数据库更改生成的代码,设计器文件表示基于到这个迁移为止的所有迁移创建和更新数据库的代码。

主文件包含两个方法,Up()Down()Up()方法包含用这次迁移的变更更新数据库的代码,而Down()方法包含回滚这次迁移的变更的代码。本章前面的初始迁移(One2Many迁移)的部分列表如下:

public partial class One2Many : Migration
{
  protected override void Up(MigrationBuilder migrationBuilder)
  {
    migrationBuilder.CreateTable(
      name: "Make",
      columns: table => new
        {
          Id = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:Identity", "1, 1"),
          Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
          TimeStamp = table.Column<byte[]>(type: "varbinary(max)", nullable: true)
        },
        constraints: table =>
        {
          table.PrimaryKey("PK_Make", x => x.Id);
        });
...
    migrationBuilder.CreateIndex(
      name: "IX_Cars_MakeId",
      table: "Cars",
      column: "MakeId");
  }

  protected override void Down(MigrationBuilder migrationBuilder)
  {
    migrationBuilder.DropTable(name: "Cars");
    migrationBuilder.DropTable(name: "Make");
  }
}

如您所见,Up()方法正在创建表、列、索引等。Down()方法正在删除创建的项目。迁移引擎将根据需要发出alteradddrop语句,以确保数据库与您的模型相匹配。

设计器文件包含两个属性,将这些部分与文件名和派生的DbContext联系起来。此处显示了设计类别的部分属性列表:

[DbContext(typeof(ApplicationDbContext))]
[Migration("20201230020509_One2Many")]
partial class One2Many
{
  protected override void BuildTargetModel(ModelBuilder modelBuilder)
  {
...
  }
}

第一次迁移在目标目录中创建一个附加文件,以派生的DbContext命名,格式为<DerivedDbContextName>ModelSnapshot.cs。该文件的格式与 designer partial 相同,包含所有迁移的代码。添加或删除迁移时,该文件会自动更新以匹配更改。

Note

不要手动删除迁移文件,这一点非常重要。这将导致<DerivedDbContext>ModelSnapshot.cs与您的迁移不同步,从根本上破坏它们。如果您要手动删除它们,请全部删除并重新开始。要删除一个迁移,使用remove命令,稍后将会介绍。

从迁移中排除表

如果一个实体在多个DbContexts之间共享,每个DbContext将在迁移文件中为该实体的任何变更创建代码。这将导致一个问题,因为如果数据库中已经存在更改,第二个迁移脚本将会失败。在 EF Core 5 之前,唯一的解决方案是手动编辑其中一个迁移文件来删除这些更改。

在 EF Core 5 中,DbContext可以将一个实体标记为排除在迁移之外,让另一个DbContext成为该实体的记录系统。以下代码显示了从迁移中排除的实体:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<LogEntry>().ToTable("Logs", t => t.ExcludeFromMigrations());
}

移除命令

remove命令用于从项目中删除迁移,并且总是在最后一次迁移时运行(基于迁移的时间戳)。移除迁移时,EF Core 将通过检查数据库中的__EFMigrationsHistory表来确保它没有被应用。如果已经应用了迁移,则该过程失败。如果迁移尚未应用或已回滚,迁移将被删除,模型快照文件将被更新。

remove命令不带任何参数(因为它总是在最后一次迁移时工作),并使用与add命令相同的选项。还有一个额外的选项,force选项(-f || --force)。这将回滚上一次迁移,然后一步将其删除。

列表命令

list命令用于显示派生DbContext的所有迁移。默认情况下,它会列出所有迁移并查询数据库以确定它们是否已被应用。如果尚未应用,它们将被列为待定。有一个选项传入特定的连接字符串,另一个选项根本不连接到数据库,而是只列出迁移。表 22-12 显示了这些选项。

表 22-12。

EF 核心迁移列表命令的附加选项

|

选项(速记||手写)

|

生命的意义

|
| — | — |
| --connection <CONNECTION> | 数据库的连接字符串。默认为在IDesignTimeDbContextFactory的实例或DbContextOnConfiguring方法中指定的。 |
| --no-connect | 指示命令跳过数据库检查。 |

脚本命令

script命令基于一个或多个迁移创建一个 SQL 脚本。该命令采用两个可选参数,分别表示迁移开始和迁移结束。如果两者都没有输入,所有迁移都将编写脚本。表 22-13 描述了这些参数。

表 22-13。

EF 核心迁移脚本命令的参数

|

争吵

|

生命的意义

|
| — | — |
| <FROM> | 开始迁移。默认为 0(零),开始迁移。 |
| <TO> | 目标迁移。默认为上次迁移。 |

如果没有命名迁移,则创建的脚本将是所有迁移的累积总和。如果提供了命名迁移,脚本将包含两次迁移之间的更改(包括两次迁移)。每个迁移都包装在一个事务中。如果执行脚本的数据库中不存在__EFMigrationsHistory表,将会创建该表。该表也将被更新,以匹配已执行的迁移。以下是一些例子:

//Script all of the migrations
dotnet ef migrations script
//script from the beginning to the Many2Many migrations
dotnet ef migrations script 0 Many2Many

还有一些附加选项可用,如表 22-14 所示。-o选项允许您为脚本指定一个文件(该目录相对于命令执行的位置),而-i创建一个等幂脚本。这意味着它包含检查以查看是否已经应用了迁移,如果已经应用,则跳过该迁移。–no-transaction选项禁用添加到脚本中的普通事务。

表 22-14。

EF 核心迁移脚本命令的附加选项

|

选项(速记||手写)

|

生命的意义

|
| — | — |
| -o &#124;&#124; -output <FILE> | 要将结果脚本写入的文件 |
| -i &#124;&#124; --idempotent | 生成一个脚本,在应用迁移之前检查是否已经应用了迁移 |
| --no-transactions | 不会将每个迁移都包含在一个事务中 |

数据库命令

有两个数据库命令,dropupdate。如果数据库存在,drop命令会删除它。update命令使用迁移来更新数据库。

Drop 命令

drop命令删除由DbContextOnConfiguring方法的上下文工厂中的连接字符串指定的数据库。使用force选项不要求确认,强制关闭所有连接。见表 22-15 。

表 22-15。

EF 核心数据库删除选项

|

选项(速记||手写)

|

生命的意义

|
| — | — |
| -f &#124;&#124; --force | 不要确认下落。强制关闭所有连接。 |
| --dry-run | 显示要删除的数据库,但不要删除它。 |

数据库更新命令

update命令有一个参数(迁移名称)和常用选项。该命令还有一个附加选项--connection <CONNECTION>。这允许使用未在设计时工厂或DbContext中配置的连接字符串。

如果在没有迁移名称的情况下执行命令,该命令会将数据库更新为最近的迁移,并在必要时创建数据库。如果迁移已命名,数据库将更新到该迁移。所有尚未应用的先前迁移也将被应用。应用迁移时,它们的名称存储在__EFMigrationsHistory表中。

如果指定迁移的时间戳早于其他应用的迁移,则所有以后的迁移都将回滚。如果 0(零)作为命名迁移被传入,所有迁移都被恢复,留下一个空数据库(除了__EFMigrationsHistory表)。

DbContext 命令

有四个DbContext命令。其中三个(listinfoscript)操作项目中的衍生DbContext类。scaffold命令从现有数据库创建一个派生的DbContext和实体。表 22-16 显示了这四个命令。

表 22-16。

DbContext 命令

|

命令

|

生命的意义

|
| — | — |
| Info | 获取关于DbContext类型的信息 |
| List | 列出可用的DbContext类型 |
| Scaffold | 为数据库搭建一个DbContext和实体类型 |
| Script | 基于对象模型从DbContext生成 SQL 脚本,绕过任何迁移 |

listinfo命令有常用的选项。list命令列出了目标项目中派生的DbContext类。info命令提供了关于指定的派生DbContext类的细节,包括连接字符串、提供者名称、数据库名称和数据源。script 命令创建一个 SQL 脚本,该脚本基于对象模型创建您的数据库,忽略可能存在的任何迁移。scaffold命令用于对现有数据库进行逆向工程,将在下一节中介绍。

DbContext Scaffold 命令

scaffold命令创建 C# 类(派生的DbContext和实体),包括数据注释(如果需要)和来自现有数据库的流畅 API 命令。有两个必需的参数,数据库连接字符串和完全限定的提供者(例如,Microsoft.EntityFrameworkCore.SqlServer)。表 22-17 描述了这些争论。

表 22-17。

DbContext 支架参数

|

争吵

|

生命的意义

|
| — | — |
| Connection | 数据库的连接字符串 |
| Provider | 要使用的 EF 核心数据库提供商(如Microsoft.EntityFrameworkCore.SqlServer) |

可用的选项包括选择特定的模式和表、创建的上下文类名和名称空间、生成的实体类的输出目录和名称空间等等。标准选项也可用。扩展选项在表 22-18 中列出,讨论如下。

表 22-18。

DbContext 支架选项

|

选项(速记||手写)

|

生命的意义

|
| — | — |
| -d &#124;&#124; --data-annotations | 使用属性来配置模型(如果可能的话)。如果省略,则仅使用 Fluent API。 |
| -c &#124;&#124; --context <NAME> | 要创建的派生DbContext的名称。 |
| --context-dir <PATH> | 放置派生的DbContext的目录,相对于项目目录。默认为数据库名称。 |
| -f &#124;&#124; --force | 替换目标目录中的任何现有文件。 |
| -o &#124;&#124; --output-dir <PATH> | 将生成的实体类放入的目录。相对于项目目录。 |
| --schema <SCHEMA_NAME>... | 要为其生成实体类型的表的架构。 |
| -t &#124;&#124; --table <TABLE_NAME>... | 要为其生成实体类型的表。 |
| --use-database-names | 直接使用数据库中的表名和列名。 |
| -n &#124; --namespaces <NAMESPACE> | 生成的实体类的命名空间。默认情况下匹配目录。 |
| --context-namespace <NAMESPACE> | 生成的派生DbContext类的名称空间。默认情况下匹配目录。 |
| --no-onconfiguring | 不生成OnConfiguring方法。 |
| --no-pluralize | 不使用复数。 |

EF Core 5.0 中的scaffold命令变得更加强大。如你所见,有很多选项可供选择。如果选择了数据注释(-d)选项,EF Core 将在可能的地方使用数据注释,并填写与 Fluent API 的差异。如果未选择该选项,整个配置(与约定不同的地方)将在 Fluent API 中编码。您可以为生成的实体和派生的DbContext文件指定名称空间、模式和位置。如果不想搭建整个数据库,可以选择某些模式和表。--no-onconfiguring选项从搭建的类中消除了OnConfiguring()方法,–no-pluralize选项关闭了复数器,它在创建迁移时将单个实体(Car)转换为多个表(Cars),在搭建时将多个表转换为单个实体。

摘要

本章开始了进入实体框架核心的旅程。本章研究了 EF 核心基础知识、查询如何执行以及变更跟踪。您学习了如何塑造您的模型、EF 核心约定、数据注释和 Fluent API,以及如何使用它们来影响您的数据库设计。最后一节介绍了 EF 核心命令行界面和全局工具的强大功能。

虽然这一章涵盖了很多理论和一些代码,但下一章几乎都是带有一点理论的代码。当您完成第二十三章时,您将拥有完整的AutoLot数据访问层。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值