译文,我的原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(下)),不对的地方欢迎指出与交流。 html
章节出自《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): async
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)ide
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状态与这四个对象:post
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。
(本章完)