一、CodeFirst模式
官网:https://docs.microsoft.com/zh-cn/ef/core/get-started/overview/first-app?tabs=visual-studio
下面进行一个简单的示例。
代码:https://github.com/qiuxianhu/EFCoreSummary
1、创建一个.NETCore空项目
2、添加相关引用
通过命令进行安装,工具》NuGet包管理》程序包管理控制台
(1)引用 EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore
(2)引用 EntityFrameworkCore.SqlServer.Tools
Install-Package Microsoft.EntityFrameworkCore.Tools
3、相关配置
(1)配置连接数据库
(2)在Startup.cs中注册服务
其中的BloggingContext就是接下来我们要新建的数据上下文
4、生成数据库
(1)创建实体模型
public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; } = new List<Post>(); } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } }
(2)创建上下文
public class BloggingContext : DbContext { public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { } public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } }
(3)迁移搭建基架
Add-Migration
命令为迁移搭建基架,以便为模型创建一组初始表。
Add-Migration InitialCreate
执行成功后,项目下会生成一个Migrations的文件夹并包含一些初始化生成数据库需要用的文件
(4)执行 update-database 命令生成数据库
Update-Database
Update-Database命令创建数据库并向其应用新的迁移。
5、添加model模型,迁移数据库
(1)新增一个UserInfo模型,并加入数据上下文中
(2)迁移数据库
Add-Migration AddUserInfoClass //参考迁移,后边也会梳理https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli
Update-Database
6、创建、读取、更新和删除
(1)新建一个API
using Microsoft.AspNetCore.Mvc; using System.Linq; namespace EFCoreSummary.Controller { [Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { private readonly BloggingContext _dbContext; public ValuesController(BloggingContext bloggingContext) { _dbContext = bloggingContext; } public void Init() { //insert _dbContext.Add(new Blog { Url = "http://blogs.msdn.com/adonet" }); _dbContext.SaveChanges(); // Read var blog = _dbContext.Blogs .OrderBy(b => b.BlogId) .First(); // Update blog.Url = "https://devblogs.microsoft.com/dotnet"; blog.Posts.Add( new Post { Title = "Hello World", Content = "I wrote an app using EF Core!" }); _dbContext.SaveChanges(); // Delete _dbContext.Remove(blog); _dbContext.SaveChanges(); } } }
(2)看下数据表数据
二、EFCore的概要
上边做了一个简单的EFCore的示例,这里根据官网进行下整体梳理,主要是做一下实用的概括。在上边的demo中涉及到DbContext、模型、增删改查等概念,下面先对这些进行个概要。
1、DbContext
DbContext是EFCore的核心,当我们使用EFCore持久化时,在一个工作单元(可以理解为一次http请求)中通常会做以下工作:
- 创建
DbContext
实例 - 根据上下文跟踪实体实例。 实体将在以下情况下被跟踪,跟踪和非跟踪会在下边的查询中介绍,这里只要知道一个工作单元中DbContext做了什么即可。
- 正在从查询返回
- 正在添加或附加到上下文
- 根据需要对所跟踪的实体进行更改以实现业务规则
- 调用 SaveChanges 或 SaveChangesAsync。 EF Core 检测所做的更改,并将这些更改写入数据库。
- 释放
DbContext
实例
(1)DbContext 生存期
从上边DbContext的工作流程可以看出,DbContext
的生存期从创建实例时开始,并在释放实例时结束。具体点说,就是从一个请求开始到请求结束, DbContext
也会相应的创建和释放。
这里需要注意一点:DbContext 不是线程安全的。 不是线程安全怎么理解呢?
简单来说就是不支持在同一 DbContext
实例上运行多个并行操作。 这包括异步查询的并行执行以及从多个线程进行的任何显式并发使用。所以在异步调用中,await显得尤为重要。举个例子,我们有个登录操作,查询数据库采用的是EFCore,同时有两个用户发出登录请求,那么对于EFCore就是两个工作单元,每一个请求都有自己的DbContext上下文,互不干扰。不要在线程之间共享上下文。 请确保在继续使用上下文实例之前,等待所有异步调用。
(2)DbContext的配置和初始化
DbContext既然这么重要,我们怎么使用到项目中呢?
-
通过依赖注入的方式
(1)配置上下文:ASP.NET Core 应用程序, 可以使用 Startup.cs
的 ConfigureServices
方法中的 AddDbContext 将 EF Core 添加到此配置。 例如:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));
}
(2)使用上边配置的上下文:其中Appli
cationDbContext
是DbContext
的子类,ApplicationDbContext
类必须公开具有 DbContextOptions<ApplicationDbContext>
参数的公共构造函数。 这是将 AddDbContext
的上下文配置传递到 DbContext
的方式。 例如:
public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } }
(3)然后我们就可以把ApplicationDbContext
通过构造函数注入在 ASP.NET Core 控制器或其他服务中使用。 例如:
public class MyController { private readonly ApplicationDbContext _context; public MyController(ApplicationDbContext context) { _context = context; } }
上边的三个流程和我们上边demo中的使用方式是一样的,可以参考下上面的demo。
-
使用“new”的简单的 DbContext 初始化
(1)配置上下文:新建一个DbContext子类ApplicationDbContext,在子类中配置上下文
public class ApplicationDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)mssqllocaldb;Database=Test"); } }
(2)使用
using System; using System.Linq; namespace EFGetStarted { internal class Program { private static void Main() { using (var db = new BloggingContext()) { db.Add(new Blog { Url = "http://blogs.msdn.com/adonet" }); db.SaveChanges(); } } } }
这种方式和微软官网上提供的demo一样,可以参考下:https://docs.microsoft.com/zh-cn/ef/core/get-started/overview/first-app?tabs=netcore-cli
-
使用 DbContext 工厂
应用程序可能需要在一个作用域内执行多个工作单元。 例如,单个 HTTP 请求中的多个工作单元。在这些情况下,可以使用 AddDbContextFactory 来注册工厂以创建 DbContext
实例。 前三个步骤和依赖注入一样。
(1)配置上下文:
public void ConfigureServices(IServiceCollection services) { services.AddDbContextFactory<ApplicationDbContext>( options => options.UseSqlServer(@"Server=(localdb)mssqllocaldb;Database=Test")); }
(2)定义DbContext
的子类ApplicationDbContext,同上
ApplicationDbContext
类必须公开具有 DbContextOptions<ApplicationDbContext>
参数的公共构造函数。例如:
public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } }
(3)定义一个工厂,在工厂类中注入上下文
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory; public MyController(IDbContextFactory<ApplicationDbContext> contextFactory) { _contextFactory = contextFactory; }
(4)在实现工厂的具体服务中,可以使用在工厂中构造 DbContext 实例。 例如:
public void DoSomething() { using (var context = _contextFactory.CreateDbContext()) { // ... } }
2、模型
模型还是非常重要的,看下官网上的这句话:Entity Framework Core 使用一组约定来根据实体类的形状生成模型。 可指定其他配置以补充和/或替代约定的内容。什么意思呢?
为了便于理解,我们可以直接将实体认为是EFCore中的模型。我们在程序中定义一个实体,其实就是定义了一个模型,然后通过迁移生成数据库中的一个数据表。这里就会有一个问题,数据表会涉及到很多属性,比如索引、主键等,那么模型应该怎么设计才能迁移成我们想看到的数据表呢?
I、DbSet
在上下文中包含一种类型的 DbSet,其实就是个集合,该集合可以包含不同的实体,我们可以将DbSet作为EFCore中存放实体的集合。
2、实体属性
(1)包含属性(列)
按照约定,具有 getter 和 setter 的所有公共属性都将包括在模型中。就是说包含getter和setter的公共属性都会被迁移为数据表的列。
(2)排除属性(列)
性上添加[
public class Blog { [NotMapped] public DateTime LoadedFromDatabase { get; set; } }
(3)控制列名
默认情况下,实体在迁移后生成的表列名就是属性名,但是我们也可以控制实体中某个属性迁移后的列名。例如:
public class Blog { [Column("blog_id")] public int BlogId { get; set; } }
(4)列数据类型
直接看例子,相信都能看得懂:
public class Blog { public int BlogId { get; set; } [Column(TypeName = "varchar(200)")] public string Url { get; set; } [Column(TypeName = "decimal(5, 2)")] public decimal Rating { get; set; } }
(5)列最大长度
public class Blog { public int BlogId { get; set; } [MaxLength(500)] public string Url { get; set; } }
(6)必需属性和可选属性
按照约定,.NET 类型可以包含 null 的属性将配置为可选,而 .NET 类型不能包含 null 的属性将根据需要进行配置。 例如,所有具有 .net 值类型的属性 (int
、 decimal
) 、等 bool
都是必需的,并且具有可为 null 的 .net 值类型 (、) 、等)的所有属性 int?
decimal?
bool?
都配置为可选。
public class CustomerWithoutNullableReferenceTypes { public int Id { get; set; } [Required] // Data annotations needed to configure as required public string FirstName { get; set; } [Required] public string LastName { get; set; } // Data annotations needed to configure as required public string MiddleName { get; set; } // Optional by convention }
(7)配置主键
键充当每个实体实例的唯一标识符。 EF 中的大多数实体都有一个键,此键映射到关系数据库中 主键 的概念 。按照约定,将名为 Id
或的属性 <type name>Id
配置为实体的主键。例如:
internal class Car { public string Id { get; set; } public string Make { get; set; } }
可以将单个属性配置为实体的主键,例如:
internal class Car { [Key] public string LicensePlate { get; set; } public string Model { get; set; } }
可以将多个属性配置为实体的键,这称为组合键
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Car>() .HasKey(c => new { c.State, c.LicensePlate }); }
设置主键名称:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasKey(b => b.BlogId) .HasName("PrimaryKey_BlogId"); }
(8)设置列默认值
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.Rating) .HasDefaultValue(3); }
(9)并发标记
配置为并发标记的属性用于实现乐观并发控制
public class Person { public int PersonId { get; set; } [ConcurrencyCheck] public string LastName { get; set; } public string FirstName { get; set; } }
(10)索引
索引是跨多个数据存储区的常见概念。 尽管它们在数据存储中的实现可能会有所不同,但也可用于基于列 (或一组列来进行查找,) 效率更高。
[Index(nameof(Url))] public class Blog { public int BlogId { get; set; } public string Url { get; set; } }
复合索引:
[Index(nameof(FirstName), nameof(LastName))] public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } }
唯一索引:
[Index(nameof(Url), IsUnique = true)] public class Blog { public int BlogId { get; set; } public string Url { get; set; } }
索引名称:
[Index(nameof(Url), Name = "Index_Url")] public class Blog { public int BlogId { get; set; } public string Url { get; set; } }
以上是常用的配置,当然还有很多高级的用法,可以看下官网:https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/
3、管理数据库架构
EF Core 提供两种主要方法来保持 EF Core 模型和数据库架构同步。即codefirst或者dbfirst,codefirst对应迁移,dbfirst对应反向工程。接下来介绍下迁移
三、迁移
在实际项目中,数据模型随着功能的实现而变化:添加和删除新的实体或属性,并且需要相应地更改数据库架构,使其与应用程序保持同步。 EF Core 中的迁移功能能够以递增方式更新数据库架构,使其与应用程序的数据模型保持同步,同时保留数据库中的现有数据。
简要地说,迁移的方式如下:
- 当引入数据模型更改时,开发人员使用 EF Core 工具添加相应的迁移,以描述使数据库架构保持同步所需的更新。EF Core 将当前模型与旧模型的快照进行比较,以确定差异,并生成迁移源文件;文件可在项目的源代码管理中进行跟踪,如任何其他源文件。
- 生成新的迁移后,可通过多种方式将其应用于数据库。 EF Core 在一个特殊的历史记录表中记录所有应用的迁移,使其知道哪些迁移已应用,哪些迁移尚未应用。
1、迁移简单示例
假设你刚刚完成了第一个 EF Core 应用程序,其中包含以下简单模型:
public class Blog { public int Id { get; set; } public string Name { get; set; } }
在开发过程中,你可能已使用创建和删除 API 以快速迭代,并根据需要更改模型;但现在,你的应用程序将进入生产环境,你需要一种方法来安全地演化架构,而无需删除整个数据库。
(1)创建第一个迁移
指示 EF Core 创建名为 InitialCreate (名字可自定义)的迁移:
Add-Migration InitialCreate
EF Core 将在项目中创建一个名为“Migrations”的目录,并生成一些文件。
(2)创建数据库和架构
EF 创建数据库并从迁移中创建架构:
Update-Database
请注意,这种应用迁移的方法非常适合本地开发,但不太适用于生产环境 - 有关详细信息,请参阅应用迁移页面。
(3)更新模型
模型更新后如下:
public class Blog { public int Id { get; set; } public string Name { get; set; } public DateTime CreatedTimestamp { get; set; } }
模型和生产数据库现在不同步,我们必须向数据库架构中添加一个新列。创建新迁移:
Add-Migration AddBlogCreatedTimestamp
请注意,我们为迁移提供了一个描述性名称(名称可自定义,不可重复),以便以后更容易了解项目历史记录。由于这不是项目的第一次迁移,EF Core 现在会在添加列之前将更新的模型与旧模型的快照进行比较;模型快照是 EF Core 在你添加迁移时生成的文件之一,并签入到源代码管理中。 基于该比较,EF Core 检测到已添加一列,并添加适当的迁移。然后进行同步:
Update-Database
请注意,这次 EF 检测到数据库已存在。 此外,在之前第一次应用迁移时,此事实记录在数据库中的特殊迁移历史记录表中;这允许 EF 自动仅应用新的迁移。
2、管理迁移
(1)添加迁移
先来看下迁移的命令:
Add-Migration AddBlogCreatedTimestamp
迁移名称可自定义,最好符合一种规范,常用时间戳的方式,便于管理。迁移后,会向 Migrations 目录下的项目添加以下三个文件:
- XXXXXXXXXXXXXX_AddCreatedTimestamp-主迁移文件。 包含应用迁移所需的操作(在
Up
中)和还原迁移所需的操作(在Down
中)。 - XXXXXXXXXXXXXX_AddCreatedTimestamp:迁移元数据文件。 包含 EF 所用的信息。
- MyContextModelSnapshot.cs--当前模型的快照。 用于确定添加下一迁移时的更改内容。
(2)指定迁移文件的路径
Add-Migration InitialCreate -OutputDir YourDirectory
(3)删除迁移
Remove-Migration
(4)列出所有迁移
Get-Migration
上边列出了简单的使用迁移的只是,当然还有很多关于迁移的高级用法,查看官网:https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/scaffolding?tabs=dotnet-core-cli
四、反向工程(DbFirst)
五、查询数据
Entity Framework Core 使用语言集成查询 (LINQ) 来查询数据库中的数据。 通过 LINQ 可使用 C#编写强类型查询。 EF Core 将 LINQ 查询的表示形式传递给数据库提供程序。 数据库提供程序将其转换为数据库特定的查询语言(例如,用于关系数据库的 SQL)。 即使结果中返回的实体已存在于上下文中,也始终对数据库执行查询。
1、客户端与服务器评估
这块非常重要,我们知道EFCore使用LINQ来查询数据库数据,那么一个条件查询,是在服务端完成后返回需要的数据到客户端,还是将数据全部返回,存在客户端内存,在进行条件筛选呢?作为一般规则,Entity Framework Core 会尝试尽可能全面地评估服务器上的查询。 EF Core 将查询的一部分转换为可在客户端评估的参数。 系统将查询的其余部分(及生成的参数)提供给数据库提供程序,以确定要在服务器上评估的等效数据库查询。 EF Core 支持在顶级投影中进行部分客户端评估。 如果查询中的顶级投影无法转换为服务器,EF Core 将从服务器中提取任何所需的数据,并在客户端上评估查询的其余部分。 如果 EF Core 在顶级投影之外的任何位置检测到不能转换为服务器的表达式,则会引发运行时异常。
(1)顶级投影中的客户端评估
在下面的示例中,一个辅助方法用于标准化从 SQL Server 数据库中返回的博客的 URL。 由于 SQL Server 提供程序不了解此方法的实现方式,因此无法将其转换为 SQL。 查询的所有其余部分是在数据库中评估的,但通过此方法传递返回的 URL
却是在客户端上完成。
var blogs = context.Blogs .OrderByDescending(blog => blog.Rating) .Select( blog => new { Id = blog.BlogId, Url = StandardizeUrl(blog.Url) }) .ToList();
public static string StandardizeUrl(string url) { url = url.ToLower(); if (!url.StartsWith("http://")) { url = string.Concat("http://", url); } return url; }
(2)不支持的客户端评估
尽管客户端评估非常有用,但有时会减弱性能。 请看以下查询,其中的 where 筛选器现已使用辅助方法。 由于数据库中不能应用筛选器,因此需要将所有数据提取到内存中,以便在客户端上应用筛选器。 根据服务器上的筛选器和数据量,客户端评估可能会减弱性能。 因此 Entity Framework Core 会阻止此类客户端评估,并引发运行时异常。
var blogs = context.Blogs .Where(blog => StandardizeUrl(blog.Url).Contains("dotnet")) .ToList();
(3)显式客户端评估
在某些情况下,可能需要以显式方式强制进行客户端评估,如下所示
- 由于数据量小,因此在进行客户端评估时才不会大幅减弱性能。
- 所用的 LINQ 运算符不会进行任何服务器端转换。
在这种情况下,通过调用 AsEnumerable
或 ToList
等方法(若为异步,则调用 AsAsyncEnumerable
或 ToListAsync
),以显式方式选择进行客户端评估。 使用 AsEnumerable
将对结果进行流式传输,但使用 ToList
将通过创建列表来进行缓冲,因此也会占用额外的内存。
var blogs = context.Blogs .AsEnumerable() .Where(blog => StandardizeUrl(blog.Url).Contains("dotnet")) .ToList();
2、客户端评估中潜在的内存泄漏
由于查询转换和编译的开销高昂,因此 EF Core 会缓存已编译的查询计划。 缓存的委托在对顶级投影进行客户端评估时可能会使用客户端代码。 EF Core 为树型结构中客户端评估的部分生成参数,并通过替换参数值重用查询计划。 但表达式树中的某些常数无法转换为参数。 如果缓存的委托包含此类常数,则无法将这些对象垃圾回收,因为它们仍被引用。 如果此类对象包含 DbContext 或其中的其他服务,则会导致应用的内存使用量逐渐增多。 此行为通常是内存泄漏的标志。 只要遇到的常数为不能使用当前数据库提供程序映射的类型,EF Core 就会引发异常。 常见原因及其解决方案如下所示:
- 使用实例方法:在客户端投影中使用实例方法时,表达式树包含实例的常数。 如果你的方法不使用该实例中的任何数据,请考虑将该方法设为静态方法。 如果需要方法主体中的实例数据,则将特定数据作为实参传递给方法。
- 将常数实参传递给方法:这种情况通常是由于在客户端方法的实参中使用
this
引起的。 请考虑将实参拆分为多个标量实参,可由数据库提供程序进行映射。 - 其他常数:如果在任何其他情况下都出现常数,则可以评估在处理过程中是否需要该常数。 如果必须具有常数,或者如果无法使用上述情况中的解决方案,则创建本地变量来存储值,并在查询中使用局部变量。 EF Core 会将局部变量转换为形参
3、跟踪与非跟踪查询
跟踪行为决定了 Entity Framework Core 是否将有关实体实例的信息保留在其更改跟踪器中。 如果已跟踪某个实体,则该实体中检测到的任何更改都会在 SaveChanges()
期间永久保存到数据库。 EF Core 还将修复跟踪查询结果中的实体与更改跟踪器中的实体之间的导航属性。
(1)跟踪查询
默认情况下,跟踪返回实体类型的查询。 这表示可以更改这些实体实例,然后通过 SaveChanges()
持久化这些更改。 在以下示例中,将检测到对博客分级所做的更改,并在 SaveChanges()
期间将这些更改永久保存到数据库中。
var blog = context.Blogs.SingleOrDefault(b => b.BlogId == 1); blog.Rating = 5; context.SaveChanges();
在跟踪查询中返回结果时,EF Core 将检查上下文中是否已存在实体。 如果 EF Core 找到现有的实体,则返回同样的实例。 EF Core 不会用数据库值覆盖该实体中实体属性的当前值和原始值。 如果未在上下文中找到该实体,EF Core 将创建新的实体实例,并将其附加到上下文。 查询结果不会包含任何已添加到上下文但尚未保存到数据库中的实体。
(2)非跟踪查询
在只读方案中使用结果时,非跟踪查询十分有用。 可以更快速地执行非跟踪查询,因为无需设置更改跟踪信息。 如果不需要更新从数据库中检索到的实体,则应使用非跟踪查询。 可以将单个查询替换为非跟踪查询。 非跟踪查询也会根据数据库中的内容提供结果,但不考虑本地更改或已添加的实体。
var blogs = context.Blogs .AsNoTracking() .ToList();
还可以在上下文实例级别更改默认跟踪行为:
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; var blogs = context.Blogs.ToList();
(3)标识解析
由于跟踪查询使用更改跟踪器,因此 EF Core 将在跟踪查询中执行标识解析。 当具体化实体时,如果 EF Core 已被跟踪,则会从更改跟踪器返回相同的实体实例。 如果结果中多次包含相同的实体,则每次会返回相同的实例。 非跟踪查询不会使用更改跟踪器,也不会执行标识解析。 因此会返回实体的新实例,即使结果中多次包含相同的实体也是如此。 从 EF Core 5.0 开始,可以在同一个查询中结合使用上述两种行为。 也就是说,可以使用非跟踪查询并对结果执行标识解析。 我们添加了另一个运算符 AsNoTrackingWithIdentityResolution()
,就像添加 AsNoTracking()
可查询运算符一样。 QueryTrackingBehavior 枚举中也添加了一个关联项。 如果将查询配置为使用标识解析和非跟踪行为,生成查询结果时我们将在后台使用独立的更改追踪器,以便仅将每个实例具体化一次。 此更改追踪器不同于上下文中的更改追踪器,因此上下文不会追踪这些结果。 完全枚举查询后,该更改追踪器将超出范围,并根据需要对其进行垃圾回收。
var blogs = context.Blogs .AsNoTrackingWithIdentityResolution() .ToList();
加载所有数据
using (var context = new BloggingContext())
{
var blogs = context.Blogs.ToList();
}
加载单个实体
using (var context = new BloggingContext())
{
var blog = context.Blogs
.Single(b => b.BlogId == 1);
}
筛选
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Where(b => b.Url.Contains("dotnet"))
.ToList();
}