数据点

兵不血刃拒绝 Entity Framework 对表的访问

Julie Lerman

在看到实际的 Entity Framework 命令创建之后,数据库所有者的第一反应往往是:“什么?我必须提供对表的访问权?”他们有这种反应是因为 Entity Framework 的核心功能之一便是生成 SELECT、UPDATE、INSERT 和 DELETE 等命令。

在本专栏中,我将带领数据库管理员了解一下 Entity Framework 如何生成命令,然后介绍一些功能,您可以利用这些功能只允许 Entity Framework 使用视图与存储过程,从而限制 Entity Framework 对您数据库的访问。而与此同时,您不会对应用程序代码产生任何影响,也不会疏远与团队中开发人员之间的关系。

认识默认命令生成

这一命令生成过程是如何实现的?Entity Framework 的要点是实体数据模型 (EDM),一个用于描述应用程序域对象的概念模型。Entity Framework 让开发人员可以针对实体数据模型提出查询,而不必操心数据库的具体操作。实体数据模型的实体以及实体之间的关系以 XML 形式定义,而开发人员基于该模型的实体来处理强类型化类。Entity Framework 运行时使用实体数据模型的 XML 以及其他元数据(用于描述数据库架构以及从实体数据模型到数据库架构的映射关系)来沟通类与数据库(参见图 1)。

图 1 Entity Framework 运行时元数据用于生成数据库命令

在运行时,利用特定于数据库的 ADO.NET 提供程序,Entity Framework 将针对实体数据模型而创建的查询转换为存储查询(例如 T-SQL),然后送至数据库。Entity Framework 将查询结果转换为由强类型化实体类所定义的对象,如图 2 所示。

图 2 Entity Framework 执行查询并处理查询结果

在用户处理这些对象的时候,Entity Framework 利用标识键跟踪属性以及对象之间的关系所发生的更改。最后,当代码调用 Entity Framework SaveChanges 方法,从而在数据库中永久保存更改时,Entity Framework 运行时会读取自己采集的所有更改跟踪信息。对于每个修改、添加或删除的实体,Entity Framework 会再次读取模型,并让提供程序生成存储命令,然后在一次可逆事务中对数据库执行这些命令。

这一段关于 Entity Framework 默认行为的描述往往会让数据库所有者发疯,但在这里我想要强调“默认”这个词。Entity Framework 有许多可以改变的默认行为。

Entity Framework 对于数据检索请求或数据保存请求的处理方式就是这样一个可以修改的行为。您不必建立依赖于 Entity Framework 的模型便能访问您的数据表。您可以建立一个只知道数据库视图与存储过程的模型,而不影响使用该模型的应用程序代码。通过结合 Entity Framework 的存储过程支持与其数据库视图支持,您能够以存储过程和视图为基础实现所有数据库交互。

将实体映射到数据库视图,而非表

建立模型有多种方法。我将重点讨论通过对旧式数据库实施反向工程处理而建立的模型。对于这一过程,Visual Studio 提供了一个向导。

在此向导中,用户可以选择数据库表、视图和存储过程。存储过程部分还会列出可以放入模型的、用户定义的标量值函数。

通常,开发人员将会选择表,并让向导根据这些表创建实体。在之前讨论的更改跟踪与 SaveChanges 过程中,Entity Framework 自动为基于表的实体生成 INSERT、UPDATE 与 DELETE 等命令。

我们先来看看如何强制 Entity Framework 针对视图而非表来执行查询。

放入模型的数据库视图也会成为实体。Entity Framework 跟踪这些实体所发生的更改,就像它跟踪映射到表的实体那样。使用视图的时候,关于标识键有一点需要注意。数据库表可能会有一个或更多被标为表主键的列。默认情况下,向导会根据表的主键创建实体的标识键。在创建映射到视图(没有主键)的实体的时候,向导会根据表中所有不可为空的值创建一个组合键,从而尽力推断该标识键。假设有一个视图有四个不可为空的列,即ContactID、FirstName、LastName 和 TimeStamp,然后根据这个视图创建一个实体。

 所生成的四个属性将被标为 EntityKey(设计器利用键图标标示 EntityKey 属性),这表示该实体有一个由这四个属性组成的 EntityKey。

