65.9K
CodeProject 正在变化。 阅读更多。
Home

Code First:实际案例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (19投票s)

2012年2月26日

CPOL

15分钟阅读

viewsIcon

74869

downloadIcon

3808

代码优先的现实生活数据模型案例

引言

自从我听说Entity Framework新的Code First方法以来,我一直想尝试一下,因为这个概念对我来说相当有吸引力。对于我们这些还没有听说过它的人来说,使用Entity Framework的三个起点是:

  • 数据库优先,当我们从数据库端开始设计时。
  • 模型优先,当我们直接在Visual Studio中定义模型,创建实体并建立它们之间的适当关系时。
  • Code First,这是将在下一个.NET 4.5版本中得到巩固的新策略,它包括在代码中将实体声明为类。

本文无意成为Code First教程,而是要展示这项技术在中等复杂度的真实场景中的实际应用。结果可能缺乏一些测试、错误处理改进和日志记录,但它的表现良好,是演示Code First的一个好工具。

本文使用的Entity Framework版本是4.2,数据库提供程序版本是SQL Server Compact 4.0,它已经支持标识字段。

必备组件

如果您想运行该应用程序,您将需要预先安装SQL Server Compact 4.0(http://www.microsoft.com/download/en/details.aspx?id=17876)。

为了编辑源代码和调试它,您必须升级到Visual Studio 2010的SP1(如果您还没有这样做),并安装Tools for SQL Server Compact 4.0。

安装这些更新和组件的最简单方法是使用Microsoft的Web Platform Installer工具:http://www.microsoft.com/web/downloads/platform.aspx

描述

本文介绍的系统是一种小型多媒体目录工具,用于管理两个概念:电影和音频专辑。电影是视频文件,专辑是包含音频文件的文件夹,所有这些文件都采用任何常见的媒体格式和编码。用户将定义存储视频或音频专辑的本地文件夹,系统将递归地搜索这些文件夹及其子文件夹下的文件。

对于视频文件,系统将提取主视频轨道、所有音频轨道以及字幕轨道的语言信息。对于音频文件,只需要主音频轨道信息。为了实现这一点,使用了MediaInfo库(http://mediainfo.sourceforge.net)。

此外,对于视频文件,设计中包含了一个电影信息提供程序接口,用于收集有关演员、导演、电影海报、流派等信息。目前,只有一个信息提供程序已实现,即FilmAffinity网站爬虫(如果网站更改其格式或设计,可能会失败),但添加新的提供程序,例如imdb,将是清晰明了的。

对于音频文件,会读取ID3信息以生成专辑的概念。如果给定文件夹下所有音频文件的某个标签都不同,则会为该字段分配一个通用的“Multiple Values”字符串。

该系统由一个包含Code First实体定义、数据上下文、其上的主要操作以及文件夹处理的库,以及一个使用该库并允许用户配置和查询数据的WinForm项目组成。

UI应用程序

现在我将介绍我们的用户应用程序的主窗口。

336187/ui-main.jpg

有三个主要区域:

  • 顶部工具栏,包含主要命令和搜索选项。
  • 左侧结果列表,显示用户执行的搜索的匹配项。
  • 右侧详细信息面板,用于显示匹配列表中所选项目的详细信息。

聚焦于工具栏,我们将简要描述其中包含的命令和控件。

336187/ui-toolbar.png

  1. 打开媒体文件夹管理窗口,我们可以在其中添加或删除媒体文件夹并触发内容处理任务。
  2. 显示一个MessageBox,指示数据库中包含的每个感兴趣实体记录的数量。
  3. 媒体类型选择按钮。
  4. 媒体类型选择更改时,媒体流派combobox会根据新选择填充相应的值,并允许用户按流派进行过滤。
  5. 一个额外的文本过滤器,用于细化搜索或直接按文本搜索,而不是选择给定的流派。
  6. 此按钮执行搜索操作,应用前三个过滤器,并用找到的匹配项填充结果列表。

结果列表是ListView控件的一个重载,它使用Tile视图来显示每个项目最重要的信息。此控件有两个值得注意的地方:动态创建与列表关联的ImageList以显示伴随匹配项的图像,以及平铺的大小调整机制,以便它们扩展到列表宽度,使其看起来更像行列表而不是正常的平铺表示。

最后,详细信息面板显示结果列表中所选项目的可用信息,包括媒体轨道技术信息、ID3或电影信息提供程序的元数据以及文件属性。视图中包含一对额外的命令作为链接,但您需要自行发现它们的确切作用。

数据库

数据库根据我们之前介绍的,使用Code First方法定义了我们的数据实体,并声明了包含这些实体的data context,其中一些关系使用Fluent API进行细化。

模型

好了,现在是时候以我们声明的Code First注释、约定和Fluent API类的方式来探索我们的模型了。目标是包含继承和所有可能的类关系。

336187/yuml-model.png

这是专注于音频项的详细视图。

336187/yuml-model-audio.png

现在,这是另一个专注于视频元素的视图。

336187/yuml-model-video.png

至于模型图,它是非常自明的,因为实体所代表的概念(文件夹、文件、轨道……)应该对几乎所有人来说都很熟悉,但我们将通过另一张图来尝试阐明提出的实体创建机制。

336187/yuml-creation.png

在图的下部,我们可以看到LanguageCodeVideoPersonVideoGenreAudioGenre的创建方式。MediaCatalog中的getter方法返回请求实体的实例,如果该实体尚不存在于相应表中,则会先创建它。无论如何,库的最终用户将不需要处理这些方法,因为它们都主要由VideoMetaInfoAudioMetaInfo在内部使用。

在上部,我们在MediaFile类的定义中找到了一个static方法,这表明应通过调用它来创建VideoFileAudioFile实例。它是一种具有Builder本质的Factory方法,实现了MediaFile创建的常见操作,并将剩余的构建过程委托给受保护方法BuildFromMediaInfoBuildMetaInfo的具体实现。

仍然讨论MediaFileAddRemove方法是我们实现这些操作的方式,因为与MediaFile关联的AudioTrackInfo项目的删除被配置为不级联。

最后,尽管图中没有显示,MediaFolder实现了一个Remove方法,该方法处理其相关的MediaFile记录的删除以及一些清理工作,例如,目前,孤立的AudioAlbumMetaInfo记录的删除。

文件夹处理

数据库包含一个命名空间,其中包含必要的类来递归地更新目录中的媒体文件记录,删除已不存在的记录并添加新记录。

对于此处理,使用组合模式以及访问者模式,如以下图所示。如果您想深入了解,请查看源代码。

336187/yuml-tree.png

Code First分析

现在是时候更详细地了解Code First了,因此我们将尝试分析与之相关的数据模型中最有趣的部分。

您可以在MSDN上的此链接处找到很好的入门文档,您可以在其中阅读有关约定、注释和Fluent API的信息。

数据模型

作为参考,并为了能够直观地比较生成的实体模型和数据库架构,这里包含了两个图,因此您可以检查已应用的映射。

生成的实体模型

336187/edmx-model.png

映射的数据库架构

336187/database-model.png

约定

数据库中使用的主约定是主键约定,根据该约定,如果找到一个包含字符串“Id”或类名后跟该字符串的属性,而不考虑字符串“Id”中字符的大小写,Code First会推断出它是实体的primary key。此外,如果属性是整数类型,它将被映射到identity列。

注解

Code First注释是我们在此技术中使用时进一步定义数据模型的两种工具之一,另一种工具是前面提到的Fluent API。在大多数情况下,我们可以使用注释来具体化我们实体成员的特殊性,但对于某些关系,别无选择,只能使用Fluent API。最后,请注意,其中一些注释在将Code First与MVC结合使用时具有双重用途,尽管我们将只关注Code First的含义。

我们来枚举并简要评论解决方案中使用的一些注释:

  • [Required] – 该字段是必需的且非空。它也可以用于对其他实体和外键字段的引用。
  • [NotMapped] – 此注释允许我们定义不会映射到表列的属性。
  • [MaxLength(...)] – 用于限制数据库中文本字段的长度。
  • [Table("表名")] – 在实体类定义之上,用于指定与此实体映射的表的名称,而不是约定给出的名称。
  • [Key] – 当主键不符合约定规则时,我们使用此注释。
  • [Column(TypeName = "image")] – 用于将字节数组属性的存储格式指定为image。
  • [ComplexType] – 这表示以下类不会映射到实体,并且如果将其包含为实体类中的字段,它将被映射到该实体表的列。

继承

据推测,Code First的默认继承映射策略是表层级继承(TPH),除非您使用Fluent API设置了其他内容,但在我们的模型中,三种继承情况中只有一种被映射为TPH。

第一种继承情况,在MediaFile及其子类中,行为符合预期,为两个子类创建了一个带有鉴别器列的单个表。下图说明了这一点。

336187/inheritance-mediafile.png

第二种情况由MediaGenre及其两个子类表示。这次,Code First决定采用具体类型表(TPC)策略,正如我们在图中所见。我认为这对于这种情况来说是一个更好的解决方案,但我无法完全推断出这个决定的逻辑,尽管我猜测它与这些实体的关系有关。

336187/inheritance-mediagenre.png

最后,第三种情况不是经典情况,因为VideoTrackInfo被注释为ComplexType,其字段被映射为它正在使用的实体中的列。这一事实使得AudioTrackInfo成为唯一从继承角度考虑的实体,因此Code First只为它创建了一个表。

336187/inheritance-trackinfo.png

关系

如引言中所述,本文旨在提供一个涵盖Code First尽可能多方面的可运行实现,作为其他开发此技术项目的人员的参考。因此,我将指出我发现的关于使用Code First声明关系的最佳博客,尽管该博客是用4.1版本编写的,但它是一个很好的起点。它是一系列六篇文章,从这篇开始。

前面包含的图将有助于定位模型中定义的各种关系,一旦您找到了感兴趣的关系,您就可以参考源代码来查看实现细节。

表拆分

表拆分允许将多个实体映射到数据库中的同一张表。我们将在数据项目中利用此功能来定义图像字段作为实体,以启用它们的延迟加载。拆分后的实体是AlbumCoverVideoFilePoster

让我们来看看AlbumCover - AudioAlbumMetaInfo的例子。要对它们启用表拆分,我们必须指定两个实体将被持久化到同一张表中,并在它们之间定义一个共享主键关联,换句话说,是一对一的主键关联。主实体然后包含到将被延迟加载的实体的导航属性。

336187/class-table_splitting.png

索引

索引既不支持注释也不支持fluent API,默认情况下,唯一会创建的索引是我们主键字段的索引。如果我们想声明更多新的索引(这通常是常见情况),我们必须在数据项目中通过自定义数据库初始化程序来创建它们。

在数据项目中,我们选择覆盖DropCreateDatabaseIfModelChanges的继承类的Seed方法,从那里我们调用Database属性上的ExecuteSqlCommand,传递适当的SQL命令来创建索引。

至少,我曾期望Code First生成的Foreign Key字段能自动索引,但它们没有。希望在Entity Framework的未来版本中实现这一点,并在此主题上进行进一步的改进。

SQL Server Compact中的图像

Code First中的SQL Server Compact图像字段有一个特点,对于该数据库提供程序,它将字符串和二进制字段的最大长度设置为4000。有关更深入的解释,我建议阅读这篇帖子

由于要存储的图像大于此尺寸,因此当我们调用SaveChanges方法时,Code First会报错。我发现的最佳解决方案是利用一种变通方法,即通过覆盖我们DbContext继承类的ShouldValidateEntity方法来跳过包含此类字段的实体的验证。

对于依赖SQL Server Compact进行数据存储的实体,当这些实体包含图像字段时,将表拆分应用到这些实体将是另一个原因,因为这样我们可以配置我们的上下文只验证其中包含图像字段的拆分实体,而仍然验证父实体。另一种选择是禁用所有实体的验证,将DbContext配置字段的ValidateOnSaveEnabled属性设置为false,但这可能是一个非常极端的决定。

这是我们在DbContext继承类中实现的

protected override bool ShouldValidateEntity(DbEntityEntry entityEntry)
{
    if ((entityEntry.Entity is AlbumCover) || (entityEntry.Entity is VideoFilePoster))
        return false;
    return base.ShouldValidateEntity(entityEntry);
} 

最终注释

本文中包含的所有图像均属于以下许可证之一:Creative Commons、Freeware、GNU/GPL。

本文中的图表是使用yUML(http://yuml.me/)或Visual Studio Tools创建的。

错误的关联定义

在完成文章并回顾结果时,我意识到VideoFileMetaInfoVideoPerson之间存在一个设计错误。我会尝试花时间修复它,但现在,看看您是否能发现它。

关于SQL Server Compact 4.0的评论

也许只是我的旧PIV盒或者Entity Framework与SQL Server Compact 4.0使用Code First交互的方式,但它似乎性能非常低下。正如我们将看到的,主要问题似乎是身份键的生成,我的猜测是,由于这是SQL Server Compact支持它的第一个版本,实现还没有完全完善。

解决方案中包含了一个测试项目,该项目包含一个只有两个字段的实体。它允许我们向SQL Server Compact 4.0数据库写入和读取元素,并报告操作的经过时间,以便我们可以测试性能。在这里,我们可以看到一个执行的三个顺序写入测试和一个最终读取测试的结果。

336187/CE40PerformanceTest.png

---------------------------------------------------------
- CE40PerformanceTest
---------------------------------------------------------
- R: Read test
- W: Write test
- X: Exit

WRITE TEST: Write 1000 items
Create objects: 5 ms  |  200000 items/s
Add to DbSet: 5075 ms  |  197 items/s
Save changes: 6358 ms  |  157 items/s

WRITE TEST: Write 1000 items
Create objects: 6 ms  |  166666 items/s
Add to DbSet: 4743 ms  |  210 items/s
Save changes: 12453 ms  |  80 items/s

WRITE TEST: Write 1000 items
Create objects: 4 ms  |  250000 items/s
Add to DbSet: 8786 ms  |  113 items/s
Save changes: 18955 ms  |  52 items/s

READ TEST: Get items where Number < 50
150 items read  |  1336 ms

我们在这里观察到的是,根据数据库中记录的数量,新记录的插入速度是如何下降的。读取测试似乎表现更好,但不足以用于生产系统。

在第二次运行中,对已插入的3000个项目再次执行读取测试,耗时接近10秒。

为了比较这些结果,根据我的经验,将具有此类类别的3000个对象持久化到XML文件(例如,使用序列化)应该只需要不到一秒钟的时间,这快了许多倍。是的,我知道这个比较很棘手,因为XML测试只是一个序列化,但在这种情况下,数据库中没有定义任何关系、索引或触发器,大部分工作就是将数据写入磁盘。

针对SQL Server数据库运行测试将很有趣。我确定结果会更接近(如果不是更好)XML序列化时间,而不是SQL Server Compact 4.0的时间。

个人想法

这项技术具有巨大的潜力,尤其是能够在不离开Visual Studio IDE的情况下开发完整的项目,并且Code First实体是用POCO类定义的,这意味着我们不需要为服务序列化生成另一组类。

另一方面,我希望有更多的注释来处理模型,例如设置索引或自定义关系,而不是依赖于fluent API或SQL命令。

截至本文完成之时,Entity Framework的新版本4.3已经问世,实现了第一个迁移功能版本,允许对我们的模型进行增量更改,修复了一些错误以及其他一些更改,正如我们在MSDN上的ADO.NET博客这篇文章中读到的。

在2012年,.NET Framework 4.5版本应该会发布,其中包括Entity Framework 4.5,我认为当谈到Code First时,这将是第一个可以考虑用于生产系统的版本。

历史

  • 2012-02-23:初版
© . All rights reserved.