实体框架核心现代数据访问教程(五)

原文:Modern Data Access with Entity Framework Core

协议:CC BY-NC-SA 4.0

十七、性能调优

本章提供了使用实体框架核心加速数据库访问的指导。

实体框架核心中性能优化的过程模型

与经典的实体框架一样,以下用于优化数据库访问性能的流程模型已经在实体框架核心中得到证明:

  • 第一步是使用实体框架核心和 LINQ 实现所有数据访问,除了UPDATEDELETEINSERT批量操作。批量操作直接使用 SQL 或批量插入。
  • 然后用真实的数据集测试应用。速度太慢的地方,分三个阶段优化。
    • 在阶段 1 中,使用了实体框架核心中的技巧,例如无跟踪、缓存、分页、改变加载策略(例如,急切加载或预加载而不是显式加载),以及减少往返次数。
    • 如果这还不够,在第 2 阶段,将检查 LINQ 命令,并替换为优化更好的 SQL 命令或其他数据库结构,如视图、存储过程或表值函数(tvf)。通过实体框架核心继续访问它们。
    • 只有在阶段 3 中,实体框架核心才被用于访问 SQL 命令、视图、存储过程和 tvf 的DataReaderCommand对象所取代。

您自己的性能测试的最佳实践

如果您正在执行性能测试以检查不同替代方案的速度,请考虑以下事项:

  • 不要在 Visual Studio 中运行性能测试。调试器和可能激活的 IntelliTrace 功能会以不同的方式降低程序代码的速度,这取决于过程,并且您不会收到绝对正确或按比例正确的结果。
  • 不要在图形用户界面(GUI)应用中运行性能测试,也不要进行任何控制台输出。为性能测试编写一个控制台应用,但是不要将任何内容打印到控制台,并且不要将控制台输出的时间包括在您的度量中。
  • 每个测试重复几次,至少十次,计算平均值。有许多因素会影响结果(例如。NET 垃圾收集和 Windows 分页文件)。
  • 不要将第一次运行(冷启动)包括在平均值中。实体框架核心和数据库管理系统的额外任务必须在第一次运行时完成(例如,启动数据库、建立连接、生成映射代码等等),这会伪造您的结果。如果你想得到十个有效结果,就做十一次。
  • 在不同的系统上测试远程数据库管理系统(除非您的解决方案确实使用本地数据库)。
  • 确保测试机器在测试过程中不执行任何其他重要的进程,并且网络不明显活跃(当其他人都在家时运行测试,或者设置您自己的网络!).

中各种数据访问技术的性能比较。网

当开发实体框架核心时,微软的目标是平台独立性和在经典的 ADO.NET 实体框架上提高性能。本章涵盖了许多性能测试场景中的一个。

图 17-1 显示实体框架核心 1.1。无跟踪模式下的. NET Framework 4.7 几乎与手动映射的DataReader一样快(换句话说,从DataReader复制到. NET 对象:obj.x = Convert (dr ["x"]))。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-1

Performance comparison of various data access techniques in .NET

和。NET Core 2.0,无论是手动映射还是实体框架核心都比。NET 框架 4.7。有趣的是,实体框架核心的无跟踪模式大大受益于。NET Core 2.0,相当于在。网芯 2.0。

同样在跟踪模式下,实体框架核心比 ADO.NET 实体框架 6.x 更快

最快的测量(22 毫秒。NET Framework 4.7 或 21 毫秒。NET Core 2.0)来自于一个DataReader,但是数据集没有被映射到对象。

请注意以下关于此测量场景的内容:

  • 数据库服务器在 Windows Server 2016 上运行 Microsoft SQL Server 2016(虚拟化)。如果数据库服务器没有被虚拟化,或者至少可以使用非虚拟化的硬盘空间,那么它的性能会更好。
  • 客户端是 Windows 10 机器。
  • 两台计算机通过 1GB 以太网连接。
  • 执行的命令是一个简单的SELECT,没有连接或聚合操作符。
  • 加载了 10,000 条记录。
  • 结果记录由单个表中的 13 列组成。
  • 数据类型有intsmallintnvarchar(30)nvarchar(max)bittimestamp
  • 显示的值是 100 次重复的平均值。
  • 每种技术(冷启动)的第一次访问不包括在平均值中。

Note

当然,这只是众多可能的对比场景之一。由于性能取决于硬件、操作系统、软件版本,尤其是数据库模式,因此在这一点上记录进一步的性能比较没有意义。无论如何,您必须在您的特定场景中衡量性能。

优化对象分配

