深入理解 EF Core:EF Core 写入数据时发生了什么? (2)

一个创建数据库的方法——第 8 行:第一次执行时,这句代码会创建一个新的数据库,包括创建正确的表、键、索引等。EnsureCreated 方法用于单元测试,但对于真实的应用程序,你最好手动执行 EF Core 的 Migration 命令。

向数据库写入数据的命令——第 17 到 18 行:

第 17 行:Add 方法告诉 EF Core 需要将一个 Book 实体及其关系(在本例中,只是一个 Review 实体)写入数据库。

第 18 行:SaveChange 方法将在数据库中的 Books 和 Reviews 表中创建新行。

在 //VERIFY 注释之后的最后几行用来检查数据是否已经被写入数据库。

在本例中,你向数据库添加了新的记录(SQL 的 INSERT INTO 命令)。EF Core 也可以处理更新和删除数据库的数据,下一节介绍这个新增示例,然后介绍其他新增、更新和删除的示例。

写入数据时数据库端发生了什么

我将从创建一个新的 Book 实体类和新的 Review 实体类开始。这两个类的关系比较简单。使用上面单元测试的例子,主要代码如下:

var book = new Book { Title = "Test", Reviews = new List<Review>() }; book.Reviews.Add(new Review { NumStars = 1 }); context.Add(book); context.SaveChanges();

为了将这两个实体添加到数据库,EF Core 需要这样做:

确定它应该以什么顺序创建这些新行——在本例中,它必须在 Books 表中创建一行,这样它就拥有 Books 的主键。

将主键复制到与其关联的外键——在本例中,它将 Books 中的主键 BookId 复制到 Review 的外键。

复制数据库中新创建的数据,以便实体类正确表示数据库的数据——在这种情况下,它必须复制 BookId 并更新 BookId 属性,包括 Book 和 Review 实体类以及 Review 实体类的 ReviewId。

下面我们看看上面代码生成的 SQL 语句:

-- 第一次访问数据库 SET NOCOUNT ON; -- 向数据库的 Books 表生成一条新数据. -- 数据库生成 Books 的主键值 INSERT INTO [Books] ([Description], [Title], ...) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6); -- 返回主键值,检查并确认数据行是否已添加 SELECT [BookId] FROM [Books] WHERE @@ROWCOUNT = 1 AND [BookId] = scope_identity(); -- 第二次访问数据库 SET NOCOUNT ON; -- 向数据库的 Review 表生成一条新数据. -- 数据库生成 Review 的主键值 INSERT INTO [Review] ([BookId], [Comment], ...) VALUES (@p7, @p8, @p9, @p10); -- 返回主键值,检查并确认数据行是否已添加 SELECT [ReviewId] FROM [Review] WHERE @@ROWCOUNT = 1 AND [ReviewId] = scope_identity();

重要的一点是,EF Core 是按正确的顺序处理实体类的,这样它就可以填充外键。这是简单的例子,但我遇到一个客户项目的例子是,我不得不建立一个非常复杂的数据组成的 15 个不同的实体类,一些实体类是新增,一些是更新和删除,EF Core 通过一个 SaveChanges 将把所有工作有序地完成了库。因此,EF Core 使开发者可以很容易地将复杂的数据写入数据库。

我之所以提到这一点,是因为我看到过在 EF Core 代码中,开发人员多次调用 SaveChanges 方法来从第一个新增命令中获得主键,并把它设置为相关实体的外键。例如:

var book = new Book { Title = "Test" }; context.Add(book); context.SaveChanges(); var review = new Review { BookId = book.BookId, NumStars = 1 } context.Add(review); context.SaveChanges();

虽然这代码效果是一样的,但它有一个缺陷——如果第二 SaveChanges 失败,那么就会发生部分数据更新到数据库的情况。在某种情况下,这可能不是个问题,但对于像我客户那种需要保证数据一致的情况,就非常糟糕了。

因此,从中得到的收获是,您不需要将主键复制到外键中,因为你可以设置导航属性,EF Core 将为您挑选出外键。因此,如果你认为需要调用两次 SaveChanges,那么通常意味着你没有设置正确的导航属性来处理这种情况。

写数据时 DbContext 做了什么

在上一节中,你看到了 EF Core 在数据库端做了什么,现在你要看看在 EF Core 中发生了什么。大多数情况,你不需要知道,但有时候知道这些是非常重要的。例如,你只能在 SaveChanges 之前捕获数据的状态。而对于自增主键,你只有在 SaveChanges 被调用之后才能拿到主键的值。

与上一个示例相比,这个示例稍微复杂一些。在这个示例中,我想向你展示 EF Core 通过从数据库中读取的已有实体类的实例来处理另一个实体类的新实例。下面的代码创建了一个新的 Book,但 Author 已经在数据库中了。代码注明了阶段 1、阶段 2 和阶段 3,然后我用图表描述每个阶段发生的事情。

// 阶段 1 var author = context.Authors.First(); var bookAuthor = new BookAuthor { Author = author }; var book = new Book { Title = "Test Book", AuthorsLink = new List<BookAuthor> { bookAuthor } }; // 阶段 2 context.Add(book); // 阶段 3 context.SaveChanges();

接下来的三个图向你展示了实体类及其跟踪数据在每个阶段内发生的事情。每个图显示了其阶段结束时的以下数据:

流程的每个阶段中每个实例的状态。

Book 和 BookAuthor 类是棕色的,表示它们是类的新实例,需要添加到数据库中,而 Author 实体类是蓝色的,表示从数据库中读取的实例。

主键和外键旁边的括号是其当前的值。如果一个键是 (0),那么它还没有被设值。

箭头连线连接的是从导航属性到其相应实体类。

每个阶段之间的变化通过粗体文本或箭头连线的粗线显示。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpxfsf.html