C# 6 与 .NET Core 1.0 高级编程

译文,个人原创,转载请注明出处(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 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 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 tuple1 = await PrepareUpdateAsync();
  tuple1.Item2.Title ="updated from user 1";
 
  // user 2
  Tuple 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> 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.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 tuple1 = await PrepareUpdateAsync();
  tuple1.Item2.Title ="user 1 wins";
 
  // user 2
  Tuple 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。

 

(本章完)

你可能感兴趣的:(C# 6 与 .NET Core 1.0 高级编程)