LINQ 在多层应用程序中






4.17/5 (14投票s)
高效地传输数据在 LINQ 实体类和您自己的数据传输对象之间。
引言
如果您有机会使用 LINQ,即 .NET 3.5 平台中最新的对象关系映射 (ORM) 工具,那么您就会明白 LINQ 如何将数据库结构映射到您应用程序中的多个类。它会为每个表创建一个实体类,该类的属性映射到相关表中的列。ORM 工具的全部意义在于将数据库结构提升到语言的层面,以便您的代码在编译时就了解该结构。如今,开发人员倾向于将应用程序的体系结构划分为几个不同的层,通常是 3 层。
- 物理层,封装以某种方式访问数据库的代码
- 业务层,通常包含您的业务逻辑代码
- 用户界面层,通常是 .NET 桌面应用程序或 ASP.NET 应用程序
我将不去详细介绍将设计划分为多层的好处,也不讨论 LINQ 本身的长处和短处,因为互联网上有很多关于这个主题的文章。
当您在物理层中使用 LINQ 访问数据库时,LINQ 会创建实体对象。一个简单的例子可以说明这一点。如果您有一个名为 Accounts
的表,那么 LINQ 会创建一个名为 Account
的类,其属性设置与数据库中的列类似。现在,如果您使用该类本身将数据传输到上层(特别是到表示层),您就会意识到您的应用程序将不再是多层的依赖关系。您的表示层将“看到”由物理层创建的实体类,这很糟糕,因为您的层不再像人们希望的那样松耦合,而是紧密集成在一起。
为了保持多层体系结构的理念,开发人员通常会创建一组并行的类来映射实体类,通过这样做,我们添加了一个必要的抽象层,以保持层之间依赖关系的清晰。这样,表示层将只依赖于业务层,而不再依赖于任何更低的层。
通常有两种方法可以创建这些中间类,也称为数据传输对象(DTO)。在您的代码的某个地方,您需要将数据在实体对象和您的数据传输对象之间进行传输。那么我们如何做到这一点呢?有些人选择手动编写这部分代码,这在性能方面是好的,但在维护这些类时会遇到大麻烦,更不用说浪费了多少人力在繁重的工作上了。有些人选择利用反射的强大功能,并编写一些代码在运行时根据对象的属性复制对象,这通常是首选方法。对象的属性通过反射在运行时发现,并相互复制。这种方法有一个很大的优点,就是您不需要为每一对类编写代码来将属性从实体类复制到数据传输对象,这为您节省了大量的维护时间和精力。不幸的是,这种方法存在性能低下的问题,因为反射很慢,而且直到您真正测试之前,您都不会知道性能有多低。
解决方案:更好的方法
如果能兼顾这两种方法的优点而避免它们的缺点,那会怎么样?如果您选择第一种方法,即为每一对类编写代码来复制它们的属性,但不是自己编写,而是有一个开发人员在运行时为您工作,并非常快速地为您编写这段代码,最重要的是,他免费工作。这位程序员就是 Emit 先生。
Emit 先生,全名 System.Reflection.Emit
,将在运行时使用 Reflection
来发现两个对象——实体和 DTO——然后为您编写一次代码,您可以在运行时使用这段代码高效地在两个对象之间复制属性。Reflection.Emit
是一个允许您使用 .NET 平台中间语言 IL(Intermediate Language)在运行时创建代码的 命名空间
。
Using the Code
使用这段代码非常简单,只需将其添加到您的解决方案或引用其 DLL。每当您有两个对象需要复制时,只需将它们传递给 静态
函数 DynamicFiller.FillObject(…)
,或者如果您的输入是一个对象数组而不是单个对象,则使用 DynamicFiller.FillArray
。该类中有一个名为 AllowedType
的方法。该方法包含该类允许复制的属性类型,当有未处理的属性类型时,您可以向其中添加。
性能优势
在我编写这段代码之前,我使用的是一段使用反射的代码。使用反射,在 1.8 Mhz Core 2 Duo Centrino CPU、2 GB RAM 的笔记本电脑上(非调试模式),复制 10,000 个对象(每个对象的类大约有 15 个属性)大约需要 32 秒,在调试模式下需要 1 分 19 秒。使用新的 Emit 代码执行相同的操作,在非调试模式下需要 19 毫秒,在调试模式下需要 22 毫秒。是的,没错,是毫秒。数字本身就说明了一切。
内部原理
解释如何编写 IL(中间语言)代码超出了本文的范围,但如果您有兴趣了解代码的作用及其工作原理,我将尽力给您一个初步的了解。代码会在运行时创建实现 IDynamicFiller
接口的类。IDynamicFiller
只有一个名为 FillObject
的方法,该方法接受两个 Object
类型的对象作为参数,类应该复制这些对象。当这些类在运行时被编码时,它们会被赋予一个取决于传入对象类型的复合名称,并被添加到字典中,这样下次需要复制相同类型的对象之间的数据时,就可以检索到同一个类。创建的类通过创建一个新的方法类 FillObject
来实现 IDynamicFiller
接口,在该方法中,传入的两个对象被强制转换为它们的正确类型,然后属性从一个对象复制到另一个对象。
结论
System.Reflection.Emit
是一个被低估的 命名空间
,因为它需要了解 IL 才能使用它。但它并没有那么难,如果您花点时间,回报是值得的。许多使用 Reflection 命名空间
开发的解决方案都可以使用 Emit 命名空间
来获得显著的性能提升。这是一个很好的例子。祝您编码愉快。
历史
- 2008 年 4 月 25 日:首次发布