我们只需要 ContactID 这个属性来唯一标识此实体。因此,在模型创建完成之后,您可以利用设计器将其他三个属性的 EntityKey 属性更改为 False,从而只将 ContactID 设为指定的 EntityKey。

或者(如有可能),您还可以事先规划好,让设计出的数据库视图提供正确的、不可为空的列。

确定键之后,Entity Framework 可以唯一标识每个实体,因而能对这些实体执行更改跟踪,然后在调用 SaveChanges 的时候将更改永久保存到数据库中。

用您自己的存储过程取代命令生成

对于将更改永久保存到数据库中的操作,您可以覆盖默认命令生成,在需要永久保存到数据库的时候让 Entity Framework 使用您自己的 Insert、Update 和 Delete 存储过程。此举称为“存储过程映射”。让我们来看看这是如何实现的。

您在 EDM 向导(或之后在更新向导)中选定应放入模型的任何存储过程,都会变成模型 XML 元数据中用于描述数据库架构的那个部分中的一个函数。它不会自动成为概念模型的一个组成部分,在设计图面中不会有任何表示。

下面是我的某个数据库中 Person 表的一个简单的 Insert 存储过程。

ALTER procedure [dbo].[InsertPerson]
           @FirstName nchar(50),
           @LastName nchar(50),
           @Title nchar(50)
AS
INSERT INTO [Entity FrameworkWorkshop].[dbo].[Person]
           ([FirstName]
           ,[LastName]
           ,[Title]           )
     VALUES
(@FirstName,@LastName,@Title)
SELECT @@IDENTITY as PersonID

此存储过程不仅执行数据库插入操作,随后它还返回 SQL Server 为新行创建的主键值。

如果您在向导中选择此过程,那么它在模型的数据库架构中将表示为以下函数:

<Function Name="InsertPerson" Aggregate="false" BuiltIn="false"   
 NiladicFunction="false" IsComposable="false" 
 ParameterTypeSemantics="AllowImplicitConversion" Schema="dbo">
  <Parameter Name="FirstName" Type="nchar" Mode="In" />
  <Parameter Name="LastName" Type="nchar" Mode="In" />
  <Parameter Name="Title" Type="nchar" Mode="In" />
</Function>

然后您可以使用设计器的“映射详细信息”窗口将这个 InsertPerson 函数映射到根据 Person 表创建的 Person 实体,如图 3 所示。

图 3 将存储过程映射到实体

请注意,在图 3 中,PersonID 属性映射到存储过程的返回值。这一映射将会导致在数据库中执行插入操作之后,Entity Framework 便会立即利用数据库生成的键来更新内存中的 Person 对象。

映射函数时的一个重要要求是:函数中的每个参数必须映射到实体中的某个属性。不允许将某个公式或值映射到参数。然而,开发人员有许多机会自定义表示这些实体的 Microsoft .NET Framework 类。

您也可以映射 Update 函数和 Delete 函数。虽然不是必须映射所有三个操作(Insert、Update 和 Delete),但开发人员必须注意文档中所述有关只映射部分函数的某些规则。

图 3 中,请注意在属性的右侧还有两列(因为列宽的原因而缩略显示):“使用原始值”和“影响的行数”。Entity Framework 支持最优并发,而您可以使用这些属性对 Update 函数和 Delete 函数提供并发检查。有关此功能的更多信息,请参见 MSDN 文档“演练:将实体映射到存储过程(实体数据模型工具)”。

在运行时,如有用户创建了一个新的 Person 类型,并且随后触发了 SaveChanges 方法,Entity Framework 则会在元数据中看到 Insert 函数映射(基于在图 3 中定义的映射)。它将发出下列命令以执行存储过程,而不会动态生成自己的 INSERT 命令:

exec [dbo].[InsertPerson] @FirstName=N'Julie',@LastName=N'Lerman',
@Title=N'Ms.'

弥补隔阂以及避免 Entity Framework 访问表

