C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(下)
译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(下)),不对的地方欢迎指出与交流。
章节出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位阅读时仔细分辨,唯望莫误人子弟。
附英文版原文:Professional C# 6 and .NET Core 1.0 - 38 Entity Framework Core
本章节译文分为上下篇,上篇见: C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(上)
--------------------------------------
使用对象状态工作
创建数据库后,可以进行写入。在第一个示例中,已添加了单个表,那么如何添加关系?
添加对象关系
以下代码片段写入一个关系,MenuCard包含Menu对象。MenuCard和Menu对象被实例化,然后分配双向的关联关系。使用Menu将 MenuCard 属性分配给 MenuCard,而使用 MenuCard 将 Menu 属性将填充Menu对象。 MenuCard实例被添加到调用MenuCards属性的Add方法的上下文中。默认情况下,向上下文添加对象时所有对象都添加树并保存为Added 状态。不仅保存MenuCard,还保存 Menu 对象。 设置IncludeDependents 后,所有关联的Menu对象也将添加到上下文中。在上下文中调用SaveChanged现在创建四条记录(代码文件MenusSample / Program.cs):
private static async Task AddRecordsAsync() { // etc. using (var context = new MenusContext()) { var soupCard = new MenuCard(); Menu[] soups = { new Menu { Text ="Consommé Célestine (with shredded pancake)", Price = 4.8m, MenuCard = soupCard }, new Menu { Text ="Baked Potato Soup", Price = 4.8m, MenuCard = soupCard }, new Menu { Text ="Cheddar Broccoli Soup", Price = 4.8m, MenuCard = soupCard }, }; soupCard.Title ="Soups"; soupCard.Menus.AddRange(soups); context.MenuCards.Add(soupCard); ShowState(context); int records = await context.SaveChangesAsync(); WriteLine($"{records} added"); // etc. }
将四个对象添加到上下文后调用的方法ShowState显示与上下文相关联的所有对象的状态。 DbContext类有一个ChangeTracker关联,可以使用ChangeTracker属性访问。 ChangeTracker的Entries方法返回变化跟踪器的所有对象。使用foreach循环,每个对象包括其状态都将输出到控制台(代码文件MenusSample/Program.cs)
public static void ShowState(MenusContext context) { foreach (EntityEntry entry in context.ChangeTracker.Entries()) { WriteLine($"type: {entry.Entity.GetType().Name}, state: {entry.State}," + $" {entry.Entity}"); } WriteLine(); }
运行应用程序以查看已Added状态与这四个对象:
type: MenuCard, state: Added, Soups type: Menu, state: Added, Consommé Célestine (with shredded pancake) type: Menu, state: Added, Baked Potato Soup type: Menu, state: Added, Cheddar Broccoli Soup
处于这种状态的对象都将被SaveChangesAsync方法创建SQL Insert语句写入数据库。
对象跟踪
可以看到上下文掌握所有被添加的对象。但上下文还需要知道所作的更改。要知道更改,检索的每个对象都需要其在上下文中的状态。为了看到这一点,我们创建两个返回相同对象的不同查询。以下代码段定义了两个不同的查询,其中每个查询返回相同的对象,即存储在数据库中的Menus。实际上,只有一个对象被实现,如同第二查询结果一样,检测返回的记录具有与已经从上下文引用的对象相同的主键值。验证引用变量m1和m2是否返回相同的对象(代码文件MenusSample / Program.cs):
private static void ObjectTracking() { using (var context = new MenusContext()) { var m1 = (from m in context.Menus where m.Text.StartsWith("Con") select m).FirstOrDefault(); var m2 = (from m in context.Menus where m.Text.Contains("(") select m).FirstOrDefault(); if (object.ReferenceEquals(m1, m2)) { WriteLine("the same object"); } else { WriteLine("not the same"); } ShowState(context); } }
第一个LINQ查询返回含有比较关键字 LIKE 的SQL SELECT语句的结果,即以字符串“Con”开始的值:
SELECT TOP(1) [m].[MenuId], [m].[MenuCardId], [m].[Price], [m].[Text] FROM [mc].[Menus] AS [m] WHERE [m].[Text] LIKE 'Con' + '%'
第二个LINQ查询同样需要查询数据库。比较关键字 LIKE 以比较“(”在文本中间:
SELECT TOP(1) [m].[MenuId], [m].[MenuCardId], [m].[Price], [m].[Text] FROM [mc].[Menus] AS [m] WHERE [m].[Text] LIKE ('%' + '(') + '%'
运行应用程序相同的对象将写入控制台,并且ChangeTracker只保留一个对象。状态是Unchanged:
the same object type: Menu, state:Unchanged, Consommé Cé lestine(with shredded pancake)
如果不需要跟踪数据库运行查询的对象,可以使用DbSet调用 AsNoTracking 方法:
var m1 = (from m in context.Menus.AsNoTracking() where m.Text.StartsWith("Con") select m).FirstOrDefault();
还可以将ChangeTracker的默认跟踪行为配置为QueryTrackingBehavior.NoTracking:
using (var context = new MenusContext()) { context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
使用以上的配置,数据库进行两个查询,两个对象实现,并且状态信息为空。
注意 当上下文仅用于读取记录且没有更改时,使用NoTracking配置非常有用。因为不保持状态信息,可以减少上下文的开销。
更新对象
跟踪对象时,可以轻松地更新对象,如以下代码段所示。首先,检索Menu对象。使用此跟踪对象,在将更改写入数据库之前,会修改Price。所有更改的状态信息将输出到控制台(代码文件MenusSample / Program.cs):
private static async Task UpdateRecordsAsync() { using (var context = new MenusContext()) { Menu menu = await context.Menus .Skip(1) .FirstOrDefaultAsync(); ShowState(context); menu.Price += 0.2m; ShowState(context); int records = await context.SaveChangesAsync(); WriteLine($"{records} updated"); ShowState(context); } }
运行应用程序可以看到对象的状态,在加载记录后为 Unchanged,属性值更改后为 Modified,保存完成后为 Unchanged:
type: Menu, state: Unchanged, Baked Potato Soup type: Menu, state: Modified, Baked Potato Soup 1 updated type: Menu, state: Unchanged, Baked Potato Soup
从跟踪器访问实体时,默认情况下会自动检测更改。可以通过设置ChangeTracker的AutoDetectChangesEnabled属性进行配置。要手动查看是否已完成更改,可以调用方法DetectChanges。通过调用SaveChangesAsync,状态将改为Unchanged。可以通过调用AcceptAllChanges方法手动执行此操作。
更新未跟踪对象
对象上下文的生存周期通常是短暂的。通过ASP.NET MVC使用Entity Framework,一个HTTP请求创建一个对象上下文去检索对象。从客户端收到更新时必须再次在服务器上创建对象。该对象不与对象上下文相关联。要在数据库中更新它,该对象需要与数据上下文相关联,并且需要更改状态去创建INSERT,UPDATE或DELETE语句。
下一个代码段用来模拟这样的场景。 GetMenuAsync方法返回一个与上下文断开的Menu对象,在方法的结尾上下文被释放(代码文件MenusSample / Program.cs):
private static async Task<Menu> GetMenuAsync() { using (var context = new MenusContext()) { Menu menu = await context.Menus .Skip(2) .FirstOrDefaultAsync(); return menu; } }
GetMenuAsync方法由方法ChangeUntrackedAsync调用。该方法可以更改与任意上下文无关的Menu对象。更改后,将Menu对象传递给UpdateUntrackedAsync方法,将其保存在数据库中(代码文件MenusSample / Program.cs):
private static async Task ChangeUntrackedAsync() { Menu m = await GetMenuAsync(); m.Price += 0.7m; await UpdateUntrackedAsync(m); }
方法UpdateUntrackedAsync接收更新的对象,需要附加到上下文中。上下文附加对象的一种方法是调用DbSet的Attach方法,并根据需要设置状态。 Update方法同时执行一个调用:附加对象并将状态设置为Modified(代码文件MenusSample / Program.cs):
private static async Task UpdateUntrackedAsync(Menu m) { using (var context = new MenusContext()) { ShowState(context); // EntityEntry<Menu> entry = context.Menus.Attach(m); // entry.State = EntityState.Modified; context.Menus.Update(m); ShowState(context); await context.SaveChangesAsync(); } }
运行ChangeUntrackedAsync方法的应用程序,可以看到状态已被更改。该对象最初未被跟踪,但由于状态已明确更新,所以可以看到 Modified 状态:
type: Menu, state: Modified, Cheddar Broccoli Soup
冲突处理
试想如果多个用户同时更改相同的记录,然后保存状态会怎么样?最后哪个成功保存更改?
如果访问同一数据库的多个用户在不同的记录上工作,是没有冲突的,所有用户都可以保存其数据,也不会干扰其他用户编辑的数据。但是,如果多个用户在同一个记录上工作,那么就需要考虑解决冲突的方案了。处理这个问题有很多不同的方法。最简单的一个是,最后一个操作保存成功。最后保存数据的用户将覆盖先执行更改的用户操作。
Entity Framework还提供了选择第一个用户成功的方式。使用此选项,在保存记录时如果最初读取的数据仍在数据库中,则需要进行验证。如果验证通过,读、写期间数据没有更改,可以继续保存数据。但是,如果数据更改,则需要执行冲突解决。
让我们进入这些不同的选项。
保存最后一个操作
默认情况是,最后一个操作保存成功。为了查看对数据库的多个访问,扩展了BooksSample应用程序。
为了容易模拟两个用户,方法ConflictHandlingAsync调用PrepareUpdateAsync方法两次,对引用同一记录的两个Book对象进行不同的更改,并调用UpdateAsync方法两次。最后,图书ID传递到CheckUpdateAsync方法,该方法显示来自数据库的图书的实际状态(代码文件BooksSample / Program.cs):
public static async Task ConflictHandlingAsync() { // user 1 Tuple<BooksContext, Book> tuple1 = await PrepareUpdateAsync(); tuple1.Item2.Title ="updated from user 1"; // user 2 Tuple<BooksContext, Book> tuple2 = await PrepareUpdateAsync(); tuple2.Item2.Title ="updated from user 2"; // user 1 await UpdateAsync(tuple1.Item1, tuple1.Item2); // user 2 await UpdateAsync(tuple2.Item1, tuple2.Item2); context1.Item1.Dispose(); context2.Item1.Dispose(); await CheckUpdateAsync(tuple1.Item2.BookId); }
PrepareUpdateAsync方法打开一个BookContext,并返回元组(Tuple)类型的上下文和Book对象。留意该方法被调用了两次,并且返回与不同上下文对象相关联的不同Book对象(代码文件BooksSample / Program.cs):
private static async Task<Tuple<BooksContext, Book>> PrepareUpdateAsync() { var context = new BooksContext(); Book book = await context.Books .Where(b => b.Title =="Conflict Handling") .FirstOrDefaultAsync(); return Tuple.Create(context, book); }
注意 元组在第7章“数组和元组”中进行了解释。
UpdateAsync方法接收了已打开的BooksContext与已更新的Book对象,将其保存到数据库。留意这个方法同样也被调用两次(代码文件BooksSample / Program.cs):
private static async Task UpdateAsync(BooksContext context, Book book) { await context.SaveChangesAsync(); WriteLine($"successfully written to the database: id {book.BookId}" + $"with title {book.Title}"); }
CheckUpdateAsync方法将指定 id 的图书输出控制台(代码文件BooksSample / Program.cs):
private static async Task CheckUpdateAsync(int id) { using (var context = new BooksContext()) { Book book = await context.Books .Where(b => b.BookId == id) .FirstOrDefaultAsync(); WriteLine($"updated: {book.Title}"); } }
运行应用程序时会发生什么?可以看到第一次更新是成功的,第二次更新也是如此。此示例应用程序的情况是,在更新记录时,不会验证在读取记录后是否发生任何更改。只是第二次更新覆盖了第一次更新的数据,可以看到应用程序输出:
successfully written to the database: id 7038 with title updated from user 1
successfully written to the database: id 7038 with title updated from user 2
updated: updated from user 2
保存第一个操作
如果需要不同的行为,例如第一个用户的更改保存到记录,则需要进行一些更改。示例项目ConflictHandlingSample使用像之前一样的Book和BookContext对象,但它处理first-one-wins方案。
此示例应用程序使用以下依赖项和命名空间:
依赖项
NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
命名空间
Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.ChangeTracking System System.Linq System.Text System.Threading.Tasks static System.Console
对于冲突解决,需要指定属性,使用并发令牌验证读取和更新之间是否已发生更改。基于指定的属性,修改SQL UPDATE语句以不仅验证主键,还验证并发令牌中的所有属性。向实体类型添加许多并发令牌会使用UPDATE语句创建一个巨大的WHERE子句,这不是很有效率。但可以在每个UPDATE语句添加一个由SQL Server更新的属性 - 这是对Book类做的。属性TimeStamp在SQL Server中定义为timeStamp(代码文件ConflictHandlingSample / Book.cs):
public class Book { public int BookId { get; set; } public string Title { get; set; } public string Publisher { get; set; } public byte[] TimeStamp { get; set; } }
要在SQL Server中将TimeStamp属性定义为时间戳类型,可以使用Fluent API。 SQL数据类型使用HasColumnType方法定义。每个SQL INSERT或UPDATE语句的TimeStamp属性都会更改,方法ValueGeneratedOnAddOrUpdate通知上下文,同时在这些操作后需要使用上下文设置。 IsConcurrencyToken方法根据需要标记此属性,以检查它在读取后是否没有更改(代码文件ConflictHandlingSample / BooksContext.cs):
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); var book = modelBuilder.Entity<Book>(); book.HasKey(p => p.BookId); book.Property(p => p.Title).HasMaxLength(120).IsRequired(); book.Property(p => p.Publisher).HasMaxLength(50); book.Property(p => p.TimeStamp) .HasColumnType("timestamp") .ValueGeneratedOnAddOrUpdate() .IsConcurrencyToken(); }
注意 不仅可以在Fluent API 中使用IsConcurrencyToken方法,也可以将属性ConcurrencyCheck应用于要检查并发性的属性。
冲突处理检查的过程类似于前面所做的。用户1和用户2调用PrepareUpdateAsync方法,更改书名,并调用UpdateAsync方法将更改保存到数据库(代码文件ConflictHandlingSample / Program.cs):
public static async Task ConflictHandlingAsync() { // user 1 Tuple<BooksContext, Book> tuple1 = await PrepareUpdateAsync(); tuple1.Item2.Title ="user 1 wins"; // user 2 Tuple<BooksContext, Book> tuple2 = await PrepareUpdateAsync(); tuple2.Item2.Title ="user 2 wins"; // user 1 await UpdateAsync(tuple1.Item1, tuple1.Item2); // user 2 await UpdateAsync(tuple2.Item1, tuple2.Item2); context1.Item1.Dispose(); context2.Item1.Dispose(); await CheckUpdateAsync(context1.Item2.BookId); }
此处不重复使用PrepareUpdateAsync方法,因为此方法以与上一个示例相同的方式实现。不同的是UpdateAsync方法。要查看不同的时间戳,在更新之前和之后,自定义扩展方法StringOutput 实现字节数组以可读形式输出到控制台。接下来将显示调用ShowChanges辅助方法对Book对象进行更改。调用SaveChangesAsync方法将所有更新写入数据库。如果更新失败产生DbUpdateConcurrencyException,则会向控制台输出有关失败的信息(代码文件ConflictHandlingSample / Program.cs):
private static async Task UpdateAsync(BooksContext context, Book book, string user) { try { WriteLine($"{user}: updating id {book.BookId}," + $"timestamp: {book.TimeStamp.StringOutput()}");ShowChanges(book.BookId, context.Entry(book)); int records = await context.SaveChangesAsync(); WriteLine($"{user}: updated {book.TimeStamp.StringOutput()}"); WriteLine($"{user}: {records} record(s) updated while updating" + $"{book.Title}"); } catch (DbUpdateConcurrencyException ex) { WriteLine($"{user}: update failed with {book.Title}"); WriteLine($"error: {ex.Message}"); foreach (var entry in ex.Entries) { Book b = entry.Entity as Book; WriteLine($"{b.Title} {b.TimeStamp.StringOutput()}"); ShowChanges(book.BookId, context.Entry(book)); } } }
上下文相关联的对象用PropertyEntry对象访问原始值和当前值。从数据库读取对象时可以用OriginalValue属性访问检索的原始值,用CurrentValue属性访问当前值。用EntityEntry属性方法访问 PropertyEntry对象,如下所示ShowChanges和ShowChange方法(代码文件ConflictHandlingSample / Program.cs):
private static void ShowChanges(int id, EntityEntry entity) { ShowChange(id, entity.Property("Title")); ShowChange(id, entity.Property("Publisher")); } private static void ShowChange(int id, PropertyEntry propertyEntry) { WriteLine($"id: {id}, current: {propertyEntry.CurrentValue}," + $"original: {propertyEntry.OriginalValue}," + $"modified: {propertyEntry.IsModified}"); }
定义扩展方法StringOutput来将从SQL Server更新的TimeStamp属性的字节数组转换为可视输出,(代码文件ConflictHandlingSample / Program.cs):
static class ByteArrayExtension { public static string StringOutput(this byte[] data) { var sb = new StringBuilder(); foreach (byte b in data) { sb.Append($"{b}."); } return sb.ToString(); } }
运行应用程序可以看到如下输出。时间戳值和图书ID每次运行都不相同。第一个用户将标题“ sample book”的书更新为新标题并且保存。 Title属性的 IsModified 属性返回true,但 Publisher属性的 IsModified 返回false,因为只有标题已更改。原始时间戳以1.1.209结束;在更新到数据库之后,时间戳记更改为1.17.114。同时,用户2打开同一记录,这本书的时间戳仍1.1.209。用户2尝试更新该图书信息,但此处更新失败,因为此图书的时间戳与数据库的时间戳不匹配,会抛出DbUpdateConcurrencyException异常。在异常处理程序中,异常的原因输出到控制台,可以在程序输出中看到:
user 1: updating id 17, timestamp 0.0.0.0.0.1.1.209. id: 17, current: user 1 wins, original: sample book, modified: True id: 17, current: Sample, original: Sample, modified: False user 1: updated 0.0.0.0.0.1.17.114. user 1: 1 record(s) updated while updating user 1 wins user 2: updating id 17, timestamp 0.0.0.0.0.1.1.209. id: 17, current: user 2 wins, original: sample book, modified: True id: 17, current: Sample, original: Sample, modified: False user 2 update failed with user 2 wins user 2 error: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions. user 2 wins 0.0.0.0.0.1.1.209. id: 17, current: user 2 wins, original: sample book, modified: True id: 17, current: Sample, original: Sample, modified: False updated: user 1 wins
使用并发令牌和处理DbConcurrencyException时,可以根据需要处理并发冲突。例如,可以自动解决并发问题。如果更改了不同的属性,可以检索更改的记录并合并更改。如果更改的属性是进行某些计算的数字(例如,点系统),则可以从这两个更新中增加或减少值,如果达到限制,则抛出异常。还可以向用户提供数据库中当前的信息后要求用户解决并发问题,询问用户想要做什么更改。但不要问用户询问太多。很有可能用户唯一需要的是摆脱这个极少显示的对话框,这意味着用户可能不阅读内容就单击确定或取消。对于罕见的冲突,还可以写入日志并通知系统管理员需要解决问题。
使用事务
第37章介绍了事务的编程。每次使用 Entity Framework 访问数据库都涉及事务。可以隐式使用事务或根据需要使用配置显式创建事务。本节中使用的示例项目以两种方式演示事务。Menu,MenuCard和MenuContext类如前所示用于MenusSample项目。此示例应用程序使用以下依赖项和命名空间:
依赖项
NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
命名空间
Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.Storage System.Linq System.Threading System.Threading.Tasks static System.Console
使用隐式事务
调用SaveChangesAsync方法会自动解析为一个事务。如果需要完成的更改的一部分失败,例如,由于数据库约束,所有已完成的更改都将回滚。通过以下代码段演示:使用有效数据创建第一个Menu(m1)。通过提供MenuCardId来对现有MenuCard的引用完成。更新成功后,菜单m1的MenuCard属性自动填充。但是创建第二个 Menu mInvalid 时 ,引用一个无效的 Menu Card , 并设置 MenuCardId 为比数据库中可用的最高ID高一个值 (译者注:自增1) 。由于MenuCard和Menu之间定义的外键关系,添加此对象将失败(代码文件TransactionsSample / Program.cs):
private static async Task AddTwoRecordsWithOneTxAsync() { WriteLine(nameof(AddTwoRecordsWithOneTxAsync)); try { using (var context = new MenusContext()) { var card = context.MenuCards.First(); var m1 = new Menu { MenuCardId = card.MenuCardId, Text ="added", Price = 99.99m }; int hightestCardId = await context.MenuCards.MaxAsync(c => c.MenuCardId); var mInvalid = new Menu { MenuCardId = ++hightestCardId, Text ="invalid", Price = 999.99m }; context.Menus.AddRange(m1, mInvalid); int records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); } } catch (DbUpdateException ex) { WriteLine($"{ex.Message}"); WriteLine($"{ex?.InnerException.Message}"); } WriteLine(); }
调用方法AddTwoRecordsWithOneTxAsync运行应用程序后,查看数据库的内容验证,没有一条记录被添加。异常消息以及异常的内部消息给出了详细信息:
AddTwoRecordsWithOneTxAsync An error occurred while updating the entries. See the inner exception for details. The INSERT statement conflicted with the FOREIGN KEY constraint"FK_Menu_MenuCard_MenuCardId". The conflict occurred in database"MenuCards", table"mc.MenuCards", column 'MenuCardId'.
如果将第一条记录写入数据库应该成功,即使第二条记录写入失败,必须多次调用SaveChangesAsync方法,如下面的代码段所示。在方法AddTwoRecordsWithTwoTxAsync中,第一次调用SaveChangesAsync插入m1菜单对象,而第二次调用尝试插入mInvalid菜单对象(代码文件TransactionsSample / Program.cs):
private static async Task AddTwoRecordsWithTwoTxAsync() { WriteLine(nameof(AddTwoRecordsWithTwoTxAsync)); try { using (var context = new MenusContext()) { var card = context.MenuCards.First(); var m1 = new Menu { MenuCardId = card.MenuCardId, Text ="added", Price = 99.99m }; context.Menus.Add(m1); int records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); int hightestCardId = await context.MenuCards.MaxAsync(c => c.MenuCardId); var mInvalid = new Menu { MenuCardId = ++hightestCardId, Text ="invalid", Price = 999.99m }; context.Menus.Add(mInvalid); records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); } } catch (DbUpdateException ex) { WriteLine($"{ex.Message}"); WriteLine($"{ex?.InnerException.Message}"); } WriteLine(); }
运行应用程序时,第一个INSERT语句添加成功,当然第二个会导致DbUpdateException。可以查看数据库验证,此次添加了一条记录:
AddTwoRecordsWithTwoTxAsync 1 records added An error occurred while updating the entries. See the inner exception for details. The INSERT statement conflicted with the FOREIGN KEY constraint"FK_Menu_MenuCard_MenuCardId". The conflict occurred in database"MenuCards", table"mc.MenuCards", column 'MenuCardId'.
创建显式事务
除了隐式创建事务,也可以显式地创建它们。这提供了一个优点,即可以选择回滚,以防某些业务逻辑失败,并且可以在一个事务中合并多个SaveChangesAsync调用。要启动DbContext派生类相关联的事务,需要调用从Database属性返回的DatabaseFacade类的BeginTransactionAsync方法。事务返回接口IDbContextTransactio的实现。用关联的DbContext完成的SQL语句加入到事务中。要提交或回滚,必须显式调用方法Commit或Rollback。示例代码中,在达到DbContext作用域结束时执行Commit,发生异常则回滚(代码文件TransactionsSample / Program.cs)的情况下完成:
private static async Task TwoSaveChangesWithOneTxAsync() { WriteLine(nameof(TwoSaveChangesWithOneTxAsync)); IDbContextTransaction tx = null; try { using (var context = new MenusContext()) using (tx = await context.Database.BeginTransactionAsync()) { var card = context.MenuCards.First(); var m1 = new Menu { MenuCardId = card.MenuCardId, Text ="added with explicit tx", Price = 99.99m }; context.Menus.Add(m1); int records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); int hightestCardId = await context.MenuCards.MaxAsync(c => c.MenuCardId); var mInvalid = new Menu { MenuCardId = ++hightestCardId, Text ="invalid", Price = 999.99m }; context.Menus.Add(mInvalid); records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); tx.Commit(); } } catch (DbUpdateException ex) { WriteLine($"{ex.Message}"); WriteLine($"{ex?.InnerException.Message}"); WriteLine("rolling back…"); tx.Rollback(); } WriteLine(); }
运行应用程序可以看到没有添加任何记录,但SaveChangesAsync方法被多次调用。第一次返回SaveChangesAsync时,会将一条记录列为已添加的记录,但此记录基于Rollback稍后被移除。根据设置的隔离级别,更新的记录只能在事务内完成回滚之前查看,不能在事务外部查看。
TwoSaveChangesWithOneTxAsync 1 records added An error occurred while updating the entries. See the inner exception for details. The INSERT statement conflicted with the FOREIGN KEY constraint"FK_Menu_MenuCard_MenuCardId". The conflict occurred in database"MenuCards", table"mc.MenuCards", column 'MenuCardId'. rolling back…
注意通过BeginTransactionAsync方法,还可以提供隔离级别的值去指定数据库中所需的隔离要求和锁定。隔离级别在第37章中做了讨论。
总结
本章介绍了Entity Framework Core的功能。了解对象上下文如何保存有关检索和更新的实体的情况,以及如何将更改写入数据库。了解如何使用迁移用C#代码创建和更改数据库结构。了解如何使用数据批注来完成数据库映射去定义结构,还看到了与批注相比提供更多功能的Fluent API。
多个用户在同一个记录上工作时对冲突做出反应的可能性,隐式或显式地使用事务进行事务控制。
下一章将展示利用Windows Services 创建一个系统自动启动的程序,可以在Windows服务中使用Entity Framework。
(本章完)