ORM 用户已经习惯于处在 .NET 和 LINQ 角度思考问题,缺忘记了特定数据库的细节——在功能和执行 SQL 效率上的差异。本文简要讨论一些在用户利用实体框架(Entity Framework,缩写EF)与 Oracle、MySQL、PostgreSQL 或是 SQLite 交互,而不是与 Microsoft SQL Server,开发应用程序过程中可能面临的一些问题。希望对那些为数据库创建EF的用户,和创建应用程序以支持与多个数据库(如SQL Server和Oracle)交互的用户有用。
我们将使用 Devart providers来实施数据库EF提供者的例子,而不是 SQL Server,因为VS已经对提供 SQL Server 提供了相关很好的支持。
模型开发
开发者工具
目前,EF支持如下三个开发方法:Database-First(数据库优先)、Model-First(模型优先)和Code-First(代码优先)。没有最好的方法,方法的选择取决于特定应用程序的细节和领域。无论如何,用户必须面对的一个情形是,Visual Studio设计阶段模型开发提供的标准功能不够方便,或是有限,或是功能只针对 SQL Server,而不支持 Oracle。鉴于此,出现了很多第三方的解决方案:
1) Devart Entity Developer免费版为开发ORM模型提供全部的设计功能。
3) LLBLGen Pro是另一个可选的 EF。
一个应用程序中使用多种数据库
如果需要开发一个可以支持多个不同数据库的应用程序,那么在EF 中有很多解决方案。在EF v1/v4,开发者除了使用XML映射方法外,没有其他方法。但随着EF v4.1 的发布,使用fluent映射变得可行。
XML映射
开发人员完全可以在模型优先(Model-First)的方法下开发。或是在某个关系型数据库上先使用数据库优先(Database-First)的方法,然后再转到模型优先(Model-First),通过改变数据库连接和DLL生成模版,重新生成模型的映射和存储过程,来改变目标数据库。必须保存数据库创建 DDL SQL 脚本和生成的元数据,以便在每个阶段获得一套CSDL、MSL、SSDL文件。CSDL 和 MSL 是相同的,而 SSDL 文件对每个数据库文件都不同。进一步,形成正确的EF连接字符串必须基于特定的提供者程序连接字符串和对每个数据库所要求的资源名称,也就是说,若想生成正确的EF连接字符串,必须提供给EF要访问何种数据库,及其相应的数据库信息。以上仅仅是这个过程的简要说明。详细过程请参考: Migrating database schema from Microsoft SQL Server to Oracle using Entity Framework 和 Preparing for multiple databases。
使用XML映射的好处:
1) 支持EF映射的所有功能
2) 当使用标准的 VS EDM 设计时,创建和编辑模型的大多数操作可以在设计器上完成
3) 如果使用其他类型的代码生成,而不是标准的实体对象代码生成,那么用户可以获得POCO对象,或是自追踪实体(self-tracking entities,STE)。
4) 通过编辑T4模版,自定义生成代码
使用XML映射的坏处:
1) 繁琐,修改复杂
2) 标准的 VS EDM 设计器缺少很多功能,不支持 XML 映射的所有功能,因此,用户必须在XML编辑器中手动修改,或是使用三方EF设计器
3) T4 代码生成模版复杂,不便于修改,特别是对于 POCO和STE
Fluent 映射
fluent 映射功能变得可用后,开发针对访问多种数据库的应用程序变得异常容易。可以手动编写全部代码;也可以使用设计器和借助代码生成模版生成代码。更多资料,针对多种数据库开发一个EF代码优先(Code-First)的应用程序,请参考 Entity Framework Code-First support for Oracle, MySQL, PostgreSQL and SQLite。
与XML映射相比,代码优先映射有如下好处:
1) 开发过程相对容易
2) 模型对象是 POCO-classes.
3) 可以使用第三方EF设计器通过fluent映射开发一个模型,如 Devart Entity Developer
使用 fluent 映射的坏处:
1) 如果不使用设计器和代码生成模版,手动完成这个过程,将需要带量代码
2) 只支持一些映射功能。例如,存储过程、已编译的查询、复杂的实体分割则不支持
3) 目前,不能使用标准的 VS EDM 设计器,通过fluent映射来开发模型。然后,用户可以通过 Entity Framework Power Tools为数据库生成 fluent 映射代码。
代码优先的行为
动态数据库删除和创建
在SQL Server的行为中,当映射到模型中相应对象的数据库对象被删除和创建时,整个数据库会被删除,之后再创建,并填充到新的表中。DatabaseExists 用于检查数据库是否存在。
这种行为并不总是最优的,这涉及权限问题。例如,在Oracle,模式(Schema)就是用户,不可能删除已经连接到数据库的这个用户,因此这个行为总是不能被执行。
注:模式(Schema)是用户使用的数据库对象的一个集合,这个集合包含了各种对象,如表、视图、序列、存储过程、同义词、索引等。模式的对象是数据库数据的逻辑结构。一个用户就是定义在数据库中的一个名字,可以连接数据库,并访问对象。模式和用户帮助数据库管理员管理数据库的安全。一个用户一般对应一个 Schema。可以简单理解为用户的 Schema 名等于用户名。例如,在 Oracle 数据库中,创建一个用户的同时为这个用户创建一个与用户名相同的 Schema,并作为该用户的缺省 Schema。如果想访问数据库中一个表,没有指明该表属于哪一个Schema,那么系统会自动在表上加上缺省的 Schema 名。比如,访问 scott 用户下的emp表“select * from emp”,其实,完整的写法为“select *from scott.emp”。一个对象的完整名称为“Schema.object”,而不是“user.object”。
这就是为什么 Oracle、MySQL、PostgreSQL 和 SQLite有三种删除行为。开发者自行选择使用哪一个:
1) Model Objects Only - 只有被映射到模型对象的表(和Oracle序列)才可以被删除。这是默认行为。
2) All Schema Objects - 所有被映射到模型对象的表(和Oracle中用于自动增长列的相应序列)都可以被从模式(schemas)和数据库删除。
3) Schema - 整个模式(或数据库被删除。如果模型包含其他模型的对象,那么这些模式(数据库)也会被删除。这个方式仅在 Oracle、MySQL 和 PostgreSQL下可用,在 SQLite 下不行。
数据库创建意味着创建表和表之间的主外键关系。对于 Oracle 数据库来说,如果把主键规定为 DatabaseGeneratedOption.Identity,那么会为这个表创建一个序列和一个插入数据时的触发器,使这个列自动增加。另外,如果选择了模式删除/创建方式,那么将会创建整个模式(数据库)。
数据库核查是否执行检查是否存在于数据库中至少有一个所需的对象。至少有一个表在 MySQL,PostgreSQL 和 SQLite 的存在,以及验证。此外甲骨文,存在的测序实现数据库存在的验证是检查数据库中是否至少有一个要求的对象。而在 MySQL, PostgreSQL, and SQLite 则验证是否至少有一个表。另外,对于 Oracle,还会验证序列是否存在。
默认模式(schema)
目前代码优先(Code-First)实施的行为是,在映射过程中,当创建所有表的名称时,会定义默认的dbo模式名称。因此,Column类会被映射到表 "dbo"."Column"。
这种行为原则上适合SQL Server,但不适合其他类型的数据库。可以通过为每个实体类显式设置模式名称来纠正,使用TableAttribute属性(对fluent映射)或.ToTable()方法。别忘了那些很多彼此关联的EdmMetadata表和中间表,如果在模型中使用了的话。在当前模式/数据库执行的所有操作,允许在表名前不生成数据库模式名。没有标准的方法;然而,从EF提供者实施的水平看,这种解决方法是可以实现的,就像Devart dotConnect。
数据类型
EF支持有限的.NET数据类型,包括signedintegers、Single、Double、Decimal、String、Byte[]、TimeSpan、DateTime、DateTimeOffset、Boolean和Guid。然而,上边所有的.NET 数据类型不能代表所有数据库。
数值(Numeric)类型
不同的数据库用不同的数据类型来表示数值类型。它们中的一些将数值类型有具体分成多种类型。而在Oracle,所有.NET数值类型都存储在NUMBER类型中。这个安排没什么特殊的问题,然而,开发者应该确保数据库数据类型的精度和范围能够足以存储相应的.NET数据类型,反之亦然。理想情况下,类型的范围是相同的。
特别地,EF不支持的.NET无符号整数的映射,因此,MySQL的无符号整数类型必须被映射到一个范围更大的有符号整数上。例如,MySQL的无符号整数被映射到System.Int64,等等。
通过数据优先(Database-First)方法创建一个模型时,一些EF提高者可以自定义规则,将数据库的数值类型映射到.NET类型。而其他EF提供者则是一套固定的规则。一般,Oracle 的EF提供商实现了这个功能。
另外,请记住,Oracle已经没有自动增长的列了。而是,这个列(标识列)作为整数列(如,NUMBER(9)),加上插入记录触发器,基于序列分配一个值给该列。
字符串类型
记住,不同数据库的字符串类型对于可以保存的最大字符串长度有不同的限制,也分成Unicode /非Unicode字符串类型。
最容易发生问题的地方是如何在Oracle中处理空字符串。数据库把空字符串"" 保存为 NULL;这个特性如何?如果这个列有NOT NULL约束,那么就会出现保存问题。如果列为nullable,那么保存操作就没问题。然而,当读取数据,期望,返回null,而不是""。应用程序逻辑必须能够正确地处理这样的特性。
麻烦的另一个来源是不同类型的字符串,或者说Unicode /非Unicode字符串,因为Oracle在这方面有一定限制。在大多数情况下,一个全功能的EF提供者其他方法生成正确的SQL代码。
日期精度
1) 开发人员应注意特定数据库DateTime类型的精度,也就是说,存储秒的小数部分的能力。System.DateTime可以存储小数点后7位的时间值,其精度为100纳秒。一些数据库无法存储这样的精度,精度的不同取决于特定的类型。所以,有时,不截取而保存整个System.DateTime的值,是不可能的。例如:
2) Oracle的 DATE 类型存储日期和时间,不存储秒的小数部分,而TIMESTAMP 类型则可以存储,直到纳秒(小数点后9位)。
3) MySQL 的DATE 类型,仅仅存储日期,而不存储时间。DATETIME 和 TIMESTAMP 类型存储日期和时间,但 TIMESTAMP 的范围有限,从 1970 到 2038,DATETIME 的范围就比较大了,另外,它们都不存储秒的小数位;秒的小数部分只存储在计算过程中,其只有微秒精度。
4) PostgreSQL 的TIMESTAMP 类型存储秒的小数为微妙,小数点后6位。
5) SQLite 的 DateTime 类型在数据库被存储为文本,这样,您可能会遇到日期比较问题,通过EF,手动,或是其他工具写入日期数据。
DateTimeOffsett支持
DataTimeOffset 是最有疑问的.NET数据类型。在MS SQLServer 、Oracle 和 SQLite中支持,而MS SQL Server 、MySQL 或 PostgreSQL则不支持,因为这些数据库没有本地 DateTime 类型,不能存储time zone offset。
并发检查
在SQL Server中,RowVersion / timestamp类型可以被用来方便地实现并发检查。当使用其他数据库时,可以用不同的方法。没有最佳的方法建立并发检查。当然可以为一个实体的所有列设置СoncurrencyMode=Fixed来检查所有值,但是这个选项是最慢的。
通常情况下,我们创建一个UPDATE触发器,把一个值分配给特定并发列。为了进行并发检查,我们可以使用一个 numeric、DateTime 或 Guid (binary) 列:
1) DateTime类型是可行的,如果这个数据库数据类型型可以存储足够精度的时间值,以确定它们之间在最小的时间间隔内做出的修改,也就是说,在不到一秒的时间内做出的修改。
2) numeric列和当列中的值是递增时的触发器。
3) Guid可以用,由服务器端创建。这在大多数现代的DBMS几乎都是可行的。
如果并发检查是一个浮点列,那么要注意精度。
一般在Oracle中,并发检查不能是ORA_ROWSCN pseudo-column,因为这些列的值只有在事务保存后才会改变。
性能
当一个特定数据库的细节没有考虑到,完全有可能,在一个数据库里处理LINQ查询很快,而在另一个数据库却很慢。
通过LINQ方法.Skip()/.Take() 执行数据分页是最消耗资源的操作之一。不同于SQL Server,Oracle中的Skip/Take会产生两个子查询,排序在内层子查询中进行,这可能会负面地影响性能。应该指出,在其他数据库中,分页是完全无效的,特别是,如果排序是在非索引列上进行执行的。
EF提供者的性能和使用SaveChanges()支持批量更新的可用性都会影响保存数据到数据库的过程。
在开发阶段,你需要控制生成用于查询和更新的SQL语句。由于没有内置机制来监测所有生成的SQL,你可以使用第三方工具。当使用Devart EF提供者时,你可以使用免费工具 dbMonitor。如果使用其他EF提供者,你可以在数据库端追踪SQL。
有很多方式来优化LINQ查询:一个LINQ查询可以被重写,可以被分成两个。通过创建存储过程/函数或是视图,把一些操作转移到服务器端的逻辑。可以写和执行本地SQL,而不用ORM功能,通过物化从服务端接受的对象集合,在服务器内存中执行操作等等,把逻辑的一部分,从LINQ to Entities移到LINQ to Objects。
LINQ to Entities 和 EntitySQL: 行为有什么区别?
计算精度
当在数值类型上进行计算或在不同数据库上使用聚合功能时,在相同的初始化下,可能得到的值略有不同。因为不同数据类型的计算精度不同。
通过下边的例子来说明。
表 TEST_TABLE,它的列COLUMN1,Oracle下为 NUMBER(15,10) 或 BINARY_DOUBLE,MySQL下为DOUBLE(15,10) 或 DECIMAL(15,10)。初始化数据: 1.0001
1.0002
1.0003
1.0004
1.0005
1.0006
1.0007
1.0008
1.0009
1.0010
查询:SELECT AVG(COLUMN1) FROM TEST_TABLE
在上边Oracle的两种情况和MySQL DECIMAL(15,10),将返回 1.00055;而在MySQL 5.1 DOUBLE(15,10),将得到1.0005500078。
查询限制
在EF,开发者可以使用LINQ to Entities或Entity SQL形式化查询,或者两者结合。无论如何,这样一个查询由EF引擎解释成一个表达式树的中间表示,发送到EF提供者来生成SQL。由于EF最初是为SQL Server的开发,因此,有时,EF 提供者为了为特定的目标数据库生成正确的SQL,不得不改变最初的表达式树,然而这并不总是可行的。
最典型的例子是,一个只在Microsoft SQL Server支持的SQL命令CROSS APPLY和OUTER APPLY。因此,在某个情况下(参考 MSDN article对这种情形的描述),EF通过这些结构转换查询。由于Oracle、MySQL、PostgreSQL和SQLite没有相应的语法结构,不能生成查询,在SQL生成过程中,就会发生错误,
一个带子查询的复杂LINQ查询在SQL Server下会执行,而在Oracle下就不行,参看 existing limitation。通常,当在复杂查询中,使用.Skip()/.Take() 方法时,这个问题就会发生。因为,不像SQL Server,Oracle的实现导致了额外生成的子查询。
Canonical functions
在LINQ to Entities查询中,调用.NET类型的属性和方法会被转换成所谓的EF Canonical Function(参考 MSDNarticle)生成SQL。Canonical functions也可以直接用在Entity SQL。
MSDN的描述如下:
“本节讨论所有的数据提供者都支持的canonical functions,所有的查询技术都使用它。”
这个描述似乎过于乐观,因为不同的DBMS功能不同,有时不能实现所有的功能。
对于DBMS,下面的可能不支持:
字符串类型:Reverse
1) DateTime类型:AddNanoseconds,AddMicroseconds, AddMilliseconds, CreateDateTimeOffset, CreateTime,CurrentDateTimeOffset, DiffNanoseconds, DiffMilliseconds, DiffMicroseconds, DiffSeconds,DiffMinutes, DiffHours, GetTotalOffsetMinutes
2) bitwise 操作:BitWiseNot
3) 其他:NewGuid
Microsoft SQL Server /、Oracle、MySQL、PostgreSQL 和 SQLite更详细的canonicalfunctions 比较,参考 Entity Framework Canonical Functions。
保存改变到数据库
不同EF提供者的实现功能也不同。因此,是否在INSERT / UPDATE / DELETE命令产生字面常量或参数,或是让用户选择一个特定类型的行为,这个问题完全是提供者开发商的责任。
对于向数据库发送NULL值,以及处理DefaultValue,行为同样不一样。在SaveChanges()处理中,批量更新功能的问题没有标准的解决方案。提供者开发商可以随意地自己解决——这就是为什么我推荐,在任何特定情况下,参考特定提供者的文档。Devart dotConnect提供者实现了很多调整DML操作行为的功能,包括批量更新。
结论
本文,我简要描述,一个在为数据库开发EF应用程序的过程中,用户面临的最常见的问题,而不是说Microsoft SQL Server。毫无疑问,许多其他方面没有在文章中天线出来。简言之,我想为开发者提供一些建议。尽管这些建议有点老生常谈,司空见惯,但无论ORM和数据库,遵守它们是明智的。
1) Attentively study documentation relatedto the EF-provider of your choice, articles published on the company's blog, aswell as community discussions on appropriate forums.
2) Test not only the functionality of yourapplication, but also its performance. Performance tests are also of importancefor ORM solutions. An exceedingly large amount of objects in a model, certainsmart architectural solutions, for instance, table-per-types inheritance (TPT),non-optimal LINQ-queries and a huge number of other factors may lead to acertain decrease in performance.