要分配相关对象,实体框架核心为您提供了两个选项。

  • 通过对象引用的赋值(清单 17-1
  • 通过外键属性的赋值(清单 17-2
  public static void ChangePilotUsingObjectAssignment()
  {
   CUI.Headline(nameof(ChangePilotUsingObjectAssignment));
   var flightNo = 102;
   var newPilotID = 123;

   using (var ctx = new WWWingsContext())
   {
    ctx.Log();
    Flight flight = ctx.FlightSet.Find(flightNo);
    Pilot newPilot = ctx.PilotSet.Find(newPilotID);
    flight.Pilot = newPilot;
    var count = ctx.SaveChanges();
    Console.WriteLine("Number of saved changes: " + count);
   }
  }

Listing 17-1Assignment via an Object Reference

  public static void ChangePilotUsingFK()
  {
   CUI.Headline(nameof(ChangePilotUsingFK));
   var flightNo = 102;
   var newPilotID = 123;

   using (var ctx = new WWWingsContext())
   {
    ctx.Log();
    Flight flight = ctx.FlightSet.Find(flightNo);
    flight.PilotId = newPilotID;
    var count = ctx.SaveChanges();
    Console.WriteLine("Number of saved changes: " + count);
   }

  }

Listing 17-2Assignment via a Foreign Key Property

使用外键属性会更有效一些,因为不必显式加载对象。实体框架核心发送给数据库的UPDATE命令在两种情况下看起来是一样的(见图 17-2 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-2

Output of Listing 17-2

如果要分配的对象已经在 RAM 中,您可以在两种语法形式之间进行选择。但是如果在 RAM 中只有要分配的对象的主键值,那么应该使用外键属性。这种情况在 web 应用和 web 服务中很常见,因为客户端只获得主键值。

Tip

外键属性在实体框架核心中是可选的。然而,赋值中的优化选项是除了实体类中的导航属性之外,还应该实现外键属性的一个很好的理由!

虽然没有外键属性,但是您可以通过使用实体框架核心中外键列的 shadow 属性来绕过加载对象(参见清单 17-3 )。然而,这个过程的缺点是外键列的名称必须在程序代码中作为字符串使用,这在输入时更麻烦,也更容易出错。

  public static void ChangePilotUsingFKShadowProperty()
  {
   CUI.Headline(nameof(ChangePilotUsingFKShadowProperty));
   var flightNo = 102;
   var neuerPilotNr = 123;
   using (var ctx = new WWWingsContext())
   {
    ctx.Log();
    Flight flight = ctx.FlightSet.Find(flightNo);
    ctx.Entry(flight).Property("PilotId").CurrentValue = neuerPilotNr;
    var count = ctx.SaveChanges();
    Console.WriteLine("Number of saved changes: " + count);
   }
  }
Listing 17-3Assignment via the Shadow Key Property of the Foreign Key Column

批量操作

Entity Framework Core 在其默认配置中不支持对多条记录进行批量操作,而是单独处理每条记录以进行删除和修改。本章讨论了对 1,000 条记录使用删除命令(DELETE)的主题。该信息也适用于使用UPDATE的批量数据变更。

单次删除

清单 17-4 显示了一种从Flight表中删除主键的值大于 20,000 的所有记录的低效方法。必须首先将所有记录加载并具体化到。NET 对象。每个。然后,使用Remove()方法将. NET 对象标记为删除,当对每个对象执行SaveChanges()方法时,删除最终被实体框架核心上下文转换为DELETE命令。

所以,如果要删除 1000 个航班,就需要 1001 个命令(1 个SELECT和 1000 个DELETE命令)。在实现中,1000 个DELETE命令,一次一个,被发送到数据库管理系统,因为SaveChanges()方法出现在循环中每个单独的Remove()之后。

  public static void BulkDeleteEFCAPIwithoutBatching()
  {
   CUI.Headline(nameof(BulkDeleteEFCAPIwithoutBatching));
   var sw = new Stopwatch();
   sw.Start();
   int total = 0;
   using (var ctx = new WWWingsContext())
   {
    var min = 20000;
    var flightSet = ctx.FlightSet.Where(x => x.FlightNo >= min).ToList();
    foreach (Flight f in flightSet)
    {
     ctx.FlightSet.Remove(f);
     var count = ctx.SaveChanges();
     total += count;
    }
   }
   sw.Stop();
   Console.WriteLine("Number of DELETE statements: " + total);
   Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
  }
Listing 17-4Bulk Clear Without Batching with the Entity Framework Core API

批处理优化

在经典的实体框架中,在每个Remove()之后SaveChanges()是在循环中还是在循环之外并不重要。旧的 OR 映射器总是一次一个地将每个DELETE命令传输到数据库管理系统。在实体框架核心中,有批处理(见第章第十部分)来缓解这个问题。

清单 17-5 ,它在最后只执行SaveChanges()一次,因此不会导致 1000 次DELETE到数据库管理系统的往返,而是只有一次(见图 17-3 )。总共只剩下两个往返行程(一个用于加载SELECT,一个用于 1000 个DELETE命令)。执行时间显著减少(见表 17-1 )。

表 17-1

Execution Time

| 方法 | 往返次数 | 执行时间 | | :-- | :-- | :-- | | 批量删除 1,000 条`Flight`记录,无需使用实体框架核心 API 进行批处理 | One thousand and one | 11110 秒 | | 使用实体框架核心 API 批量删除 1,000 条`Flight`记录 | Two | 3395 秒 |

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-3

Here, 1,000 delete commands are transferred in one round-trip to the database management system

public static void BulkDeleteEFCAPIwithBatching()
{
 CUI.Headline(nameof(BulkDeleteEFCAPIwithBatching));
 int total = 0;
 var sw = new Stopwatch();
 sw.Start();
 using (var ctx = new WWWingsContext())
 {
  var min = 20000;
  var flightSet = ctx.FlightSet.Where(x => x.FlightNo >= min).ToList();
  foreach (Flight f in flightSet)
  {
   ctx.FlightSet.Remove(f);
  }
  total = ctx.SaveChanges();
 }
 sw.Stop();
 Console.WriteLine("Number of DELETE statements: " + total);
 Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
}
Listing 17-5Bulk Deleting Batching with the Entity Framework Core API

删除而不加载伪对象

即使使用批处理,这种操作仍然是低效的,因为最初所有记录都是在删除后才加载的。运行实体框架时,核心需要一个实体对象。

这里有一个技巧,在 RAM 中手动构造这样一个实体对象作为伪对象,并将其作为参数传递给Remove()(参见清单 17-6 )。这显著提高了性能(见表 17-2 )。然而,这只有在以下两种情况下才有可能:

  • 要删除的对象的主键是已知的。
  • 没有通过[ConcurrencyCheck]运行的并发检查,没有IsConcurrencyToken()方法,也没有时间戳列。
public static void BulkDeleteEFCAPIusingPseudoObject()
{
 CUI.Headline(nameof(BulkDeleteEFCAPIusingPseudoObject));
 int total = 0;
 var sw = new Stopwatch();
 sw.Start();
 using (var ctx = new WWWingsContext())
 {
  for (int i = 20001; i < 21000; i++)
  {
   var f = new Flight() { FlightNo = i };
   ctx.FlightSet.Attach(f);
   ctx.FlightSet.Remove(f);
  }
 total = ctx.SaveChanges();
 }
 sw.Stop();
 Console.WriteLine("Number of DELETE statements: " + total);
 Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
}
Listing 17-6Bulk Deleting Batching with Entity Framework Core API Using Pseudo-Objects

表 17-2

Execution Time

| 方法 | 往返次数 | 执行时间 | | :-- | :-- | :-- | | 批量删除 1,000 条`Flight`记录,无需使用实体框架核心 API 进行批处理 | One thousand and one | 11110 秒 | | 使用实体框架核心 API 批量删除 1,000 条`Flight`记录 | Two | 3395 秒 | | 使用伪对象通过实体框架核心 API 批量删除 1,000 条`Flight`记录 | one | 0.157 秒 |

使用经典 SQL 代替实体框架核心 API

通过迄今为止采取的措施,执行时间已经大大缩短,但时间仍然比必要的长得多。这里不使用实体框架核心 API,在要删除一组连续记录的情况下,发出一个简单的经典 SQL 命令要有效得多。

DELETE dbo.Flight where FlightNo >= 20000

您可以通过带有Parameter对象的 ADO.NET 命令以传统方式设置这个 SQL 命令(参见清单 17-7 ),或者更简洁地通过实体框架核心中的直接 SQL 支持(参见清单 17-8 )使用实体框架核心上下文的Database子对象中的ExecuteSqlCommand()方法来设置这个 SQL 命令。应该强调的是,基于string.Format()的通配符语法可以防止 SQL 注入攻击,如清单 17-7 中的参数化所示。这里,字符串不是简单地放在一起,如语法所示,而是在内部生成 SQL 参数对象。

在这两种情况下,执行时间都减少到不到 40 ms,这并不奇怪,因为程序现在只需要建立一个数据库连接和传输一些字符。无法测量通过实体框架核心上下文或Command对象的传输之间的性能差异。

当然,这种方法的缺点是再次使用 SQL 字符串,对此没有编译器检查,因此存在语法和类型错误的风险,直到运行时才被注意到。

| 方法 | 往返次数 | 执行时间 | | :-- | :-- | :-- | | 批量删除 1,000 条`Flight`记录,无需使用实体框架核心 API 进行批处理 | One thousand and one | 11110 秒 | | 使用实体框架核心 API 批量删除 1,000 条`Flight`记录 | Two | 3395 秒 | | 使用伪对象通过实体框架核心 API 批量删除 1,000 条`Flight`记录 | one | 0.157 秒 | | 通过实体框架上下文使用 SQL 批量删除 1,000 条`Flight`记录 | one | 0.034 秒 | | 通过带有参数的 ADO.NET 命令对象使用 SQL 批量删除 1000 条`Flight`记录 | one | 0.034 秒 |
public static void BulkDeleteADONETCommand()
  {
   CUI.Headline(nameof(BulkDeleteADONETCommand));
   int total = 0;
   var min = 20000;
   var sw = new Stopwatch();
   sw.Start();
   using (SqlConnection connection = new SqlConnection(Program.CONNSTRING))
   {
    connection.Open();
    SqlCommand command = new SqlCommand("DELETE dbo.Flight where FlightNo >= @min", connection);
    command.Parameters.Add(new SqlParameter("@min", min));
    total = command.ExecuteNonQuery();
   }
   sw.Stop();
   Console.WriteLine("Number of DELETE statements: " + total);
   Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
  }
Listing 17-7Bulk Delete with SQL via ADO.NET Command Object with a Parameter

public static void BulkDeleteEFCSQL()
{
 CUI.Headline(nameof(BulkDeleteEFCSQL));
 int total = 0;
 var min = 20000;
 var sw = new Stopwatch();
 sw.Start();
 using (var ctx = new WWWingsContext())
 {
  total = ctx.Database.ExecuteSqlCommand("DELETE dbo.Flight where FlightNo >= {0}", min);
 }
 sw.Stop();
 Console.WriteLine("Number of DELETE statements: " + total);
 Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
}
Listing 17-8Bulk Erase with SQL via Entity Framework Core Context

使用 EFPlus 进行质量删除的 Lambda 表达式

扩展组件 EFPlus(参见第二十章)允许基于 LINQ 命令在 lambda 表达式中公式化UPDATEDELETE命令,这样可以避免容易出错的 SQL。

EFPlus 组件实现了名为Update()Delete()的扩展方法。要实现这一点,使用Z.EntityFramework.Plus是必要的。

清单 17-9 展示了如何在 LINQ 命令中使用Delete()。不幸的是,如图 17-4 所示,EFPlus 生成的 SQL 命令并不理想。他们总是使用嵌套的SELECT,尽管这不是必须的。从 EFPlus 作者的角度来看,这是最简单的实现,因为很容易使用现有的SELECT命令生成实体框架核心。执行结果与使用直接 SQL 命令的结果相同(见表 17-3 )。

表 17-3

Execution Time

| 方法 | 往返次数 | 执行时间 | | :-- | :-- | :-- | | 批量删除 1,000 条`Flight`记录,无需使用实体框架核心 API 进行批处理 | One thousand and one | 11110 秒 | | 使用实体框架核心 API 批量删除 1,000 条`Flight`记录 | Two | 3395 秒 | | 使用伪对象通过实体框架核心 API 批量删除 1,000 条`Flight`记录 | one | 157 秒 | | 通过实体框架上下文使用 SQL 批量删除 1,000 条`Flight`记录 | one | 34 秒 | | 通过带有参数的 ADO.NET 命令对象使用 SQL 批量删除 1000 条`Flight`记录 | one | 34 秒 | | 使用 EFPlus 批量删除 1,000 条`Flight`记录 | one | 45 秒 |

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-4

SQL DELETE command that EFPlus sends to the database management system

public static void BulkDeleteEFPlus()
{
 CUI.Headline(nameof(BulkDeleteEFPlus));
 int min = 20000;
 int total = 0;
 var sw = new Stopwatch();
 sw.Start();
 using (var ctx = new WWWingsContext())
 {
  var count = ctx.FlightSet.Where(x => x.FlightNo >= min).Delete();
  Console.WriteLine("Number of DELETE statements: " + count);
 }
 sw.Stop();
 Console.WriteLine("Duration: " + sw.ElapsedMilliseconds);
 Timer_BulkDeleteEFPlus += sw.ElapsedMilliseconds;
}
Listing 17-9Mass Deletion with EFPlus

使用 EFPlus 批量更新

清单 17-10 显示了使用扩展方法Update()的附加组件 EFPlus(参见第二十章)制定的UPDATE命令,该命令将从柏林出发的未来航班的免费座位数减少一个。图 17-5 显示了发送到数据库管理系统的 SQL UPDATE命令,其工作方式与Delete()相同。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-5

SQL UPDATE command that EFPlus sends to the database management system

public static void BulkUpdateEFPlus()
{
 CUI.Headline(nameof(BulkUpdateEFPlus));
 using (var ctx = new WWWingsContext())
 {
  var count = ctx.FlightSet.Where(x => x.Departure == "Berlin" && x.Date >= DateTime.Now).Update(x => new Flight() { FreeSeats = (short)(x.FreeSeats - 1) });
  Console.WriteLine("Changed records: " + count);
 }
}
Listing 17-10Bulk Update with EFPlus

通过无跟踪实现性能优化

与它的前身 ADO.NET 实体框架一样,实体框架核心具有无跟踪模式,这大大加快了数据记录的加载速度。在新的实现中,微软通过添加上下文选项改进了该功能的实际应用。

图 17-6 中的性能测量显示,可选的无跟踪模式比标准跟踪模式提供了显著的速度优势——在传统的 ADO.NET 实体框架和新的实体框架核心中都是如此。在无跟踪模式下,实体框架核心可以在 46 毫秒内通过网络获取 10,000 条记录(来自一个表的 13 列,无连接、intsmallintnvarchar(30)nvarchar (max)bittimestamp),并将它们在 RAM 中具体化为对象。这几乎和手动映射的 ADO.NETDataReader一样快(比如obj.Name = Convert.ToString(dataReader["name"])这样的自写代码行)。在正常跟踪模式下,读取记录需要两倍多一点的时间(100 毫秒)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-6

Performance comparison

相比之下,图 17-6 也显示了 Entity Framework 6.x,这里在跟踪模式下需要 263 ms。在 53 毫秒时,与非跟踪模式下的实体框架核心相比,只有微小的差异。微软因此相对于 Entity Framework 6.1.3 加速了 Entity Framework Core,尤其是跟踪模式。尽管如此,实体框架核心中的无跟踪模式也有好处。

激活无跟踪模式

在经典实体框架的第一个版本中,您必须使用属性MergeOption为每个实体类或每个查询设置无跟踪模式,并附加一行代码。从 Entity Framework 4.1 开始,您可以使用更加优雅的AsNoTracking()扩展方法在查询级别设置模式(参见清单 17-11 )。在实体框架核心中,只有AsNoTracking()用于此。

   CUI.Headline("No-Tracking mode");
   using (WWWingsContext ctx = new WWWingsContext())
   {
    var flightSet = ctx.FlightSet.AsNoTracking().ToList();
    var flight = flightSet[0];
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
    flight.FreeSeats--;
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
    int count = ctx.SaveChanges();
    Console.WriteLine($"Saved changes: {count}"); // 0
   }
Listing 17-11Activation of No-Tracking Mode with AsNoTracking( ) in Entity Framework 6.x and Entity Framework Core

无跟踪模式的结果如图 17-7 所示,输出列表 17-11 。如果激活非跟踪模式,实体框架核心的更改跟踪功能将不再有效。在默认情况下,对象在加载后处于状态Unchanged,并且它们在改变后改变到状态Modified。当在无跟踪模式下加载时,它们在加载后是Detached,并且即使在改变后也保持如此。然后,SaveChanges()方法的执行不向数据库管理系统发送任何改变,因为实体框架核心没有注意到该改变。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-7

Screen output from Listing 17-11

无跟踪模式几乎总是可行的

在任何情况下,不跟踪模式应该总是用于只显示数据的对象,不应该进行任何修改。但是,即使您想要修改单个对象,您也可以首先以非跟踪模式加载对象,然后再将它们附加到上下文类。因此,您只需更改对象——最好是在更改之前——并使用Attach()方法将它们添加到上下文中。这个方法同时存在于DbContext类和dbSet<T>类中。

Attach()方法将一个对象添加到实体框架核心变更跟踪中。物体由此从状态Detached转移到状态Unchanged。当然,只有实体类的实例可以传递给Attach()。如果您传递实体框架核心上下文不知道的类的实例,您将得到下面的错误消息:“找不到实体类型 xy。请确保该实体类型已添加到模型中。

清单 17-12 (以及图 17-8 中附带的屏幕输出)显示了Attach()方法在这三个场景中的使用:

  • Attach()是在实际变更之前执行的。在这种情况下,没有其他事情要做,因为实体框架核心识别出Attach()之后的所有变化,并将对象独立地从状态Unchanged转移到状态Modified
  • 如果在执行Attach()之前发生了变化,那么实体框架核心对发生的变化一无所知。因此,您必须随后向ctx.Entry(obj).Property (f => f.Property).IsModified = true登记变更。
  • 如果您不知道对象的已更改属性(例如,因为更改发生在调用程序代码或另一个进程中),或者将单个属性设置为IsModified太麻烦,您可以使用ctx.Entry(Flight).State = EntityState.Modified来设置整个对象的状态。
  public static void TrackingMode_NoTracking_Attach()
  {

   CUI.MainHeadline(nameof(TrackingMode_NoTracking_Attach));

   CUI.Headline("Attach() before change");

   using (WWWingsContext ctx = new WWWingsContext())
   {
    var flightSet = ctx.FlightSet.AsNoTracking().ToList();
    var flight = flightSet[0];
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
    ctx.Attach(flight);
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Unchanged
    flight.FreeSeats--;
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
    int count = ctx.SaveChanges();
    Console.WriteLine($"Saved changes: {count}"); // 0
   }

   CUI.Headline("Attach() after change (change state per property)");
   using (WWWingsContext ctx = new WWWingsContext())
   {
    var flightSet = ctx.FlightSet.AsNoTracking().ToList();
    var flight = flightSet[0];
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
    flight.FreeSeats--;
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
    ctx.Attach(flight);
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Unchanged
    // Register changed property at EFC
    ctx.Entry(flight).Property(f => f.FreeSeats).IsModified = true;
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
    int count = ctx.SaveChanges();
    Console.WriteLine($"Saved changes: {count}"); // 1
   }

   CUI.Headline("Attach() after change (change state per object)");
   using (WWWingsContext ctx = new WWWingsContext())
   {
    var flightSet = ctx.FlightSet.AsNoTracking().ToList();
    var flight = flightSet[0];
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
    flight.FreeSeats--;
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
    ctx.Attach(flight);
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Unchanged
    ctx.Entry(flight).State = EntityState.Modified;
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
    int count = ctx.SaveChanges();
    Console.WriteLine($"Saved changes: {count}"); // 1
   }

  }

Listing 17-12Using the Attach( ) Method

如图 17-8 所示,在所有三种情况下,SaveChanges()都会保存更改。然而,在幕后,这三个场景是有区别的。在前两个场景中,实体框架核心向数据库发送一个 SQL UPDATE命令,该命令只更新实际的Free Spend列。

exec sp_executesql N'SET NOCOUNT ON;
UPDATE [Flight] SET [FreeSeats] = @p0
WHERE [FlightNo] = @p1;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-8

Output from Listing 17-12

然而,在第三个场景中,开发人员没有给出核心实体框架关于哪些属性实际上发生了变化的信息。实体框架核心不得不将所有属性的值发送回数据库,即使这些值在那里已经是已知的。

UPDATE [Flight] SET [AircraftTypeID] = @p0, [AirlineCode] = @p1, [CopilotId] = @p2, [FlightDate] = @p3, [Departure] = @p4, [Destination] = @p5, [FreeSeats] = @p6, [LastChange] = @p7, [Memo] = @p8, [NonSmokingFlight] = @p9, [PilotId] = @p10, [Price] = @p11, [Seats] = @p12, [Strikebound] = @p13
WHERE [FlightNo] = @p14 AND [Timestamp] = @p15;
SELECT [Timestamp], [Utilization]
FROM [Flight]
WHERE @@ROWCOUNT = 1 AND [FlightNo] = @p14;

Note

除了通过线路发送不必要的数据这一事实之外,更新所有列还会造成潜在的数据更改冲突。如果其他进程已经更改了部分记录,这些更改将无情地覆盖其他进程。因此,您应该始终确保实体框架核心知道已更改的列。如果对象的修改发生在调用者的Attach()方法之前,那么调用者必须提供相应的关于改变的属性的元信息。

可编辑数据网格中的无跟踪模式

当使用方法Attach()时,您可以在无跟踪模式下加载几乎所有的记录。图 17-9 和图 17-10 显示了通常的数据网格场景。用户可以加载(更多)数据并更改任何数据集。然后通过单击保存来保存更改。(见图 17-12 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-10

10,000 records loaded in no-tracking mode in 96 milliseconds

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-9

10,000 records loaded in tracking mode in 174 milliseconds

在这种情况下,完全没有必要浪费加载时跟踪模式的额外时间。使用Attach()来记录用户在实体框架上下文中工作的单个记录就足够了(参见清单 17-13 )。微软为 Windows 演示基金会(WPF)提供的 DataGrid 控件使用了BeginningEdit()事件。在事件处理程序中,Attach()将分离的对象转移到一个附加的对象(见图 17-11 ),从而将该对象注册为实体框架上下文变更跟踪的一部分。

然而,在用AsNoTracking()加载之后,用Attach(). Attach()在一个循环中附加所有对象花费每个对象不到一毫秒的时间并不是一个好主意。当您将单个对象附加到它时,这一点并不明显。但总的来说,这样一个循环比在跟踪模式下直接加载所有对象要慢。所以如果你确定所有的对象都必须被改变,你应该在加载的时候使用跟踪模式。

  /// <summary>
  /// Called when starting to editing a flight in the grid
  /// </summary>
  private void C_flightDataGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
  {
   // Access to the current edited Flight
   var flight = (Flight)e.Row.Item;

   if (flight.FlightNo > 0) // important so that new flights are not added before filling
   {
    // Attach may only be done if the object is not already attached!
    if (!ctx.FlightSet.Local.Any(x => x.FlightNo == flight.FlightNo))
    {
     ctx.FlightSet.Attach(flight);
     SetStatus($"Flight {flight.FlightNo} can now be edited!");
    }
   }
  }

Listing 17-13Attaching an Object to the Context When the User Starts Editing

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-11

The developer logs the object to be changed in the DataGrid to Attach( ) at the Entity Framework context when the change begins Note

只有当对象尚未连接到上下文实例时,才能执行Attach()。否则,将会出现运行时错误。对象是否已经连接到上下文,您不能询问对象本身。然而,DbSet<T>类有一个名为Local的属性,它包含了实体框架核心的本地缓存中的所有对象。要查询这个缓存,请使用ctx.FlightSet.Local.Any (x => x.FlightNo == flight.FlightNo)

Warning

属性Local有一个方法Clear()。正如您所料,这不仅会清空实体框架核心上下文的缓存,还会将其中包含的所有对象置于Deleted状态,这将在下一个SaveChanges()删除它们!要真正只从缓存中删除对象,您必须将对象单独设置为状态Detached,如下所示:

    foreach (var f in ctx.FlightSet.Local.ToList ())
    {
     ctx.Entry (f) .State = EntityState.Detached;
    }

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-12

Saving changes although loaded in no-tracking mode

清单 17-14 和清单 17-15 显示了 XAML 代码和完整的代码隐藏类。

<Window x:Class="GUI.WPF.FlightGridNoTracking"
        xmlns:="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:GUI.WPF"
        xmlns:wpf="clr-namespace:ITVisions.WPF;assembly=ITV_DemoUtil"
        mc:Ignorable="d"
        Title="World Wide Wings - FlightGridNoTracking" Height="455.233" Width="634.884">

 <Window.Resources>
  <wpf:InverseBooleanConverter x:Key="InverseBooleanConverter"></wpf:InverseBooleanConverter>
 </Window.Resources>

 <Grid x:Name="LayoutRoot" Background="White">
  <DockPanel>

   <!--===================== Command Bar->
   <StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
    <ComboBox Width="100" x:Name="C_City" ItemsSource="{Binding}">
     <ComboBoxItem  Content="All" IsSelected="True" />
     <ComboBoxItem  Content="Rome" />
     <ComboBoxItem Content="Paris" />
     <ComboBoxItem Content="New York/JFC" />
     <ComboBoxItem Content="Berlin" />
    </ComboBox>
    <ComboBox Width="100" x:Name="C_Count" >
     <ComboBoxItem Content="10" IsSelected="True" />
     <ComboBoxItem Content="100" IsSelected="True" />
     <ComboBoxItem Content="1000" IsSelected="True" />
     <ComboBoxItem Content="All" IsSelected="True" />
    </ComboBox>
    <ComboBox Width="100" x:Name="C_Mode" >
     <ComboBoxItem Content="Tracking" IsSelected="True" />
     <ComboBoxItem Content="NoTracking" IsSelected="False" />
    </ComboBox>
    <Button Width="100" x:Name="C_Test" Content="Test Connection" Click="C_Test_Click" ></Button>
    <Button Width="100" x:Name="C_Load" Content="Load" Click="C_Load_Click"></Button>
    <Button Width="100" x:Name="C_Save" Content="Save" Click="C_Save_Click"></Button>
   </StackPanel>
   <!-===================== Status Bar->
   <StatusBar DockPanel.Dock="Bottom">
    <Label x:Name="C_Status"></Label>
   </StatusBar>
   <!-===================== Datagrid->
   <DataGrid Name="C_flightDataGrid" AutoGenerateColumns="False" EnableRowVirtualization="True"  IsSynchronizedWithCurrentItem="True"  SelectedIndex="0" Height="Auto" BeginningEdit="C_flightDataGrid_BeginningEdit"  PreviewKeyDown="C_flightDataGrid_PreviewKeyDown" RowEditEnding="C_flightDataGrid_RowEditEnding">
    <DataGrid.Columns>
     <DataGridTextColumn Binding="{Binding Path=FlightNo}" Header="Flight No" Width="SizeToHeader" />
     <DataGridTextColumn Binding="{Binding Path=Departure}" Header="Departure" Width="SizeToHeader" />
     <DataGridTextColumn Binding="{Binding Path=Destination}" Header="Destination" Width="SizeToHeader" />
     <DataGridTextColumn Binding="{Binding Path=Seats}" Header="Seats" Width="SizeToHeader" />
     <DataGridTextColumn Binding="{Binding Path=FreeSeats}" Header="Free Seats" Width="SizeToHeader" />
     <DataGridCheckBoxColumn Binding="{Binding Path=NonSmokingFlight, Converter={StaticResource InverseBooleanConverter}}" Header="Non Smoking Flight" Width="SizeToHeader" />
     <DataGridTemplateColumn Header="Date" Width="100">
      <DataGridTemplateColumn.CellTemplate>
       <DataTemplate>
        <DatePicker SelectedDate="{Binding Path=Date}" />
       </DataTemplate>
      </DataGridTemplateColumn.CellTemplate>
     </DataGridTemplateColumn>
     <DataGridTextColumn Binding="{Binding Path=Memo}" Width="200" Header="Memo"  />
    </DataGrid.Columns>
   </DataGrid>

  </DockPanel>
 </Grid>
</Window>

Listing 17-14XAML Code FlightGridNoTracking.xaml (Project EFC_GUI)

using BO;
using DA;
using Microsoft.EntityFrameworkCore;
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace GUI.WPF
{

 public partial class FlightGridNoTracking : Window
 {

  public FlightGridNoTracking()
  {
   InitializeComponent();
   this.Title = this.Title + "- Version: " + Assembly.GetExecutingAssembly().GetName().Version.ToString();
  }

  private void SetStatus(string s)
  {
   this.C_Status.Content = s;
  }

  WWWingsContext ctx;

  /// <summary>
  /// Load flights
  /// </summary>
  private void C_Load_Click(object sender, RoutedEventArgs e)
  {
   ctx = new WWWingsContext();
   // Clear grid
   this.C_flightDataGrid.ItemsSource = null;
   // Get departure
   string Ort = this.C_City.Text.ToString();
   // Show status
   SetStatus("Loading with " + this.C_Mode.Text + "...");

   // Prepare query
   var q = ctx.FlightSet.AsQueryable();
   if (this.C_Mode.Text == "NoTracking") q = q.AsNoTracking();
   if (Ort != "All") q = (from f in q where f.Departure == Ort select f);

   if (Int32.TryParse(this.C_Count.Text, out int count))
   {
    if (count>0) q = q.Take(count);
   }

   var sw = new Stopwatch();
   sw.Start();
   // Execute query
   var fluege = q.ToList();
   sw.Stop();

   // Databinding to grid
   this.C_flightDataGrid.ItemsSource = fluege; // Local is empty at NoTracking;

   // set state
   SetStatus(fluege.Count() + " loaded records using " + this.C_Mode.Text + ": " + sw.ElapsedMilliseconds + "ms!");
  }

  /// <summary>
  /// Save the changed flights
  /// </summary>
  private void C_Save_Click(object sender, RoutedEventArgs e)
  {
   // Get changes and ask
   var added = from x in ctx.ChangeTracker.Entries() where x.State == EntityState.Added select x;
   var del = from x in ctx.ChangeTracker.Entries() where x.State == EntityState.Deleted select x;
   var mod = from x in ctx.ChangeTracker.Entries() where x.State == EntityState.Modified select x;

   if (MessageBox.Show("Do you want to save the following changes?\n" + String.Format("Client: Changed: {0} New: {1} Deleted: {2}", mod.Count(), added.Count(), del.Count()), "Confirmation", MessageBoxButton.YesNo) == MessageBoxResult.No) return;

   string Ergebnis = "";

   // Save
   Ergebnis = ctx.SaveChanges().ToString();

   // Show status
   SetStatus("Number of saved changes: " + Ergebnis);
  }

  /// <summary>
  /// Called when starting to editing a flight in the grid
  /// </summary>
  private void C_flightDataGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
  {
   // Access to the current edited Flight
   var flight = (Flight)e.Row.Item;

   if (flight.FlightNo > 0) // important so that new flights are not added before filling
   {
    // Attach may only be done if the object is not already attached!
    if (!ctx.FlightSet.Local.Any(x => x.FlightNo == flight.FlightNo))
    {
     ctx.FlightSet.Attach(flight);
     SetStatus($"Flight {flight.FlightNo} can now be edited!");
    }
   }
  }

  /// <summary>
  /// Called when deleting a flight in the grid
  /// </summary>
  private void C_flightDataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
  {
   var flight = (Flight)((DataGrid)sender).CurrentItem;

   if (e.Key == Key.Delete)
   {
    // Attach may only be done if the object is not already attached!
    if (!ctx.FlightSet.Local.Any(x => x.FlightNo == flight.FlightNo))
    {
     ctx.FlightSet.Attach(flight);
    }

    ctx.FlightSet.Remove(flight);
    SetStatus($"Flight {flight.FlightNo} can be deleted!");
   }
  }

  /// <summary>
  /// Called when adding a flight in the grid
  /// </summary>
  private void C_flightDataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
  {
   var flight = (Flight)e.Row.Item;
   if (!ctx.FlightSet.Local.Any(x => x.FlightNo == flight.FlightNo))
   {
    ctx.FlightSet.Add(flight);
    SetStatus($"Flight {flight.FlightNo} has bee added!");
   }
  }

  private void C_Test_Click(object sender, RoutedEventArgs e)
  {
   try
   {
    ctx = new WWWingsContext();
    var flight = ctx.FlightSet.FirstOrDefault();
    if (flight == null) MessageBox.Show("No flights :-(", "Test Connection", MessageBoxButton.OK, MessageBoxImage.Warning);
    else MessageBox.Show("OK!", "Test Connection", MessageBoxButton.OK, MessageBoxImage.Information);
   }
   catch (Exception ex)
   {
    MessageBox.Show("Error: " + ex.ToString(), "Test Connection", MessageBoxButton.OK, MessageBoxImage.Error);
   }

  }
 }
}

Listing 17-15Code-Behind Class FlightGridNoTracking.cs (Project EFC_GUI)

QueryTrackingBehavior 和 AsTracking()

当使用实体框架和实体框架核心数据集读取数据时,无跟踪模式显著提高了性能。您在前面的章节中已经看到,几乎应该总是使用无跟踪模式。不幸的是,在经典的实体框架中,您必须记住在每个查询中使用AsNoTracking()。这不仅令人讨厌,而且很容易忘记。在传统的实体框架中,您需要额外的解决方案,例如访问DbSet<T>的抽象,它每次都会自动启用无跟踪模式。

在实体框架核心中,微软引入了一个更优雅的解决方案:你可以将整个实体框架核心上下文置于无跟踪模式。子对象ChangeTracker的类Microsoft.EntityFrameworkCore.DbContext中有枚举属性QueryTrackingBehavior。默认设置为QueryTrackingBehavior.TrackAll;换句话说,跟踪被激活。但是,如果您将其更改为QueryTrackingBehavior.NoTracking,所有查询都将以无跟踪模式执行,即使没有AsNoTracking()扩展方法。为了在跟踪模式下执行单个查询,对于非跟踪基本模式有一个新的扩展方法AsTracking()(参见清单 17-16 )。图 17-13 显示了输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-13

Output of Listing 17-16

  public static void TrackingMode_QueryTrackingBehavior()
  {

   CUI.MainHeadline("Default setting: TrackAll. Use AsNoTracking()");
   using (WWWingsContext ctx = new WWWingsContext())
   {
    ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll; // Standard
    var flightSet = ctx.FlightSet.AsNoTracking().ToList();
    var flight = flightSet[0];
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Detached
    flight.FreeSeats-;
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
    int count = ctx.SaveChanges();
    Console.WriteLine($"Saved changes: {count}"); // 0
   }

   CUI.MainHeadline("Default setting: NoTracking.");
   using (WWWingsContext ctx = new WWWingsContext())
   {
    ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; // NoTracking
    var flightSet = ctx.FlightSet.ToList();
    var flight = flightSet[0];
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Unchanged
    flight.FreeSeats-;
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
    int count = ctx.SaveChanges();
    Console.WriteLine($"Saved changes: {count}"); // 0
   }

   CUI.MainHeadline("Default setting: NoTracking. Use AsTracking()");
   using (WWWingsContext ctx = new WWWingsContext())
   {
    ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; // NoTracking
    var flightSet = ctx.FlightSet.AsTracking().ToList();
    var flight = flightSet[0];
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Unchanged
    flight.FreeSeats-;
    Console.WriteLine(flight + " object state: " + ctx.Entry(flight).State); // Modified
    int count = ctx.SaveChanges();
    Console.WriteLine($"Saved changes: {count}"); // 1
   }

  }

Listing 17-16Setting QueryTrackingBehavior and Using AsTracking( )

无跟踪模式的后果

除了缺少更改跟踪之外,无跟踪模式还有其他后果,如下所示:

  • 对象不会加载到实体框架核心的一级缓存中。当再次访问对象时(例如用DbSet<T>.Find()),它总是被数据库管理系统加载。
  • 没有关系修复。关系修复是实体框架核心的一个特性,如果根据数据库,两个独立加载的对象属于同一个对象,那么它会将这两个对象连接起来。例如,假设Pilot 123 已经被加载。Flight 101 现在被加载,并且在其外键关系中Pilot的值为 123。实体框架核心将连接 RAM 中的Flight 101 和Pilot 123,以便您可以从Flight导航到Pilot,并且对于双向导航,从Pilot导航到Flight
  • 延迟加载不支持无跟踪,但是无论如何,延迟加载目前在实体框架核心中是不可用的。

最佳实践

新的默认QueryTrackingBehavior.NoTracking和新的扩展方法AsTracking()是实体框架核心中有意义的添加。但是在实践中已经看到了许多表现不佳的实体框架/实体框架核心应用,对我来说这还不够。QueryTrackingBehavior.NoTracking应该是标准的,以便所有开发人员得到高性能的查询执行。目前,在以QueryTrackingBehavior.TrackAll为标准设置的实体框架核心中,每个开发人员仍然需要记住设置QueryTrackingBehavior.NoTracking。最好在上下文类本身的构造函数中这样做,结果是,您将不再有跟踪查询的开销!

选择最佳装载策略

第九章讨论了实体框架核心可用于相关主数据或详细数据的加载策略(显式重新加载、快速加载和预加载)。不幸的是,我不能笼统地说什么是最好的装载策略。它总是取决于具体情况,而针对您的具体情况的最佳加载策略只能通过性能测试根据具体情况来确定。然而,一些总括声明仍然是可能的。

基本上,建议不要将连接的数据记录作为一个整体加载,如果它们不是绝对必要的,而是仅在实际需要时加载连接的数据集。它取决于潜在连接和已连接数据集的数量,以确定急切加载是否值得。

如果您知道需要连接的数据(例如,在数据导出的上下文中),您应该选择立即加载或预加载。与使用Include()的急切加载相比,所示的预加载技巧在许多情况下可以显著提高性能。

如果您不确切知道是否需要链接的数据,那么在延迟加载和急切加载之间的选择通常是在瘟疫和霍乱之间的选择。通过额外的服务器往返,重新加载减慢了所有的速度,但是急切的加载减慢了更大的结果集的速度。然而,在大多数情况下,往返次数的增加比更大的结果集更不利于性能。

如果您不确定,不要将延迟加载或急切加载绑定到代码中,而是允许在运行时通过配置来控制它。因此,应用的操作者可以随着数据量的增加并根据应用的典型用户行为来调整应用。

贮藏

web 和桌面应用都有经常使用但很少在数据存储中更新的数据。在这些情况下,在 RAM 中基于时间的数据缓存是有用的。经典。NET 从 4.0 版本开始就有了组件System.Runtime.Caching.dll。一个System.Runtime.Caching的前兆已经出现了。NET 1.0 在 ASP.NET 的名称空间System.WebCachingSystem.Web.dll。组件System.Runtime.Caching.dll引进于。另一方面,NET 4.0 可以在所有类型的应用中使用。System.Runtime.Caching本质上只提供一种缓存:MemoryCache用于 RAM 缓存。通过从基类ObjectCache派生,您可以开发其他缓存方法(例如,在专用缓存服务器上或在文件系统中)。Windows Server 的AppFabric的缓存特性是另一个缓存选项,但它不是基于System.Runtime.Caching的。

Note

。NET Core 用 NuGet 包Microsoft.Extensions.Caching.Memory代替System.Runtime.Caching。但是,System.Runtime.Caching现在是的 Windows 兼容包的一部分。净芯( https://blogs.msdn.microsoft.com/dotnet/2017/11/16/announcing-the-windows-compatibility-pack -for-net-core),也可用于。NET 核心。

超高速缓冲存储系统

清单 17-17 展示了一个结合实体框架核心使用MemoryCache的例子。首先,GetFlights1()检查出发航班的列表是否已经在缓存中。如果列表不存在,所有相关的航班都将加载一个新的实体框架上下文实例。对于这个数据集,GetFlights1()创建了一个名为FlightSet的缓存条目。带有policy.AbsoluteExpiration = DateTime.Now.AddSeconds (5)的程序代码确定缓存条目应该在五秒钟后过期。然后它会自动从 RAM 中消失。

当然,也可以为每个出发地点创建一个单独的缓存条目,在一个条目中缓存所有航班,然后从 RAM 中过滤它们。那么数据库访问的次数将会更少,但是 RAM 中也会有您可能不需要的数据。只有当数据量不太大时,才能考虑这一点。可以在应用配置文件(app.config / web.config)中设置多少 RAM 用于缓存,可以是绝对兆字节(cacheMemoryLimitMegabytes)或物理内存的百分比(physicalMemoryLimitPercentage)。也可以定义这些限值(pollingInterval)的检查间隔。作为通过应用配置文件定义这些参数的替代方法,可以将它们作为NameValueCollection传递给MemoryCache的构造函数。

清单 17-17 中的Demo_MemoryCache()方法通过在 15 秒内每秒调用两次来测试GetFlights1()的操作。清单 17-17 显示缓存解决方案按预期工作,每五秒钟重新加载一次航班。图 17-14 显示了输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-14

Output to Listing 17-17

  public static void Demo_MemoryCache()
  {
   CUI.MainHeadline(nameof(Demo_MemoryCache));
   DateTime Start = DateTime.Now;
   do
   {
    var flightSet = GetFlight1("Rome");
    // you can process the flights here...
    Console.WriteLine("Processing " + flightSet.Count + " flights...");
    System.Threading.Thread.Sleep(500);
   } while ((DateTime.Now - Start).TotalSeconds < 60);

   CUI.Print("done!");
  }

  /// <summary>
  /// GetFlight with MemoryCache (5 sek)
  /// </summary>
  private static List<Flight> GetFlight1(string departure)
  {
   string cacheItemName = "FlightSet_" + departure;

   // Access to the cache entry
   System.Runtime.Caching.MemoryCache cache = System.Runtime.Caching.MemoryCache.Default;
   List<Flight> flightSet = cache[cacheItemName] as List<Flight>;
   if (flightSet == null) // Element is NOT in the cache
   {
    CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache missed", ConsoleColor.Red);
    using (var ctx = new WWWingsContext())
    {
     ctx.Log();
     // Load flights
     flightSet = ctx.FlightSet.Where(x => x.Departure == departure).ToList();
    }
    // Store flights in cache
    CacheItemPolicy policy = new CacheItemPolicy();
    policy.AbsoluteExpiration = DateTime.Now.AddSeconds(5);
    //or: policy.SlidingExpiration = new TimeSpan(0,0,0,5);
    cache.Set(cacheItemName, flightSet, policy);
   }
   else // Data is already in cache
   {
    CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache hit", ConsoleColor.Green);
   }
   return flightSet;
  }

Listing 17-17Timed Caching of Data Loaded with Entity Framework Using MemoryCache

Note

顺便说一下,System.Runtime. Caching可以做得更多,尤其是所谓的基于资源变化的缓存失效。例如,如果文件发生变化(HostFileChangeMonitor)或数据库表的内容发生变化(SqlChangeMonitor),可以立即删除缓存条目(甚至在设置的缓存期到期之前)。

缓存管理器

GetFlights1()这样的数据访问方法在每个应用中出现上百次甚至上千次。重复出现相同的程序代码来检查缓存条目的存在,并可能创建新的条目,这当然不是一个好的解决方案。

在清单 17-18 中,任务更加简洁和整洁。GetFlights2()只包括调用CacheManager类实例的通用Get()方法。CacheManager在实例化期间接收以秒为单位的缓存持续时间。除了描述返回类型的类型参数之外,Get()方法还需要缓存条目名和对数据加载方法的引用。第三个和任何后续参数通过Get()传递给 load 方法。load 方法GetFlights2Internal()完全脱离了缓存方面,只负责用实体框架加载数据。也可以直接调用它,但这通常是不可取的。所以,这里也是“私”的。

public static void Demo_CacheManager()
  {
   CUI.MainHeadline(nameof(Demo_CacheManager));
   DateTime Start = DateTime.Now;
   do
   {
    var flightSet = GetFlight2("Rome");
    // you can process the flights here...
    Console.WriteLine("Processing " + flightSet.Count + " flights...");
    System.Threading.Thread.Sleep(500);
   } while ((DateTime.Now - Start).TotalSeconds < 60);
  }

  /// <summary>
  /// GetFlight with CacheManager (5 sek)
  /// </summary>
  private static List<Flight> GetFlight2(string departure)
  {
   string cacheItemName = "FlightSet_" + departure;
   var cm = new CacheManager<List<Flight>>(5);
   cm.CacheHitEvent += (text) => { CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache hit: " + text, ConsoleColor.Green); };
   cm.CacheMissEvent += (text) => { CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache missed: " + text, ConsoleColor.Red); };
   return cm.Get(cacheItemName, GetFlight2Internal, departure);
  }

  private static List<Flight> GetFlight2Internal(object[] param)
  {
   using (var ctx = new WWWingsContext())
   {
    ctx.Log();
    string departure = param[0] as string;
    // Load flights
    return ctx.FlightSet.Where(x => x.Departure == departure).ToList();
   }
  }

Listing 17-18Simplified Implementation of the Task Now with the CacheManager

然而,这个优雅的CacheManager类不是. NET Framework 类,而是一个自实现。这个类的完整源代码如清单 17-20 所示。除了清单 17-18 中使用的通用Get()方法,该方法需要一个类型参数和一个加载方法,您还可以使用另一个Get()重载直接从缓存中检索数据。如果数据不存在,你在这里得到零。用Save()也可以直接保存。CacheManager类的用户看不到底层库System.Runtime.Caching的任何东西。

就其本质而言,对于给定的任务,使用属性SlidingExpiration而不是AbsoluteExpiration听起来很诱人。然而,策略leads.SlidingExpiration = new TimeSpan (0,0,0,5)说数据在第一次加载后将永远不会被重新加载,因为TimeSpan (0,0,0,5)设置的 5 秒时间跨度指的是SlidingExpiration。最后一次访问,即在最后一次读取访问后仅五秒钟,高速缓存条目被移除。要强制重新加载,您必须在方法Demo_CacheManager()中将Sleep()的持续时间设置为 5000 或更高。

如果你想更简洁一点,你应该看看清单 17-20 ,它显示了一个带有匿名函数的变体。不再需要编写单独的加载方法;必要的代码完全嵌入在GetFlights4()中。得益于Closure技术,Get()不再需要获取Departure作为参数,因为嵌入在GetFlights3()中的匿名方法可以直接访问GetFlights3()方法的所有变量。

  public static void Demo_CacheManagerLambda()
  {
  CUI.MainHeadline(nameof(Demo_CacheManagerLambda));
   DateTime Start = DateTime.Now;
   do
   {
    var flightSet = GetFlight3("Rome");
    // you can process the flights here...
    Console.WriteLine("Processing " + flightSet.Count + " flights...");
    System.Threading.Thread.Sleep(500);

   } while ((DateTime.Now - Start).TotalSeconds < 60);
  }

  public static List<Flight> GetFlight3(string departure)
  {
   string cacheItemName = "FlightSet_" + departure;
   Func<string[], List<Flight>> getData = (a) =>
   {
    using (var ctx = new WWWingsContext())
    {
     // Load flights
     return ctx.FlightSet.Where(x => x.Departure == departure).ToList();
    }
   };

   var cm = new CacheManager<List<Flight>>(5);
   cm.CacheHitEvent += (text) => { CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache Hit: " + text, ConsoleColor.Green); };
   cm.CacheMissEvent += (text) => { CUI.Print($"{DateTime.Now.ToLongTimeString()}: Cache Miss: " + text, ConsoleColor.Red); };

   return cm.Get(cacheItemName, getData);
  }

Listing 17-19Variant for Using CacheManager with an Anonymous Function

using System;
using System.Collections.Generic;
using System.Runtime.Caching;

namespace ITVisions.Caching
{

 /// <summary>
 /// CacheManager for simplified caching with System.Runtime.Caching
 /// (C) Dr. Holger Schwichtenberg 2013-2017
 /// </summary>
 public class CacheManager
 {
  public static List<MemoryCache> AllCaches = new List<MemoryCache>();

  public static bool IsDebug = false;
  /// <summary>
  /// Default cache duration
  /// </summary>
  public static int DefaultCacheSeconds = 60 * 60; // 60 minutes

  /// <summary>
  /// Reduced cache duration in debug mode
  /// </summary>
  public static int DefaultCacheSeconds_DEBUG = 10; // 10 seconds
  /// <summary>
  /// Removes all entries from all caches
  /// </summary>
  public static void Clear()
  {
   MemoryCache.Default.Dispose();
   foreach (var c in AllCaches)
   {
    c.Dispose();
   }
  }

  /// <summary>
  /// Removes all entries with name part from all caches
  /// </summary>
  /// <param name="name"></param>
  public static void RemoveLike(string namepart)
  {
   foreach (var x in MemoryCache.Default)
   {
    if (x.Key.Contains(namepart)) MemoryCache.Default.Remove(x.Key);
   }
   foreach (var c in AllCaches)
   {
    foreach (var x in MemoryCache.Default)
    {
     if (x.Key.Contains(namepart)) MemoryCache.Default.Remove(x.Key);
    }
   }
  }
 }

 /// <summary>
 /// CacheManager for simplified caching with System.Runtime.Caching
 /// (C) Dr. Holger Schwichtenberg 2013-2017
 /// </summary>
 /// <typeparam name="T">type of cached data</typeparam>
 /// <example>
 /// public List<Datentyp> GetAll()
 /// {
 /// var cm = new CacheManager<List<Datentyp>>();
 /// return cm.Get("Name", GetAllInternal, "parameter");
 /// }
 /// public List<Datentyp> GetAllInternal(string[] value)
 /// {
 /// var q = (from x in Context.MyDbSet where x.Name == value select x);
 /// return q.ToList();
 /// }
 /// </example>
 public class CacheManager<T> where T : class
 {

  /// <summary>
  /// CacheHit or CassMiss
  /// </summary>
  public event Action<string> CacheEvent;
  /// <summary>
  /// triggered when requested data is in the cache
  /// </summary>
  public event Action<string> CacheHitEvent;
  /// <summary>
  /// triggered when requested data is not in the cache
  /// </summary>
  public event Action<string> CacheMissEvent;

  private readonly int _seconds = CacheManager.DefaultCacheSeconds;

  public MemoryCache Cache { get; set; } = MemoryCache.Default;

  /// <summary>
  /// Created CacheManager with MemoryCache.Default
  /// </summary>
  public CacheManager()
  {
   if (CacheManager.IsDebug || System.Diagnostics.Debugger.IsAttached)
   {
    this._seconds = CacheManager.DefaultCacheSeconds_DEBUG;
   }
   else
   {
    this._seconds = CacheManager.DefaultCacheSeconds;
   }
  }

  public CacheManager(int seconds) : this()
  {
   this._seconds = seconds;
  }

  /// <summary>
  /// Generated CacheManager with its own MemoryCache instance
  /// </summary>
  /// <param name="seconds">Gets or sets the maximum memory size, in megabytes, that an instance of a MemoryCache object can grow to.</param>
  /// <param name="cacheMemoryLimitMegabytes"></param>
  /// <param name="physicalMemoryLimitPercentage">Gets or sets the percentage of memory that can be used by the cache.</param>
  /// <param name="pollingInterval">Gets or sets a value that indicates the time interval after which the cache implementation compares the current memory load against the absolute and percentage-based memory limits that are set for the cache instance.</param>
  public CacheManager(int seconds, int cacheMemoryLimitMegabytes, int physicalMemoryLimitPercentage, TimeSpan pollingInterval)
  {
   var config = new System.Collections.Specialized.NameValueCollection();
   config.Add("CacheMemoryLimitMegabytes", cacheMemoryLimitMegabytes.ToString());
   config.Add("PhysicalMemoryLimitPercentage", physicalMemoryLimitPercentage.ToString());
   config.Add("PollingInterval", pollingInterval.ToString());
   Cache = new MemoryCache("CustomMemoryCache_" + Guid.NewGuid().ToString(), config);
   Console.WriteLine(Cache.PhysicalMemoryLimit);
   Console.WriteLine(Cache.DefaultCacheCapabilities);
   this._seconds = seconds;
  }

  /// <summary>
  /// Get element from cache. It will not load if it is not there!
  /// </summary>
  public T Get(string name)
  {
   object objAlt = Cache[name];
   return objAlt as T;
  }

  /// <summary>
  /// Get element from cache or data source. Name becomes the name of the generic type
  /// </summary>
  public T Get(Func<string[], T> loadDataCallback, params string[] args)
  {
   return Get(typeof(T).FullName, loadDataCallback, args);
  }

  /// <summary>
  /// Retrieves item from cache or data source using the load method.
  /// </summary>
  public T Get(string name, Func<string[], T> loadDataCallback, params string[] args)
  {
   string cacheInfo = name + " (" + Cache.GetCount() + " elements in cache. Duration: " + _seconds + "sec)";
   string action = "";
   object obj = Cache.Get(name);
   if (obj == null) // not in cache
   {
    action = "Cache miss";
    CacheMissEvent?.Invoke(cacheInfo);
    CUI.PrintVerboseWarning(action + ": " + cacheInfo);

    #region DiagnoseTemp
    string s = DateTime.Now + "################ CACHE MISS for: " + cacheInfo + ": " + loadDataCallback.ToString() + System.Environment.NewLine;
    int a = 0;
    var x = Cache.DefaultCacheCapabilities;
    foreach (var c in Cache)
    {
     a++;
     s += $"{a:00}: LIMIT: {Cache.PhysicalMemoryLimit}:" + c.Key + ": " + c.Value.ToString().Truncate(100) + System.Environment.NewLine;

    }

    Console.WriteLine(s);
    #endregion

    // load data now
    obj = loadDataCallback(args);
    // and store it in cache
    Save(name, obj as T);

   }
   else // found in cache
   {
    action = "Cache hit";
    CUI.PrintVerboseSuccess(action + ": " + cacheInfo);
    CacheHitEvent?.Invoke(cacheInfo);
   }

   // return data
   CacheEvent?.Invoke(action + " for " + cacheInfo);
   return obj as T;
  }

  /// <summary>
  /// Saves an object in the cache
  /// </summary>
  public void Save(string name, T obj)
  {
   if (obj == null) return;
   object objAlt = Cache[name];
   if (objAlt == null)
   {
    CacheItemPolicy policy = new CacheItemPolicy();
    policy.AbsoluteExpiration = DateTime.Now.AddSeconds(_seconds);
    policy.RemovedCallback = new CacheEntryRemovedCallback(this.RemovedCallback);
    Cache.Set(name, obj, policy);
   }
  }

  public void RemovedCallback(CacheEntryRemovedArguments arguments)
  {

  }

  /// <summary>
  /// Removes an entry with specific names from this cache
  /// </summary>
  /// <param name="name"></param>
  public void Remove(string name)
  {
   if (Cache.Contains(name)) Cache.Remove(name);
  }

  /// <summary>
  /// Removes all entries with specific name part from this cache
  /// </summary>
  public void RemoveLike(string namepart)
  {
   foreach (var x in Cache)
   {
    if (x.Key.Contains(namepart)) Cache.Remove(x.Key);
   }
  }
 }
}

Listing 17-20The Auxiliary Class CacheManager Simplifies the Use of System.Runtime.Caching

使用 EFPlus 的二级缓存

清单 17-18 中显示的CacheManager是一个通用的解决方案,它不仅允许你缓存实体框架对象,还允许你缓存任何形式的数据。实体框架核心上的缓存甚至可以更优雅!对于实体框架核心,在附加库实体框架 Plus (EFPlus)和EFSecondLevelCache.Core中有一个特殊的缓存解决方案(参见第二十章)。

这些组件基于System.MemoryCache.Runtime.Caching实现了一个上下文无关的查询结果缓存。这种缓存称为二级缓存。这些额外的组件可以操作查询,以便实体框架核心将其结果具体化为对象,并将其不仅存储在上下文实例的一级缓存中,还存储在流程级别的二级缓存中。然后,另一个上下文实例可以在这个二级缓存中查找相同的查询,并传递存储在那里的对象,而不是来自数据库管理系统的新查询(参见图 17-15 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-15

How a second-level cache works Note

本节讨论EFPlus. EFSecondLevelCache.Core中的二级缓存在配置上更复杂,但也更灵活,因为除了主内存缓存(MemoryCache),Redis 也可以作为缓存。

设置二级缓存

对于 EFPlus,不需要在上下文类中设置二级缓存。

使用二级缓存

清单 17-21 展示了二级缓存在 EFPlus 中的应用。在GetFlights4()中,在 LINQ 查询中使用了FromCache()方法,以 NuGet 包Microsoft.Extensions.Caching.Abstraction中类型为MemoryCacheEntryOptions的对象的形式指定缓存持续时间(这里:五秒)。

或者,您可以集中设置缓存持续时间,然后省略FromCache()参数。

var options = new MemoryCacheEntryOptions() { AbsoluteExpiration = DateTime.Now.AddSeconds(5) };
QueryCacheManager.DefaultMemoryCacheEntryOptions = options;

注意GetFlights4()每次被调用时都会创建一个新的上下文实例,但是缓存仍然有效,如图 17-16 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17-16

Output of Listing 17-21

不幸的是,与直接使用MemoryCache对象的解决方案不同,您无法获得对象是否来自缓存或何时查询数据库的信息,因为不幸的是,在这种情况下,EFPlus 的缓存管理器不会触发任何结果。因此,您可以通过实体框架日志记录(ctx.Log())从数据库访问中获得缓存行为;参见第十二章)或通过外部探查器(例如,实体框架探查器或 SQL Server 探查器)。

  public static void Demo_SecondLevelCache()
  {
   CUI.MainHeadline(nameof(Demo_SecondLevelCache));
   DateTime Start = DateTime.Now;
   do
   {
    var flightSet = GetFlight4("Rome");
    // you can process the flights here...
    Console.WriteLine("Processing " + flightSet.Count + " flights...");
    System.Threading.Thread.Sleep(500);
   } while ((DateTime.Now - Start).TotalSeconds < 30);

   GetFlight4("Rome");
   GetFlight4("Rome");
   GetFlight4("Rome");
   GetFlight4("Paris");
   GetFlight4("Mailand");
   GetFlight4("Mailand");
   GetFlight4("Rome");
   GetFlight4("Paris");
  }

  /// <summary>
  /// Caching with EFPlus FromCache() / 5 seconds
  /// </summary>
  /// <param name="departure"></param>
  /// <returns></returns>
  public static List<Flight> GetFlight4(string departure)
  {
   using (var ctx = new WWWingsContext())
   {
    ctx.Log();

    var options = new MemoryCacheEntryOptions() { AbsoluteExpiration = DateTime.Now.AddSeconds(5) };
    // optional: QueryCacheManager.DefaultMemoryCacheEntryOptions = options;

    Console.WriteLine("Load flights from " + departure + "...");

    var flightSet = ctx.FlightSet.Where(x => x.Departure == departure).FromCache(options).ToList();
    Console.WriteLine(flightSet.Count + " Flights im RAM!");
    return flightSet;
   }
  }

Listing 17-21Second-Level Caching with EFPlus

十八、带有实体框架核心的软件架构

实体框架核心无疑属于数据访问层。但是使用实体框架核心时,层模型整体看起来是什么样的呢?在这一章中,我简要地讨论了几种可供选择的架构。

整体模型

实体框架核心可以在一个单一的软件模型中使用。换句话说,实体框架核心上下文的实例化和命令(LINQ、存储过程、SQL)的执行都在表示层(见图 18-1 )。然而,这仅在非常小的应用中有意义(参见附录 A 中的 app MiracleList Light)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18-1

Entity Framework Core in the monolithic software architecture model

作为数据访问层的实体框架核心

图 18-2 左侧显示了多层应用的一般结构,右侧显示了一个简单的多层软件架构模型,使用实体框架核心进行数据访问。这种实用的软件架构模型不需要专用的数据访问层。相反,实体框架上下文是完整的数据层。覆盖层是业务逻辑层,它通过语言集成查询(LINQ)命令和存储过程的调用(包括所需的直接 SQL 命令)来控制数据访问。根据业务逻辑层中的语句,实体框架上下文填充实体类。实体类通过所有层向下传递到表示层。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18-2

The pragmatic Entity Framework Core–based software architecture model

一些软件架构师批评这种简化的模型,因为业务逻辑层被数据访问命令污染了。业务逻辑层实际上不应该包含任何数据库访问命令。如果你真的把 LINQ 等同于 SQL,你就能看到这一点。但是你也可以把 LINQ 理解为 SQL 的真正抽象。毕竟,LINQ 命令只是一系列与数据库无关的方法调用;C# 和 Visual Basic 中类似 SQL 的语法对于软件开发人员来说只是一种语法糖。C# 或 Visual Basic 编译器立即使 LINQ 命令再次成为方法调用字符串。你也可以用这个方法调用字符串本身,也就是用collection.Where(x => x.CatID > 4).OrderBy(x => x.Name)代替x in collection where x.CatID > 4 orderby x.Name。但是方法调用正是业务逻辑和数据访问控制通常相互通信的形式;也就是说,除了业务逻辑和数据层之间的常见做法之外,业务逻辑层中 LINQ 的必要使用没有任何作用。LINQ 只是比数据层的大多数 API 更通用。

实际上,业务逻辑层的一些污染是业务逻辑层中实体框架上下文实例的使用。这意味着业务逻辑层必须有一个对实体框架核心组件的引用。对象关系映射器的后续替换意味着业务逻辑层的变化。但是这种模式的明显优势是简单。您不必编写自己的数据库访问层,这样可以节省时间和金钱。

纯商业逻辑

然而,一些软件架构师会拒绝前面的实用模型,因为它太简单了,而是依赖于第二个模型(见图 18-3 )。这样,您就创建了自己的数据访问控制层。在这个数据访问控制层中,所有的 LINQ 调用和存储过程包装器方法都被再次打包在自己编写的方法中。这些方法然后调用业务逻辑层。在该模型中,只有数据访问控制层需要对实体框架核心组件的引用;因此,业务逻辑保持“纯净”

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18-3

The textbook Entity Framework Core–based software architecture model without distribution

第二个软件架构模型对应于“纯”原则,但是在实践中,它也需要更多的实现工作。特别是在严格意义上几乎没有业务逻辑的“数据之上的表单”应用的情况下,开发人员必须实现许多“烦人的”包装例程。对于 LINQ,数据库访问层的GetCustomers()包含 LINQ 命令,业务逻辑中的GetCustomers()转发到数据库访问层的GetCustomers()。使用存储过程时,两层都只传递。

业务类和视图模型类

这里要讨论的第三个软件架构模型(见图 18-4 )甚至更进一步,进行了一个抽象步骤,并且还禁止将实体类传递给所有层。相反,会发生实体类到其他类的映射。这些其他类通常被称为业务(对象)类,有时也被称为数据传输对象(dto ),与数据密集型实体类相反。在模型 3b 中(图的右侧),这些业务对象类再次被映射到作为模型-视图-视图模型(MVVM)模式的一部分为视图专门格式化的类。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18-4

Business objects and ViewModel classes

如果创建的实体类与实体框架核心(类似于经典 ADO.NET 实体框架第一版中的EntityObject基类)有关系,那么基于业务类的软件架构模型将是强制性的。但在实体框架核心中却不是这样。使用基于业务类的模型的一个很好的理由是,如果实体类的设计符合表示层的需求,例如,因为它是一个“历史发展”的数据库模式。

然而,这种基于业务类的模型意味着相当大的实现开销,因为所有数据都必须从实体类转移到业务类。当然,对于新的和改变的对象,这种转移必须以相反的方向实现。这种对象到对象的映射(OOM)不能与像 Entity Framework Core 这样的对象关系映射器一起工作。但是,也有其他用于对象到对象映射的框架,比如 AutoMapper ( http://automapper.org )和 value injector(http://valueinjecter.codeplex.com)。但是即使有这样的框架,实现工作也是很重要的,特别是因为没有用于对象到对象映射的图形化设计器。

此外,由于额外的映射需要计算时间,因此不仅在开发时,而且在运行时,工作量都更大。

分布式系统

图 18-5 、图 18-6 和图 18-7 展示了以实体框架为数据访问核心的分布式系统的六种软件架构模型。现在,客户端不能直接访问数据库,但是应用服务器上有一个服务外观,客户端中有代理类(调用服务外观)。在业务逻辑层和数据访问层的划分方面,您有与架构 1 和架构 2 相同的选择。这里没有显示这些选项。更多的是关于实体类。如果您在客户端和服务器端使用相同的类,这称为共享契约。每当服务器和客户机被写入时,这都是可能的。因此,客户端可以使用服务器中的类引用该程序集。共享合同的情况如图 18-5 左侧架构 4 所示;这里,客户端也使用实体框架核心实体类。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18-7

More Entity Framework Core–based software architecture models with distribution

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18-6

Entity Framework Core–based software architecture models in a distribution system

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18-5

Entity Framework Core–based software architecture models in a distribution system

如果客户端有不同的平台,那么您必须为实体类创建代理类。在图 18-5 所示的架构 5 中,显式代理类是期望的或必要的,因为客户端不是. NET。

架构模型 6 到 9 的区别仅在于实体类的映射。

  • 尽管 architecture 6 使用共享契约,但它将实体类映射到为在线传输而优化的 DTO 类。在客户端,有另一个映射到 ViewModel 类的 OO。
  • Architecture 7 假设代理类和到 ViewModel 类的 OO 映射。
  • 架构 8 使用 DTO、代理和视图模型类。
  • 最复杂的模型,architecture 9,也在客户端使用业务对象类。

你可能想知道谁使用 architecture 9。事实上,在我作为顾问的工作中,我看到许多软件架构都是这样精心设计的。这些是大型团队参与的项目,然而每个小的用户请求都需要很长的实现时间。

结论

软件架构师在使用实体框架核心时有很多架构选择。带宽从一个简单、实用的模型开始(有一些妥协),其中开发人员只需要实现三个程序集。在这里展示的架构模型的另一边,您至少需要 12 个组件。

选择哪种架构模型取决于各种因素。当然,这包括具体的需求、系统环境和可用软件开发人员的专业知识。而且预算也是一个重要因素。在我作为公司顾问的日常生活中,我一次又一次地体验到,软件架构师选择太复杂的架构,因为“纯”的原则不适应业务条件。在这样的系统中,即使是最小的用户请求(“我们仍然需要左边的字段”)通常也是非常耗时和昂贵的。许多项目因为不必要的复杂软件架构而失败。

Tip

使用尽可能少的层。在向软件架构模型添加另一个抽象之前,请三思。

十九、商业工具

本章介绍了可用于实体框架核心的商业工具。我绝不参与这些工具的开发或分发。

实体框架核心动力工具

微软为经典的实体框架提供了强大的工具,但是实体框架核心的重新发布现在由外部开发者实现。实体框架 Core Power Tools 是 Visual Studio 2017 的免费扩展。

|   | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-csharp-zh%2Fraw%2Fmaster%2Fdocs%2Fmod-data-access-ef-core%2Fimg%2FA461790_1_En_19_Figa_HTML.jpg&pos_id=img-TtgyR5gt-1723310316029) | | 工具名称 | 实体框架核心动力工具 | | 网站 | [`https://www.visualstudiogallery.msdn.microsoft.com/9674e1bb-d942-446a-9059-a8b4bd18dde2`](https://www.visualstudiogallery.msdn.microsoft.com/9674e1bb-d942-446a-9059-a8b4bd18dde2) | | 制造商 | 埃里克·埃斯基科夫·詹森(MVP), [`https://github.com/ErikEJ`](https://github.com/ErikEJ) | | 免费版本 | 是 | | 商业版 | 不 |

特征

安装后,可以通过 Visual Studio 解决方案资源管理器中项目的上下文菜单来访问实体框架核心功能工具(图 19-1 )。该附件提供以下功能:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-1

Entity Framework Core Power Tools in the context menu of a project in Solution Explorer

  • 用于对 SQL Server、SQL Compact 和 SQLite 中的现有数据库进行反向工程的图形用户界面
  • 为给定的实体框架核心上下文创建图表的能力
  • 为数据库模式创建图表的能力
  • 显示 SQL DDL 命令的能力,用于为实体框架核心上下文及其实体类创建数据库模式

使用实体框架核心工具进行逆向工程

实体框架核心动力工具逆向工程由三个步骤组成。第一步,通过 Visual Studio 的标准对话框选择数据库(图 19-2 )。在第二步中,您选择桌子(图 19-3 )。您可以将表格选择保存为文本文件,并为新的调用重新加载(图 19-4 )。在第三步中,您设置选项,这些选项也是Scaffold-DbContext cmdlet 所允许的(图 19-5 )。之后,代码生成与Scaffold-DbContext中的相同(图 19-6 )。

Note

就像使用Scaffold-DbContext一样,在数据库改变后更新程序代码(从数据库更新模型)不是由 Power Tools 实现的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-2

Reverse engineering with Entity Framework Core Power Tools (step 1)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-3

Reverse engineering with Entity Framework Core Power Tools (step 2)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-4

Storage of the table selection in a text file

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-5

Reverse engineering with Entity Framework Core Power Tools (step 3)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-6

Generated code with Entity Framework Core Power Tools reverse engineering

带有实体框架核心功能工具的图表

图 19-7 显示了实体框架核心模型的图形表示,作为一个由命令 Add DbContext Model Diagram 生成的有向图标记语言(DGML)文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-7

Entity Framework Core model as a diagram with Entity Framework Core Power Tools

你也可以在运行时用扩展方法AsDgml()生成这个图,这个扩展方法在 NuGet 包ErikEJ.EntityFrameworkCore.DgmlBuilder中可用(清单 19-1 )。

using System;

namespace EFC_PowerTools
{
 class Program
 {
  static void Main(string[] args)
  {
   using (var ctx = new Wwwingsv2_ENContext())
   {
    var path = System.IO.Path.GetTempFileName() + ".dgml";
    System.IO.File.WriteAllText(path, ctx.AsDgml(), System.Text.Encoding.UTF8);
    Console.WriteLine("file saved:" + path);
   }

  }
 }
}

Listing 19-1Using AsDgml( )

linqpad

语言集成查询(LINQ)因其静态类型而受到开发人员的欢迎。但是总是不得不运行编译器来尝试 LINQ 命令是很烦人的。当您在 Microsoft SQL Server 管理中使用查询编辑器时,您输入一个 SQL 命令,按 F5 键(或单击“执行”),然后查看结果。微软曾经想过让 LINQ 的实体以同样的方式在 Management Studio 中运行实体框架,但至今没有任何成果。

第三方工具 LINQPad 允许 LINQ 命令的交互输入和在编辑器中的直接执行。您可以对 RAM 中的对象(对象的 LINQ)、实体框架/实体框架核心和各种其他 LINQ 提供者执行 LINQ 命令。

|   | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-csharp-zh%2Fraw%2Fmaster%2Fdocs%2Fmod-data-access-ef-core%2Fimg%2FA461790_1_En_19_Figb_HTML.jpg&pos_id=img-NVBSch8t-1723310316031) | | 工具名称 | linqpad | | 网站(全球资讯网的主机站) | [`www.linqpad.net`](http://www.linqpad.net) | | 制造商 | 约瑟夫·阿尔巴哈里,澳大利亚 | | 免费版本 | 是 | | 商业版 | 起价 45 美元 |

LINQPad 是免费的免费软件版本。但是如果你想享受 Visual Studio 风格的智能感知输入支持,你必须购买专业版或高级版。在高级版本中,也有许多包含的程序代码片段。同样,在高级版本中,您可以使用几个数据库来定义查询。当前版本 5 的系统要求是。NET 框架 4.6。只有 5MB 的大小,这个应用是非常轻量级的。该工具的作者说,“当你安装它的时候,它不会减慢你的电脑速度!”

使用 LINQPad

LINQPad 在左上角显示一个连接窗口(见图 19-8 )。在下面,您可以从提供的示例集合中进行选择(简单地说,来自 C# 6.0 这本书)或者保存您自己的命令(在我的查询下)。在主区域你会发现编辑器在顶部,输出区域在底部(见图 19-8 的中间/右侧)。

LINQPad 支持 C#、Visual Basic 的语法。NET,和 F# 以及 SQL 和实体 SQL(后者只针对经典的实体框架)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-8

LINQPad in action with LINQ to Objects

包括数据源

要针对实体框架核心上下文运行 LINQ 命令,必须使用添加连接添加连接。然而,该对话框目前仅显示 LINQ 到 SQL 和经典实体框架的驱动程序。通过查看更多驱动程序,您可以下载实体框架核心的驱动程序(图 19-9 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-9

Adding Entity Framework Core drivers for LINQPad

添加驱动程序后,您应该能够选择实体框架核心(图 19-10 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-10

Selecting Entity Framework Core as the LINQPad driver

选择提供者之后,您必须集成一个实体框架核心上下文。为此,使用浏览(见图 19-11 )来选择一个实现这种上下文的. NET 程序集。

Note

LINQPad 本身不为实体框架和实体框架核心创建上下文类。您总是需要使用 Visual Studio 或其他工具来创建和编译这样的类。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-11

An Entity Framework Core context class has been selected

合并上下文后,你可以在 LINQPad 的左边看到现有的实体类(图 19-12 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-12

After incorporating the context class

执行 LINQ 命令

一些命令可以直接从实体类的上下文菜单中执行(见图 19-13 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-13

Predefined commands in the context menu of the entity class

在查询区域,您甚至可以输入命令(在支持输入的商业版本中)。图 19-14 显示了一个带有条件、预测和急切加载的 LINQ 命令。在急切加载的情况下,结果视图是分层的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-14

Execution of a custom LINQ command in LINQPad

除了结果视图之外,还可以在输出区域的其它选项卡中以下列格式显示“LINQ 到实体”命令:

  • lambda 语法中的 LINQ 命令
  • SQL 形式的 LINQ 命令
  • 微软中间语言(IL)中的 LINQ 命令
  • LINQ 命令作为表达式树

节约

查询可以保存为文件扩展名为. linq的文本文件。

结果可以导出为 HTML、Word 和 Excel 格式。

其他 LINQPad 驱动程序

除了实体框架和实体框架核心驱动程序之外,LINQPad 还提供其他工具的驱动程序,如下所示:

  • 开放数据协议(OData)源
  • 关系数据库 Oracle、MySQL、SQLite、RavenDB
  • 云服务微软 StreamInsight 和 Azure 表存储
  • Windows 事件跟踪(ETW)
  • ORM mappers Mindscape LightSpeed、LLBLGen Pro、DevExpress XPO、DevForce
  • NoSQL 数据库文件数据库

交互式程序代码输入

除了运行 LINQ 命令之外,LINQPad 工具还可以执行任何其他 C#、F# 和 Visual Basic 命令。可以在语言下选择表达模式和陈述模式。表达式模式捕捉单个表达式,然后打印结果,如System.DateTime.Now.ToString(new System.Globalization.CultureInfo("ya-JP"))所示。这些表达式应该以分号结束。一次只能做一个表情。如果编辑器中有多个表达式,必须首先标记要执行的表达式。

另一方面,在语句模式下,记录完整的程序代码片段,每个命令以分号结束。你用Console.WriteLine()发出命令。清单 19-2 显示了一个小的测试程序。

<Query Kind="Statements" />

for (int i = 0; i < 10; i++)
{
    Console.WriteLine(i);
}

Listing 19-2Small Test Program for LINQPad

也定义你自己的类型(z.Classes;见清单 19-3 是可能的。但是,请注意,LINQPad 将捕获的代码嵌入到自己的默认代码中。因此,以下规则适用:

  • 要执行的主程序代码必须在顶部。
  • 在下面的类型定义之前,它必须用一个额外的花括号括起来。
  • 类型定义必须在末尾,并且最后一个类型定义不能有右花括号。

因此,在内部,LINQPad 显然用顶部的main()和底部的花括号来补充类型定义。

<Query Kind="Statements" />

var e = new Result() { a = 1, b = 20 };

for (int i = 0; i < e.b; i++)
{
    e.a += i;
    Console.WriteLine(i + ";" + e.a);
}
} // This extra parenthesis is required!

// The type definition must be after the main program!
class Result
{
    public int a { get; set; }
    public int b { get; set; }
// // here you have to omit the parenthesis!

Listing 19-3Small Test Program for LINQPad with Class Definition

LINQPad 的结论

LINQPad 是一个非常有用的工具,可以用来学习 LINQ、测试 LINQ 命令,通常还可以用来测试 C#、Visual Basic 和 F# 中的命令,而无需启动像 Visual Studio 这样的重量级程序或在现有项目中安装一个变通例程。由于实用的导出功能,LINQPad 不仅可以用于开发,还可以在日常实践中用作专门的数据库查询工具。

实体开发者

微软还没有为实体框架核心提供 GUI 开发工具。DevArt 和产品实体开发人员已经弥合了这一差距。

在过去,DevArt 为经典的实体框架提供了比微软本身更多的工具功能。现在,它又以实体框架核心工具领先。Entity Developer 通过图形设计器支持实体框架核心中的反向工程和正向工程。

|   | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-csharp-zh%2Fraw%2Fmaster%2Fdocs%2Fmod-data-access-ef-core%2Fimg%2FA461790_1_En_19_Figc_HTML.jpg&pos_id=img-Wp8dTERk-1723310316033) | | 工具名称 | 实体开发者 | | 网站 | [`www.devart.com/entitydeveloper`](http://www.devart.com/entitydeveloper) | | 制造商 | 捷克共和国德瓦特 | | 免费版本 | 是 | | 商业版 | 99.95 美元起 |

图 19-15 显示了可用的产品变型。免费速成版最多可以管理十个表的模型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-15

Variants of Entity Developer

安装 Entity Developer 时,除了独立的 Entity Developer 应用之外,安装程序已经提供了与 Visual Studio 2015 和 Visual Studio 2017 的 VSIX 集成。这个应用非常小,需要大约 60MB 的磁盘空间。

选择 ORM 技术

根据安装的变体,Entity Developer 在启动时提供不同的 ORM 技术。对于实体框架核心,创建一个.efml文件,对于经典实体框架,创建一个.edml文件,对于 Telerik 数据访问,创建一个.daml文件,对于 NHibernate,创建一个.hbml文件。选择 EF 核心模型后(见图 19-16 ),向导的第二步是逆向工程(这里称为数据库优先)和正向工程(这里称为模型优先)之间的决策,如图 19-17 所示。除了微软 SQL Server 之外,Entity Developer 还支持 Oracle、MySQL、PostgreSQL、SQLite 和 IBMs DB2 作为数据库,每一个都结合了 DevArt 自己的实体框架核心驱动(参见 https://www.devart.com/dotconnect/#database )。

Note

在 Visual Studio 中使用 Entity Developer 时,ORM 技术没有选择向导;取而代之的是 DevArt EF 核心模型、DevArt NHibernate 模型等特定的元素模板。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-16

Selecting the ORM technique in Entity Developer

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-17

Entity Developer templates within Visual Studio

使用实体开发人员进行逆向工程

然后,数据库首先选择一个现有的数据库,并选择工件(表、视图、存储过程和表值函数),就像在 Visual Studio 的经典实体框架向导中一样,但好处是开发人员可以向下选择列级别(图 19-18 )。

接下来是代码生成命名约定向导中的一个页面,它远远超出了 Visual Studio 目前所提供的内容(图 19-19 )。在下面的选项页面中,一些选项(如 N:M 关系和每类型继承表)是灰色的,因为实体框架核心尚不具备这些映射能力(图 19-20 )。在倒数第二步中,您可以选择是将所有的工件都放在图表表面上,还是只将选定的工件放在表面上。也可以为每个模式名创建一个图表(图 19-21 )。对于每个图表,都会创建一个.view文件。

在最后一步中,您选择代码生成模板。实体开发者提供直接应用多个代码生成模板(图 19-22 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-22

Selecting diagram content

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-21

Selecting model properties

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-20

Many settings for the naming conventions of classes and class members in the code to be generated in Entity Developer

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-19

Selection of artifacts down to the column level

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-18

Selection of the process model

图 19-23 显示了 Entity Developer 提供的模板,在 https://www.devart.com/entitydeveloper/templates.html 几乎没有记录。您必须弄清楚生成的代码是否符合您的需求。可以使用“复制到模型文件夹”功能将预定义的模板复制到您自己的应用文件夹中的模板文件中,然后在那里进行修改。这些模板类似于 Visual Studio 中使用的文本模板转换工具包(??)模板,但是它们不兼容。与 ?? 模板不同,DevArt 模板允许代码生成受属性网格中设置的参数的影响。例如,您可以为名为 EF Core 的选定模板指定以下内容:

  • 您可以指定实体类和上下文类在不同文件夹中的着陆(这里您可以捕获相对或绝对路径)。
  • 可能会生成分部类。
  • 接口INotifyPropertyChangingINotifyPropertyChanged可以在实体类中实现。
  • 您可以设置实体类接收 Windows Communication Foundation(WCF)的注释[DataContract][DataMember]
  • 您可以设置实体类接收注释[Serializable]
  • 您可以覆盖实体类Equals()
  • 可以实现IClonable的实体类。

实体开发人员将图表名称、模板及其参数以及生成文件列表存储在一个.edps文件中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-23

Selecting the code generation template

完成非常灵活的助手后,当在设计器中查看模型时,您可能会有点失望,至少如果您选择了数据库视图。实体开发人员抱怨没有主键。这是因为实体框架核心尚未设置为映射视图,并像表一样处理视图,这些视图总是需要有一个主键。您必须在属性窗口中为每个数据库视图手动设置一个。

即使继承是可能的,表之间的关系也在 Entity Developer 和 Entity Framework Core 中建模为关联。对于经典的实体框架,实体开发人员可以选择识别每种类型的表继承,但是实体框架核心还不支持按类型的表继承。

现在,您可以在模型浏览器中调整图表或创建新图表(参见图 19-24 的左侧)。你可以从数据库浏览器中直接拖放额外的表、视图、过程和函数到模型中(见图 19-24 的右边),而不必在 Visual Studio 中反复运行向导。与 Visual Studio 中的经典实体框架工具一样,Entity Developer 可以管理每个重叠实体模型的多个图表。你可以通过拖放来改变实体类中属性的顺序,而奇怪的是,用微软的工具来改变顺序只能通过繁琐的上下文菜单或键盘快捷键来实现。不同实体之间也可以拖放属性。Entity Developer 允许将颜色分配给模型中的实体,以实现更好的视觉分离。然后,这种颜色会应用到实体所在的所有图中。在图表表面上,您还可以随时添加注释。

程序代码生成由菜单项“车型➤生成代码”(按键 F7)触发。标准代码生成模板 EF Core 创建了以下内容:

  • 上下文类
  • 每个表和每个视图一个实体类
  • 存储过程的每个返回类型的类

使用存储过程和表值函数的程序代码在 context 类中,它可能很长。有趣的是,实体开发人员并不依赖实体框架核心来实现,而是通过DataReader拾取数据记录,并自己实现完整的映射(参见清单 19-4 )。毕竟,Entity Developer 认识到清单中显示的存储过程GetFlight返回与表Flight相同的结构,因此在返回类型中使用实体类Flight。实体开发者或模板也可以通过扩展方法FromSql()使用实体框架核心。DevArt 自己实现的优点是它也适用于不返回实体类型的存储过程。实体框架核心还不能做到这一点。在这些情况下,Entity Developer 为返回类型创建自己的类。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-24

Graphical designer in Entity Developer

public List<Flight> GetFlight(System.Nullable<int> FlightNo)
  {

   List<Flight> result = new List<Flight>();

   DbConnection connection = this.Database.GetDbConnection();
   bool needClose = false;
   if (connection.State != ConnectionState.Open)
   {
    connection.Open();
    needClose = true;
   }

   try
   {
    using (DbCommand cmd = connection.CreateCommand())
    {
     if (this.Database.GetCommandTimeout().HasValue)
      cmd.CommandTimeout = this.Database.GetCommandTimeout().Value;
     cmd.CommandType = CommandType.StoredProcedure;
     cmd.CommandText = @"Operation.GetFlight";

     DbParameter FlightNoParameter = cmd.CreateParameter();
     FlightNoParameter.ParameterName = "FlightNo";
     FlightNoParameter.Direction = ParameterDirection.Input;
     if (FlightNo.HasValue)
     {
      FlightNoParameter.Value = FlightNo.Value;
     }
     else
     {
      FlightNoParameter.DbType = DbType.Int32;
      FlightNoParameter.Size = -1;
      FlightNoParameter.Value = DBNull.Value;
     }
     cmd.Parameters.Add(FlightNoParameter);

     using (IDataReader reader = cmd.ExecuteReader())
     {
      while (reader.Read())
      {
       Flight row = new Flight();
       if (!reader.IsDBNull(reader.GetOrdinal("FlightNo")))
        row.FlightNo = (int)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"FlightNo")), typeof(int));

       if (!reader.IsDBNull(reader.GetOrdinal("Timestamp")))
        row.Timestamp = (byte[])Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Timestamp")), typeof(byte[]));
       else
        row.Timestamp = null;

       if (!reader.IsDBNull(reader.GetOrdinal("Airline")))
        row.Airline = (string)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Airline")), typeof(string));
       else
        row.Airline = null;

       if (!reader.IsDBNull(reader.GetOrdinal("Departure")))
        row.Departure = (string)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Departure")), typeof(string));

       if (!reader.IsDBNull(reader.GetOrdinal("Destination")))
        row.Destination = (string)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Destination")), typeof(string));

       if (!reader.IsDBNull(reader.GetOrdinal("FlightDate")))
        row.FlightDate = (System.DateTime)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"FlightDate")), typeof(System.DateTime));

       if (!reader.IsDBNull(reader.GetOrdinal("NonSmokingFlight")))
        row.NonSmokingFlight = (bool)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"NonSmokingFlight")), typeof(bool));

       if (!reader.IsDBNull(reader.GetOrdinal("Seats")))
        row.Seats = (short)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Seats")), typeof(short));

       if (!reader.IsDBNull(reader.GetOrdinal("FreeSeats")))
        row.FreeSeats = (short)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"FreeSeats")), typeof(short));
       else
        row.FreeSeats = null;

       if (!reader.IsDBNull(reader.GetOrdinal("Pilot_PersonID")))
        row.PilotPersonID = (int)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Pilot_PersonID")), typeof(int));
       else
        row.PilotPersonID = null;

       if (!reader.IsDBNull(reader.GetOrdinal("Memo")))
        row.Memo = (string)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Memo")), typeof(string));
       else
        row.Memo = null;

       if (!reader.IsDBNull(reader.GetOrdinal("Strikebound")))
        row.Strikebound = (bool)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"Strikebound")), typeof(bool));
       else
        row.Strikebound = null;

       if (!reader.IsDBNull(reader.GetOrdinal("`Utilization `")))
        row.Utilization = (int)Convert.ChangeType(reader.GetValue(reader.GetOrdinal(@"`Utilization `")), typeof(int));
       else
        row.Utilization = null;

       result.Add(row);
      }
     }
    }
   }
   finally
   {
    if (needClose)
     connection.Close();
   }
   return result;
  }

Listing 19-4Mapping for the Stored Procedure GetFlight( )

如果您尚未执行这些步骤,现在可以将生成的程序代码包含在 Visual Studio 项目中。使用的模板文件可以在 Entity Developer 中随时调整(参见模型资源管理器中的分支模板)。还可以在 Entity Developer 中编辑模板,包括 IntelliSense 输入支持。

或者,您也可以使用已安装的 Visual Studio 扩展。在 Visual Studio 中,您会在类别数据下的元素模板中找到像 DevArt EF Core Model 这样的新条目。选择其中之一将打开与独立应用相同的助手,并最终打开相同的设计器(包括模型浏览器和模板编辑器)。好处是生成的程序代码自动属于 Visual Studio 项目,其中还包含了.efml文件。

如果数据库模式已更改,您可以使用菜单项“模型➤从数据库更新模型”来更新模型。您可以使用定义数据库类型的一般映射规则。工具➤选项➤服务器选项下的. NET 类型。

数据预览也很有帮助(在实体类的上下文菜单中选择检索数据);它包括导航到链接的数据记录和分层展开。您也可以直接从每个实体的上下文菜单中的图表或从数据库浏览器中的表或视图访问数据预览。

与实体开发人员一起进行正向工程设计

我现在将介绍实体开发人员的正向工程。选择 Model First 后,对话框 Model Properties 打开,因为既没有现有的数据库,也没有它的工件或任何命名约定可供选择。使用模型优先设置中的设置,您可以设置数据库模式生成的标准。

  • 默认精度:对于十进制数,是逗号前的位数
  • 默认小数位数:对于小数,是小数点后的位数
  • 默认长度:对于字符串,最大字符数(空表示字符串不受限制)

在模型优先的情况下,向导的第三步也是最后一步是用于选择代码生成模板的对话框。

然后出现空的设计器界面,您可以使用模型资源管理器中的符号用类、枚举、关联和继承关系填充该界面。然后通过属性窗口配置它们(见图 19-25 )。例如,您可以设置主键,启用[ConcurrencyCheck]注释,并指定一个属性为存在于数据库中但不存在于生成的实体类中的影子属性。实体框架核心中有一些不可用的选项,比如 N:M 映射,实体开发人员甚至没有在实体框架核心模型中提供。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-25

Creation and configuration of new properties in Entity Developer

使用函数模型➤从模型更新数据库,您可以从中创建数据库模式。该向导要求目标数据库,该数据库必须已经存在。然后,向导会显示哪些架构更改将被传输到数据库,并提供不传输某些更改的选项。在最后一步中,您可以查看要执行的 SQL 脚本。实体开发者不使用实体框架核心的命令行模式迁移工具(dotnet ef或 PowerShell cmdlets 相反,它使用自己的方法将现有模式与目标模式进行比较。但是,Entity Developer 也会尝试获取数据。使用重建数据库表选项,您可以使现有的表(包括它们的数据)消失。实体开发人员迁移中不存在名为__EFMigrationsHistory的附加表。您可以在“模型➤设置➤同步➤数据库命名”下定义要生成的数据库模式中的命名约定。

在模型➤的“从模型生成数据库脚本”菜单中,您可以为要生成的模式生成 SQL 脚本,而无需引用特定的数据库。这样,您可以配置目标数据库管理系统和版本(例如,对于 SQL Server ),如 2000、2005、2008、2012、2014 和 Azure(但不是 SQL Server 2016)。

实体开发者用很多小事来支持。例如,先前指定的默认值“无名称”不仅输入到数据库模式中,还用于实体类的构造函数中(参见清单 19-5 ),它是通过模型➤生成的代码(F7)在逆向工程中生成的。在代码生成设置中,设置 inotifypropertychanging➤inotifypropertychanging 和 WCF 数据协定属性已激活。用类和属性“名字”填充的注释已经输入到实体开发人员设计器中。此外,在 Entity Developer 中,您可以捕获对实体类型和属性的任何注释。为此,首先在上下文菜单中选择属性,然后选择。任何. NET 属性。NET 程序集。如果。NET 属性需要参数,您可以在对话框中捕获它们。您可以在属性窗口中设置一些注释,如[DisplayName][Range][RegularExpression](参见图 19-25 左下角的“验证”)。为了使验证注释在生成的程序代码中真正永久化,您必须在代码生成模板中选择一个验证框架。除了。NET 验证批注,您可以选择旧的。NET 企业库或 NHibernate 验证器。

有趣的是,您可以将属性网格扩展到任何设置。然后可以在代码生成过程中考虑这些设置。其他设置在“模型➤设置➤模型”下的树中定义,该树显示在“模型➤扩展属性”下,用于工件,如类、属性和关联。然后,您必须考虑这些附加设置在单独的.tmpl代码生成模板中的意义。

//-------------------------------------------------------------------------
// This is auto-generated code.
//-------------------------------------------------------------------------
// This code was generated by Entity Developer tool using EF Core template.
// Code is generated on: 31/12/2017 00:04:31
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//-------------------------------------------------------------------------

using System;
using System.Data;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Data.Common;
using System.Collections.Generic;

namespace Model
{
    public partial class Person {

        public Person()
        {
            OnCreated();
        }

        public virtual string ID
        {
            get;
            set;
        }

        public virtual string Name
        {
            get;
            set;
        }

        public virtual System.DateTime Birthday
        {
            get;
            set;
        }

        #region Extensibility Method Definitions

        partial void OnCreated();

        #endregion
    }

}

//-------------------------------------------------------------------------
// This is auto-generated code.
//-------------------------------------------------------------------------
// This code was generated by Entity Developer tool using EF Core template.
// Code is generated on: 31/12/2017 00:04:31
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//-------------------------------------------------------------------------

using System;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.ComponentModel;
using System.Reflection;
using System.Data.Common;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Model
{

    public partial class Model : DbContext
    {

        public Model() :
            base()
        {
            OnCreated();
        }

        public Model(DbContextOptions<Model> options) :
            base(options)
        {
            OnCreated();
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any(ext => !string.IsNullOrEmpty(ext.ConnectionString) || ext.Connection != null))
            CustomizeConfiguration(ref optionsBuilder);
            base.OnConfiguring(optionsBuilder);
        }

        partial void CustomizeConfiguration(ref DbContextOptionsBuilder optionsBuilder);

        public virtual DbSet<Person> People
        {
            get;
            set;
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            this.PersonMapping(modelBuilder);
            this.CustomizePersonMapping(modelBuilder);

            RelationshipsMapping(modelBuilder);
            CustomizeMapping(ref modelBuilder);
        }

        #region Person Mapping

        private void PersonMapping(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Person>().ToTable(@"People");
            modelBuilder.Entity<Person>().Property<string>(x => x.ID).HasColumnName(@"ID").IsRequired().ValueGeneratedNever();
            modelBuilder.Entity<Person>().Property<string>(x => x.Name).HasColumnName(@"Name").IsRequired().ValueGeneratedNever();
            modelBuilder.Entity<Person>().Property<System.DateTime>(x => x.Birthday).HasColumnName(@"Birthday").HasColumnType(@"datetime2").IsRequired().ValueGeneratedNever();
            modelBuilder.Entity<Person>().HasKey(@"ID");
        }

        partial void CustomizePersonMapping(ModelBuilder modelBuilder);

        #endregion

        private void RelationshipsMapping(ModelBuilder modelBuilder)
        {
        }

        partial void CustomizeMapping(ref ModelBuilder modelBuilder);

        public bool HasChanges()
        {
            return ChangeTracker.Entries().Any(e => e.State == Microsoft.EntityFrameworkCore.EntityState.Added || e.State == Microsoft.EntityFrameworkCore.EntityState.Modified || e.State == Microsoft.EntityFrameworkCore.EntityState.Deleted);
        }

        partial void OnCreated();
    }
}

Listing 19-5Example of an Entity Class Generated by Entity Developer

实体框架分析器

对象-关系映射意味着从 SQL 中抽象出来,自然会出现这样的问题:哪些命令以及有多少命令实际上被发送到数据库管理系统。您可以使用 DBMS 自己的探查器(如 Microsoft SQL Server 探查器)或特定于 ORM 的工具(如实体框架探查器)来监视通信。

几乎所有的 OR mappers 都使用自己的查询语言,比如 NHibernate 上的 HQL,Entity Framework 和 Entity Framework Core 上的 LINQ。这些语言在数据库无关的对象模型上工作,OR 映射器翻译成每个数据库管理系统的 SQL 方言。SQL 命令的自动生成总是对 ORM 的基本批评的起点,尤其是关于 SQL 优化器的仓库。事实上,并不是所有由 OR 映射器生成的 OR 语句都是最佳的。

使用 OR 映射器的软件开发人员的职责之一就是跟踪非优化的 SQL 和不利的加载策略。这就是冬眠犀牛公司的实体框架分析器的用武之地。它与实体框架核心和经典实体框架一起工作。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-26

Entity Framework Profiler licensing options

|   | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-csharp-zh%2Fraw%2Fmaster%2Fdocs%2Fmod-data-access-ef-core%2Fimg%2FA461790_1_En_19_Figd_HTML.jpg&pos_id=img-mSPSXLeO-1723310316036) | | 工具名称 | 实体框架分析器 | | 网站 | [`www.efprof.com`](http://www.efprof.com) | | 制造商 | 冬眠的犀牛,以色列 | | 免费版本 | 不 | | 商业版 | 每月 45 美元起 |

集成实体框架分析器

为了使实体框架探查器能够记录 or 映射器和数据库管理系统之间的活动,必须对要监控的应用进行“检测”这需要对程序代码进行两处修改。

  • 开发者必须有对HibernatingRhinos装配.Profiler.CreateAppender .dll的引用。

Tip

这个程序集附带了实体框架分析器(文件夹/Appender)的三个版本:for。NET 3.5,对于。NET 4.x,并作为. NET 标准程序集(包括。网芯)。虽然您可以使用经典的。在. NET 核心项目中,你不应该直接从/Appender/netstandard/文件夹中引用程序集。相反,你应该通过 NuGet ( Install-Package EntityFrameworkProfiler.Appender)来安装它。否则,您可能会丢失依赖项。

  • 在程序开始时(或者在程序中您希望开始分析的地方),程序行HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize()出现在程序代码中。

Tip

实体框架探查器不要求应用在 Visual Studio 调试器中运行。如果应用是直接启动的,即使它是在发布模式下编译的,也可以进行记录。这是一行检测代码。因此,您可以创建应用,以便在需要时调用插装代码;例如,它可以由配置文件控制。

使用实体框架探查器监视命令

在要监控的应用之前,启动基于 WPF 的实体框架分析器用户界面(EFProf.exe)。启动要监控的应用后,实体框架概要分析器(在左侧列表中)显示实体框架类ObjectContext(或所有派生类)的所有已创建实例。不幸的是,单独的上下文实例没有命名;您必须自己在实体框架分析器用户界面中指定它们。

每个上下文包含通过上下文执行的 SQL 命令的数量以及相应的执行时间,包括 DBMS 中的执行时间和总时间,包括 RAM 中对象的具体化。例如,在图 19-27 中,问题出现了,许多对象上下文是在根本没有执行任何命令的情况下创建的。

Note

实体框架分析器谈到了对象上下文,在经典的实体框架中,对象上下文是实体框架上下文的原始基类。在实体框架核心中,只有更现代的DbContext。实体框架探查器中的名称未更改。然而,实体框架分析器在实体框架和实体框架核心中都使用DbContext基类。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-27

Entity Framework Profiler in action

在屏幕的右边部分,您会发现当前所选上下文的已执行命令列表。在详细信息下,您可以看到完整的带参数的 SQL 命令和相关的执行计划(见图 19-28 )以及结果集。然而,要做到这一点,您必须在实体框架分析器中输入连接字符串(参见图 19-29 )。

Stack Trace 选项卡显示哪个方法触发了一个 SQL 命令。双击“堆栈跟踪”选项卡中的条目可以直接在打开的 Visual Studio 窗口中找到匹配的代码,这很好。这将帮助您快速找到触发 SQL 命令的 LINQ 或 SQL 命令。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-29

Displaying the result in the Entity Framework Profiler

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-28

Execution plan of the database management system in the Entity Framework Profiler

警报和建议

特别注意灰色圆圈(建议)和红色圆圈(警告)(见图 19-30 )。这里,实体框架分析器帮助您发现潜在的问题。在图 19-30 中,这就是实体框架分析器所说的SELECT N + 1问题。一个接一个地执行大量类似的 SQL 命令表明这里错误地使用了延迟加载。你应该考虑急于装货。

Entity Framework Profiler 很好地展示了另一个问题,即不推荐在不同的线程中使用上下文对象。其他提示(见图 19-30 )存在于查询使用许多连接,以通配符开始(如% xy),返回许多记录,并且不包含TOP语句(无界结果集)。最后一点是有争议的。这个提议的意图是,您不应该冒险要求比您实际预期需要的更多的记录。但是在实践中(除了使用滚动特性显式显示记录的应用),您通常无法设置永久适用的上限。当许多INSERTUPDATEDELETE命令被执行时,也有一个警告,你应该检查这不是由批量操作映射的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-30

Alerts and suggestions in the Entity Framework Profiler

分析

分析选项卡中的分析功能非常有用。您会发现评估显示了以下内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-31

Analysis of Queries By Method

  • 程序代码中的哪些方法触发了哪些 SQL 命令(按方法查询;参见图 19-31
  • 存在多少不同的命令(尽管名称为唯一查询,INSERTUPDATEDELETE命令也出现在这里!)
  • 哪些命令持续时间最长(昂贵的查询)

一个有趣的功能是隐藏在文件➤导出到文件菜单。这将创建一个 JavaScript 丰富的 HTML 页面,看起来像实体框架分析器。您可以查看所有的上下文实例和 SQL 命令,并调用分析结果。但是,缺少堆栈跟踪和警告。

正常存储功能创建一个二进制文件,文件扩展名为.efprof。被监控的程序代码也可以通过调用 start 命令中的方法InitializeOfflineProfiling(filename.efprof)而不是Initialize()直接生成这样的文件。然后,在应用运行时,不必运行实体框架分析器 UI。因此,在目标系统上进行概要分析也是可能的,不会出现问题。

命令行支持和 API

就持续集成而言,您还可以从命令行运行实体框架分析器。但是每台计算机需要一个许可证。分析器本身在HibernatingRhinos.Profiler.Integration.dll中也有一个编程接口。

实体框架分析器的结论

Entity Framework Profiler 是一个有用的工具,可以用来了解基于 Entity Framework 核心的应用实际执行哪些 SQL 命令。但是,价格高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值