第11章函数
函数提供了一个有力代码复用机制, 并且让你的代码保持简洁和易懂。
它们同样也是EF运行时能利用的数据库层代码.函数有几类: Rowset Functions, 聚合函数, Ranking Functions, 和标量值函数.
函数要么确定,要么不确定。当用一些指定的值调用函数,而函数返回的结果总是一样时,它就是确定的函数。当甚至用同样的一些值调用时,而函数每次返回的结果也可能不一样,它就是不确定的函数。
在前七小节,我们探讨“模型定义”的函数,这些函数允许我们在概念层上创建。这些函数依照EF类型和你的模型实体来定义。这样使得它们能便捷地通过数据存储执行。
在剩下的小节,我们就展示如何使用被EF定义的函数和数据库层的函数.
这些函数允许你影响已有的代码,在EF运行时或更接近于你数据的数据库层.
11-1. 从“模型定义”函数返回一个标量值
问题
想在概念模型定义一个函数,接受一个实体的实例,并且返回一个标量值.
解决方案
假设已有一个如Figure 11-1.所示模型
Figure 11-1. 一个产品和分类的模型
接下来创建一个接受一个Category 实体实例并返回给定Category 里所有product的平均单价:
1. 在解决方案资源管理器中,右击 .edmx 文件,选择“打开方式” ? XML 编辑器.
2.在.edmx文件的概念模型(conceptual models)节点<Schema>标签里,插入Listing 11-1里的代码,这样就在模型里定义了函数。
Listing 11-1. Definition of the AverageUnitPrice() Function in the Model
<Function Name="AverageUnitPrice" ReturnType="Edm.Decimal">
<Parameter Name="category" Type="EFRecipesModel1101.Category" />
<DefiningExpression>
ANYELEMENT(Select VALUE Avg(p.UnitPrice) from EFRecipesEntities1101.Products as p where p.Category == category)
</DefiningExpression>
</Function>
3. 插入和查询这个模型的代码,如Listing 11-2所示.
class Program
{
static void Main(string[] args)
{
RunExample();
Console.WriteLine("\nPress any key to exit...");
Console.ReadKey();
}
static void RunExample()
{
using (var context = new EFRecipesEntities1101())
{
context.Database.ExecuteSqlCommand("delete from chapter11.product;delete from chapter11.category");
var c1 = new Category { CategoryName = "Backpacking Tents" };
var p1 = new Product
{
ProductName = "Hooligan",
UnitPrice = 89.99M,
Category = c1
};
var p2 = new Product
{
ProductName = "Kraz",
UnitPrice = 99.99M,
Category = c1
};
var p3 = new Product
{
ProductName = "Sundome",
UnitPrice = 49.99M,
Category = c1
};
context.Categories.Add(c1);
context.Products.Add(p1);
context.Products.Add(p2);
context.Products.Add(p3);
var c2 = new Category { CategoryName = "Family Tents" };
var p4 = new Product
{
ProductName = "Evanston",
UnitPrice = 169.99M,
Category = c2
};
var p5 = new Product
{
ProductName = "Montana",
UnitPrice = 149.99M,
Category = c2
};
context.Categories.Add(c2);
context.Products.Add(p4);
context.Products.Add(p5);
context.SaveChanges();
}
// with eSQL
using (var context = new EFRecipesEntities1101())
{
Console.WriteLine("Using eSQL for the query...");
Console.WriteLine();
string sql = @"Select c.CategoryName, EFRecipesModel1101
.AverageUnitPrice(c) as AveragePrice from
EFRecipesEntities1101.Categories as c";
var objectContext = (context as IObjectContextAdapter).ObjectContext;
var cats = objectContext.CreateQuery<DbDataRecord>(sql);
foreach (var cat in cats)
{
Console.WriteLine("Category ‘{0}‘ has an average price of {1}",
cat["CategoryName"], ((decimal)cat["AveragePrice"]).ToString("C"));
}
}
// with LINQ
using (var context = new EFRecipesEntities1101())
{
Console.WriteLine();
Console.WriteLine("Using LINQ for the query...");
Console.WriteLine();
var cats = from c in context.Categories
select new
{
Name = c.CategoryName,
AveragePrice = MyFunctions.AverageUnitPrice(c)
};
foreach (var cat in cats)
{
Console.WriteLine("Category ‘{0}‘ has an average price of {1}",
cat.Name, cat.AveragePrice.ToString("C"));
}
}
}
}
public class MyFunctions
{
[EdmFunction("EFRecipesModel1101", "AverageUnitPrice")]
public static decimal AverageUnitPrice(Category category)
{
throw new NotSupportedException("Direct calls are not supported!");
}
}
Listing 11-2.用 “模型定义” 的AverageUnitPrice()函数插入和查询模型
输出结果如下面的 Listing 11-2所示:
Using eSQL for the query...
Category ‘Backpacking Tents‘ has an average price of $79.99
Category ‘Family Tents‘ has an average price of $159.99
Using LINQ for the query...
Category ‘Backpacking Tents‘ has an average price of $79.99
Category ‘Family Tents‘ has an average price of $159.99
它是如何工作的?
“模型定义”的函数,在概念层创建,并且用eSQL来写. 当然, “模型定义”允许你引用你模型中的实体,就像我们这里做的这样,在函数的实现中引用了Category 实体和Product 实体以及它们之间的关系。函数带来的额外好处是:我们不会被绑定在一个指定的存储层上。把函数放在更低的层,甚至是数据库驱动, 我们的程序也可以工作.
目前的设计器不支持“模型定义”函数,不像存储过程,能被设计器支持,“模型定义”函数不会被模型浏览器显示也不会出现在设计器的其它地方。设计器也不会检查eSQL中的语法错误,只有在运行时才会报错,但至少可打开.edmx来定义。
在Listing 11-2,代码先插入两个类别(category)和各自的一些产品(product).之后用两种略微不同的方式查询这些数据。在第一个查询例子,我们创建eSQL语句来调用AverageUnitPrice() 函数.并执行查询. 在查询结果中的每行,我们取出第一列数据(category名称)和第二列数据(每个类别产品的平均单价). 并且输出。
第二个查询例子,更有趣,我们在LINQ查询中使用AverageUnitPrice()函数,不过需要先在另一个类里添加一个方法存根,方法用 [EdmFunction()] 特性装饰, 把它标记为是一个“模型定义”函数. 运行时方法不可以调用它(一旦调用,方法中就显式抛出异常). 因为我们只是返回一个标量值,所以这个方法签名比较简单(参数个数,类型,和返回值类型). 在In the LINQ 查询中query, 我们获取每个category并且把结果(category名称,调用MyFunction类里AverageUnitPrice()方法返回的结果)映射到一个匿名类. 并且输出。
DbContext是 ObjectContext轻量级的版本. 每当需要执行eSql (Entity SQL)时, 是必须使用ObjectContext 的. 因为我们要通过DbContext获取ObjectContext (使用:(context as IObjectContextAdapter) ObjectContext).
“模型定义”函数的参数可以是:标量值,实体类,复杂类型,匿名类型,或是上述类型的集合).在本章的很多小节,我们就演示如何创建和使用这些类型参数的“模型定义”函数。
“模型定义” 函数的参数没有方向性,没有“输出”参数,只有“输入”参数,原因是“模型定义” 函数只是一个”组件”,并且能成为LINQ查询的一部分。
在这个例子中,我们返回单一的标量decimal类型的值。因为Select查询结果会被理解成一个集合,所以我们需要为返回的结果显式地使用AnyElement运算符。EF不知道如何把一个集合映射成一个标量值,所以我们在这儿使用AnyElement运算符告诉它返回的结果只是一个元素。当Select结果只有一个元素的时候,我们也会运用该运算符告诉调用者它只是一个元素。
最佳实践
“模型定义”函数提供了一个纯净和有效的概念模型的组成部分.下面列几个它的最佳实践:.
>“模型定义”函数用eSQL 定义到概念层. 这使用我们可以从存储模型细节中抽象出一个更完成的模型.
>你可以把LINQ或eSQL查询中常用的表达定义成函数. 这样使代码组织结构更好并且可复用. 当然,如果使用LINQ, VS提供的智能感知和编译时检查,会让代码减少因为误输入带来的问题.
>“模型定义”函数是一个”组件”,允许你把它当成一个组成部分用在更复杂的表达式中. 这样可以合你代码更简单些,并具可维护性.
>“模型定义”函数能被用在有需要计算的地方,比如一个需要计算的属性,当实体被实例化会带来计算的消耗,不管你用没用到这个属性,而“模型定义”函数只是在你确实用到这个属性时,它才去计算这个属性值.