Entity Framework 将会生成命令以永久保存基于视图的各实体所返回的数据,但视图可能无法更新。 如果视图无法更新,您可以将 Insert、Update 和 Delete 存储过程映射到实体,并能完成完整的数据检索与数据永久保存操作,而不必提供对数据库表的直接访问权限。

您可以只创建与表相匹配的数据库视图,也可以只创建用于更新表列的存储过程。 您也可以拥有更为复杂的视图,以及包含用以执行更新操作的高级逻辑的复杂存储过程。 您甚至可以将您的某些读取存储过程替换为视图,让开发人员能够针对视图创建查询,而这是存储过程无法做到的。

关于这一查询创建功能的示例是,应用程序可以针对 CustomersInPastYear 实体提出一个查询请求,从而利用客户的 LastName 属性进一步筛选视图:

from c in context.CustomersInPastYears
 where c.LastName.StartsWith("B")
 select c;

这样,在数据库中便会执行下列命令:

SELECT
[Extent1].[CustomerID] AS [CustomerID], [Extent1].[FirstName] AS [FirstName], 
[Extent1].[LastName] AS [LastName], [Extent1].[EmailAddress] AS [EmailAddress], 
[Extent1].[TimeStamp] AS [TimeStamp]
FROM (SELECT 
      [CustomersInPastYear].[CustomerID] AS [CustomerID], 
      [CustomersInPastYear].[FirstName] AS [FirstName], 
      [CustomersInPastYear].[LastName] AS [LastName], 
      [CustomersInPastYear].[EmailAddress] AS [EmailAddress], 
      [CustomersInPastYear].[TimeStamp] AS [TimeStamp]
      FROM [dbo].[CustomersInPastYear] AS [CustomersInPastYear]) AS [Extent1]
WHERE [Extent1].[LastName] LIKE N'B%'

.NET 编译器会接受针对某个存储过程(已映射到模型中)而创建的类似查询。 然而,Entity Framework 会对数据库执行该存储过程,将所有结果返回给应用程序,然后将筛选器应用于内存中由存储过程所返回的对象。 这样便有可能造成资源浪费以及对性能造成不利影响,而不为开发人员所知晓。

图 4 中是一个存储过程,它利用参与了 Customers­InPastYear 视图的相同列更新 Customer 表。 这个存储过程可以充当 CustomersInPastYear 实体的 Update 函数。

图 4 UpdateCustomerFirstNameLastNameEmail 存储过程

ALTER PROCEDURE UpdateCustomerFirstNameLastNameEmail
@FirstName nvarchar(50),
@LastName nvarchar(50),
@Email nvarchar(50),
@CustomerId int,
@TimeStamp timestamp

AS

UPDATE Customer
   SET [FirstName] = @FirstName
      ,[LastName] = @LastName
      ,[EmailAddress] = @Email
 WHERE CustomerID=@CustomerId AND TimeStamp=@TimeStamp
 
 SELECT TimeStamp 
 FROM Customer
 WHERE CustomerID=@CustomerId

现在,您可以将此存储过程映射到实体。图 5 中的映射将初始 TimeStamp 送至存储过程,然后利用“结果列绑定”捕获存储过程所返回的经过更新的 TimeStamp。

图 5 将存储过程映射到基于视图的实体

总而言之,只要模型设计得当,基于视图的实体有相应的标识键,并且函数得到正确映射,就不需要对利用 Entity Framework 实现数据访问策略的应用程序公开数据库表。数据库视图和存储过程可以为 EDM 以及 Entity Framework 提供它们要与您的数据库成功进行交互所需要的所有条件。

Julie Lerman* 是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。您可以在全球的用户组和会议中看到她对数据访问和其他 Microsoft .NET 主题的演示。Lerman 是《Programming Entity Framework》(O’Reilly Media,2009)一书的作者,该书受到广泛称赞,她的博客地址是 thedatafarm.com/blog。请关注她的 Twitter:julielerman。*

衷心感谢以下技术专家对本文的审阅:Noam Ben-AmiSrikanth Mandadi