Entity Framework走马观花之把握全局
Entity Framework走马观花 之
=========================================
这是一个系列文章
上一篇《Entity Framework技术导游系列开篇与热身 》
=========================================
在深入学习某项技术之前,应该努力形成对此技术的总体印象,并了解其基本原理,本文的目的就在于此。
一、理解EF数据模型EF本质上是一个ORM框架,它需要把对象映射到底层数据库中的表,为此,它使用了三个模型来描述这种映射关系。
(1)概念模型(Conceptual Model):主要体现为一组可以被应用程序直接使用的类。这些类也是我们在程序中直接使用的类,通常称之为“实体(Entity)”
(2)存储模型(Storage Model):主要体现为一组与底层数据存储介质(比如数据库系统)直接对应的类。
(3)概念-存储模型映射(Conceptual- Storage Mapping),解决“概念模型”中的类如何与“存储模型”中的类相互对应的问题。
(2)和(3)中的类型由EF内部使用,在实际开发中通常触及不到。
所有这三种模型都集中放在名为edmx文件中,以XML方式表达。
VisualStudio提供了一个向导,完成从现有数据库到EF数据模型间的映射转换工作:
当此向导完成之后,EF成功地在数据库与程序中使用的对象之间建立了以下对应关系:
关系数据库的世界
数据库应用程序的世界
数据库
DbContext类
表
DbContext中的DbSet<实体类名>
表间的关联
实体类之间的关联
表中的字段
实体类的公有属性
表中的单条记录
单个实体类的对象
视图
DbContext中的DbSet<视图名称>
存储过程
DbContext中的公有方法
可以在Visual Studio 的模型浏览面板中看到EF数据模型的概念模型和存储模型,如果直接以xml格式打开.edmx文件,可以看到“原汁原味”的全部三大数据模型。
Visual Studio提供的EF向导不仅生成了上述三大数据模型,还使用T4代码模板(其文件扩展名为.tt)直接生成了相应实体类,还有一个派生自DbContext的子类,在其中包容了所有实体集合属性和导入的存储过程等数据库元素。这一DbContext子类是我们使用EF开发程序的核心类型。
在解决方案资源管理器中双击“edmx”文件将打开EF设计器,这是一个很强悍的工具,几乎所有的调整数据映射关系的工作都可以使用它来完成。
如果基于Database First或Model First方式开发,那么EF设计器是天天要打交道的东西。其操作方式很简单:右击设计器,从弹出菜单中选择相应命令。
EF设计器提供的各种命令和具体使用方法有很多资料介绍,在此就不废话了。
二、使用EF访问数据库的基本方式EF中可以使用以下四种方式访问数据库:
Entity SQL是专门为EF设计的一种查询语言,非常类似于通用的关系型数据库查询语言SQL,但它返回的数据都是在EF“概念模型”中所定义的,而非数据库中数据的“真实模样”。
LINQ to Entities可以看成是LINQ to Objects的一个“变种”,通过LINQ来查询EF数据模型。它在底层使用“对象服务(Object services)”来完成其功能。对象服务是一组用于查询实体数据模型的类,它可以将这些查询结果转换为强类型的CLR对象。
不管是使用Entity SQL还是LINQ to Entities,最终都是依赖“Entity Client”来完成其工作的。
Entity Client包容一组类,比如EntityConnection、EntityDataReader等,与ADO.NET对象模型非常类似,其功能也类似。Entity Client会将对数据的CRUD请求转发给ADO.NET数据提供者(ADO.NET Provider)组件,由其将相关SQL命令直接地发送给数据库。
在实际开发中,大家都使用LINQ to Entities和针对IEnumerable<T>/IQueryable<T>的一组扩展方法完成数据查询工作,几乎不会有人直接使用Entity SQL和 Entity Client。
下图展示了数应用程序运行过程中EF查询的内部处理流程:
可以看到,使用EF的数据查询从发出到真正执行要经过两次“命令树(Command Tree)转换”,而从数据库中读取的数据,也要经历从DbDataReader-->EntityDataReader-->IEnmerable<T>的转换。虽然EF采用了缓存、预编译等手段提升性能,但与ADO.NET相比,由于中间处理环节更多,整个查询处理流程中参与的对象也更多,因此总体性能一般比不上ADO.NET,并且会占用更多的内存,这也我们是在享用EF带来的方便的同时,所需要付出的代价。
另外,由于所有查询最终还是要转换为SQL命令,因此,有些LINQ to Objects可用的扩展方法,比如Last(),在EF中将不能用,因为EF不知道如何把它们翻译成底层数据库支持的SQL命令。
EF支持三种开发模式:
Code First、Database First和Model First。
到底应该用那一种模式是令人纠结的问题。
方式一:Code First对于初次接触的人,EF的Code First实在很有点魔幻色彩。下面就让我们来体会一下。
创建两个类:Book(书)和BookReview(书评)。一本书可以有多条书评,因此,它们是一对多的关系:
public class Book
{
public virtual int Id {get;set;}
public virtual string Name { get; set; }
public virtual List<BookReview> Reviews { get; set; }
}
public class BookReview
{
public int Id{get;set;}
public int BookId { get; set; }
public virtual string Content { get; set; }
public virtual Book AssoicationWithBook { get; set; }
}
好了,现在创建一个派生自DbContext的子类:
public class BookDb : DbContext
{
public DbSet<Book> Books { get; set; }
public DbSet<BookReview> Reviews { get; set; }
}
现在可以在程序中随意写几行代码从数据库中提取数据:
static void Main(string[] args)
{
using (var context = new BookDb())
{
Console.WriteLine("数据库中有{0}本书",context.Books.Count());
}
}
运行一下,如果计算机上安装有SqlExpress,那么或者是在应用程序文件夹,或者是打开SQL Server Management Studio(SSME)查看本机SQLServer,你就会发现,数据库己经创建好,其中的表及表的关联也帮助你完成了:
貌似我什么也没干,一切就OK了,神奇啊!
现在修改Book类,给它添加一个Authors属性,代表书的作者:
public class Book
{
public virtual int Id {get;set;}
public virtual string Name { get; set; }
public virtual string Authors { get; set; }
public virtual List<BookReview> Reviews { get; set; }
}
有了前面良好的第一印象,你一定以为只要再次运行程序,底层数据库就会自动更新,然而,EF会给你当头一棒让你清醒:
很明显,因为你修改了实体类,数据库结构也需要修改,比较郁闷的是,你不能打开创建好的数据库直接修改,而需要使用一个名为“数据库迁移(Database Migration)”的功能,采用两种方式(自动迁移和手动迁移)之一完成。
以自动迁移为例:
首先从从Tools菜单中打开Package Manager Console,然后键入:
enable-migrations –EnableAutomaticMigrations
上述命令会在项目中添加一个Migrations文件夹,其中会有一个Configuration类,为了方便,你需要在其构造函数中添加“AutomaticMigrationDataLossAllowed = true;”一句,让其自动重建数据库时不理会可能的数据丢失:
internal sealed class Configuration :DbMigrationsConfiguration<EFCodeFirst.BookDb>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
……
}
好了,现在运行update-database命令更新数据库:
再次运行程序,现在将一切OK。
以后每次更改实体类,都必须手动运行update-database命令更新数据库。
手动迁移方式与自动迁移基本一致,不同之处在于它会记录每次更新的情况,从而允许回滚数据库到某个“较老”的版本。
自动方式比较适合于单个人写的应用。而手动方式可以控制数据库结构的更新,比较适合于团队开发。
Code First模式的优点。从上面的介绍,可以看到Code First有着突出的优点。
(1)是代码清洁,添加自定义逻辑容易。
这是使用Code First最大的好处
特别是在诸如WPF这种可以保持长连接的桌面应用中,可以让实体类实现INotifyProperty接口,将它们放入ObservableCollection中作为UI界面的绑定数据源,便可以充分利用WPF的数据绑定和EF自动维持实体状态的特点,大幅度地削减代码。
(2)易于实现继承
Code First在实现继承上非常方便,如果数据实体中有大量的继承的情况,使用Code First很方便,但需要注意的是Code First在生成数据库表时,默认使用“TPH:Table Per Hierarchy”方式,把父类子类塞到同一张表中,并在表中添加一个Discriminator字段,表明此记录所属的具体类型。
以下是CodeFirst为拥有继承关系的两个Parent/Child类生成的数据库表,可以看到,Discriminator字段保存了具体的数据类型。
有些朋友问能否把Discriminator字段名给改掉,很遗憾,我没发现可以修改它的方法。
很明显,Code First采用的这种实现策略违反了关系数据库设计范式,对大项目来说,这不易于维护数据的一致性。
实现继承的另外一种方式是TPT(table pertype ),不管子类父类,每个类型一张表。
比如有三个类,Instruct和Student派生自Person,若采用TPT策略,EF将生成三个表,并创建以下的关联:
对于TPH,手写SQL代码很容易,性能高,但对于TPT,EF在生成SQL命令时会产生许多Inner Joint,性能低,另外,对于这种方式存储的数据,手写SQL代码比较麻烦。
Database First和Model First默认情况下都是采用TPH的。
我的建议:除非两实体间确实是IS_A关系,并且在中间层需要使用多态,在“数据存取层(DAL)”尽量少用继承,别自找麻烦。
(3)一些开发高手们还为EF提供了一个EntityFramework Power Tools,这一工具增强了Code First的不少功能,比如它可以从现有数据库直接逆向生成实体类代码,之后就可以修改这些代码,为Code First方式进行开发省去了不少编码工作。同时,它还能为编写的实体类代码生成只读的数据模型视图,以图形的方式展示出实体类间的关联。
Code First存在的问题CodeFirst试图“用代码搞掂一切”,其问题在于以下几点:
(1)当Model改变时,往往需要编写代码实现数据库的更改,远不如直接使用数据库所提供的工具修改数据库安全和直观,至少丢失数据的可能性小了很多。
(2)当数据实体间有复杂的关联时,需要使用Fluent API手动编写不少代码定义类之间的关联,这实在麻烦。
(3)对数据库表和关联属性的一些微调(比如改改字段名字,修改字段长度限制等),使用数据库设计工具能轻易实现,但Code First只能通过代码来完成,而且必须使用EF的数据迁移特性,这实在麻烦,整个过程还容易出错。
简而言之,Code First应用的场景是:快速开发,迅速迭代。
方式二:Database First这是EF从1.0开始就支持的特性,其思路是:先设计并建好数据库,然后使用Visual Studio的向导创建EF数据模型并生成实体类代码。
这是最成熟稳定的方式,其设计器相当地完善,基本上能满足实际开发中的各种需求。
我个人认为这是开发正式项目最合适的方式。
当然,DatabaseFirst也有一些问题,主要是需要定制时会有些麻烦。比如:
(1)要想给实体类或生成的DbContext子类添加一些自定义的逻辑,需应用分部类,因为每次更新数据模型,这些代码都会被设计器覆盖并重写。
(2)生成的实体类中不包括任何Data Annotation(所谓“Data Annotation”就是附在实体类代码上的诸如[Required]之类的东东),因此它需要被转换为另一个类,才能方便地在诸如ASP.NET MVC之类的项目中使用(比如ASP.NET MVC项目中的视图模型(ViewModel)类往往需要有Data Annotation,以配合jQuery Validation插件生成网页上的数据验证代码)。
(3)默认情况下实体类与edmx文件放在同一个项目中,想将实体类分离到独立的项目,需要完成一些额外的配置工作(主要是把T4模板文件移到另一个项目,这需要适当地修改T4模板文件中的代码以保证文件路径引用正确)。
方式三:Model First这种模式是先在可视化设计器中创建实体和它们间的关联,然后设计器生成SQL命令并保存于一个SQL文件中,通过执行这一SQL文件完成数据库的创建和修改工作。
我个人感觉:
这种方式最适合于全新开发的项目,从系统分析开始,逐步分析建立和完善领域模型,之后可以立即创建数据库,如果模型有修改,重新生成一个SQL文件,再执行一次即可,非常适合于新项目OOAD阶段的需要。
当进入OOP阶段时,由于数据库己经存在,就可以很方便地转用Database First方式,整个过程流畅自然。
三种模式PK结果:Code First:对于小的或用于试验的项目,特别是像我经常要讲课的,教学实例使用Code First开发就比较合适,当程序运行时数据库自动生成,比较省事。
DB First:最为成熟,是正规项目的首选方式,因为是由开发者自己(而不是通过一堆“不太可靠”的代码)直接操作数据库,整个过程高度可控,能很好地保证数据安全,贯彻了“数据比代码重要”的理念。
Model First:当开始一个全新的项目,既没有DB,也没有代码时则非常好,是OOAD的好工具,需要时甚至可以直接生成创建各种不同类型数据库(比如MySQL)的SQL代码!
==============================================================
本篇文章介绍了一些EF相关的通用话题,下一篇文章将讨论一下EF实现CRUD的内部原理。