i-nercya EntityLite: 轻量级、数据库优先的微型 ORM






4.89/5 (21投票s)
本文介绍了EntityLite及其使用方法

引言
i-nercya EntityLite 是一个开源、轻量级、数据库优先的微型 ORM,用 C# 编写,用于访问关系型数据库的 C# 应用程序中。
EntityLite 的设计旨在实现以下目标:
- 简洁性。核心代码只有大约 2,600 行,可维护性指数为 85。
- 易用性和易理解性。EntityLite 提供 T4 模板来生成易于使用的数据层。使用 EntityLite 提供的流式接口,查询很容易编写。
-
高性能。生成 SQL 语句所花费的处理时间可以忽略不计。EntityLite 使用
DynamicMethod
在运行时生成高性能方法,以便从数据读取器加载实体。 - 满足应用程序查询需求。使用 EntityLite,您可以基于表、视图或表值函数构建查询,然后添加过滤和排序。存储过程也受支持。这涵盖了您大部分的查询需求。其余查询需求则由高度灵活和动态的运行时 T4 模板查询来满足。
- 发挥数据库的强大功能。关系型数据库服务器拥有强大的处理能力和功能,您可以通过使用视图、函数和存储过程来加以利用。
- 支持多种数据库系统。EntityLite 开箱即用地支持 SQL Server、Oracle、Postgre-SQL、MySQL 和 SQLite。但它也可以轻松扩展到其他关系型数据库管理系统。
背景
早在 2009 年 Entity Framework 刚刚兴起时,我们就开始使用它,但很快我们便感觉到需要一个更简单、性能更好的 ORM。于是我开始开发 EntityLite,当时它叫 ORMLite,最近我改了名字,因为有其他微型 ORM 也叫这个名字。EntityLite 从那时起发展了很多,现在我决定将其作为开源项目发布。
是的,目前有许多成熟的 ORM,如 Entity Framework、NHibernate 和 LLBLGen Pro,也有许多微型 ORM,如 PetaPoco、Massive 和 Dapper。所以 EntityLite 只是又一个微型 ORM。但我希望您能像我一样喜欢它。
尽管 EntityLite 看起来像是一个全新的微型 ORM,但它已经投入生产多年。我们开发的所有应用程序都使用它。
实体、表和视图
在 EntityLite 中,实体是映射到数据库表和视图的 POCO 对象。您可以将实体视为数据库的非规范化投影或视图。一个实体可以映射到零或一个表,以及零或多个视图。当然,一个实体必须至少映射到一个表或视图。如果一个实体映射到一个表,则该实体是可更新的,您可以使用该实体在该表上执行 insert
、update
和 delete
操作。如果一个实体没有映射到任何表,则该实体是只读的,您可以查询它,但不能将其保存到数据库中。例如,实体 Product
映射到表 Products
和视图 Product_Detailed
。这意味着您可以 insert
、update
和 delete
产品。这也意味着您可以基于 Products
表或 Product_Detailed
视图查询产品。
实体是简单的。它们没有复杂的属性,也没有集合属性。关系以外键的形式实现。实体属性映射到表列和视图列。映射表和视图的每个不同列都有一个属性。
EntityLite 没有将数据库抽象化并将其视为对象图持久化存储,而是拥抱关系模型,提供很少的抽象,并将其视为其本来的样子:一个关系型数据库。
如果实体映射到表,则其实体视图应符合以下关于主键的规则:
- 基表的主键应存在于实体视图中。
- 实体视图不应返回具有相同主键值的多于一行。
在 EntityLite 中,映射到实体的视图称为实体视图。它们必须遵循特定的命名约定。实体视图名称必须以实体名称开头,后跟下划线,并以投影名称结尾。例如,Product_Detailed
是 Product
实体的实体视图,Detailed
是投影名称。
EntityLite 鼓励使用实体视图。它们帮助您构建读取模型,提供不同的方式来查询您的实体,并允许您加载不同的属性集。例如,Northwind Traders 的一名员工需要审查产品目录,您希望在网格中显示产品目录。您可以查询 Products
表,但这还不够,因为该 employee
希望查看产品的类别名称和供应商名称。为了解决这个问题,您可以创建包含这些列的 Product_Detailed
实体视图。
CREATE VIEW [dbo].[Product_Detailed]
AS
SELECT
P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice,
P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued,
C.CategoryName, S.CompanyName AS SupplierName
FROM
[dbo].[Products] P
LEFT OUTER JOIN [dbo].[Categories] C
ON P.CategoryID = C.CategoryID
LEFT OUTER JOIN [dbo].Suppliers S
ON P.SupplierID = S.SupplierID
类似地,当您在网格中显示订单(仅标题)时,您可能希望显示订单总额、订单详情数量以及客户名称和发货人名称。为此,您可以创建以下两个视图。第一个视图只是一个辅助视图,第二个视图是一个实体视图。
CREATE VIEW [dbo].[OrderDetailsSummary]
WITH SCHEMABINDING
AS
SELECT
OD.OrderID, SUM(OD.Quantity * OD.UnitPrice * (1 - OD.Discount)) AS OrderTotal, COUNT_BIG(*) AS LineCount
FROM
[dbo].[OrderDetails] OD
GROUP BY
OD.OrderID
为了使上述视图更高效,您可以对其进行索引。
CREATE UNIQUE CLUSTERED INDEX [UK_OrderDetailsSummary_OrderID]
ON [dbo].[OrderDetailsSummary]([OrderID])
这是您的实体视图。
CREATE VIEW [dbo].[Order_Extended]
AS
SELECT
O.OrderID, O.CustomerID, O.EmployeeID, O.OrderDate, O.RequiredDate, O.ShippedDate, O.ShipVia,
O.Freight, O.ShipName, O.ShipAddress, O.ShipCity, O.ShipRegion, O.ShipPostalCode, O.ShipCountry,
C.CompanyName AS CustomerCompanyName,
E.FirstName AS EmployeeFirstName, E.LastName AS EmployeeLastName,
S.CompanyName AS ShipperCompanyName,
OS.OrderTotal, OS.LineCount
FROM
[dbo].[Orders] O
LEFT OUTER JOIN dbo.Customers C
ON O.CustomerID = C.CustomerID
LEFT OUTER JOIN dbo.Employees E
ON O.EmployeeID = E.EmployeeID
LEFT OUTER JOIN dbo.Shippers S
ON O.ShipVia = S.ShipperID
LEFT OUTER JOIN dbo.OrderDetailsSummary OS WITH (NOEXPAND)
ON O.OrderID = OS.OrderID
在开发过程中,您可能会倾向于这样做:创建一个实体视图,当您需要另一列时,修改该视图,然后,每当您需要更多列时,就将它们添加到该视图中。最终,您会得到一个“通用视图”。请不要误解,这是一个怪物,有 12 个或更多表连接,效率低下且难以维护的视图。请不要这样做,您可以为每个实体创建任意数量的视图,所以为新的用例创建一个新的视图。当某个视图访问的表是当前用例不需要时,请不要使用该视图,只需创建另一个视图即可。
视图比应用程序生成的代码有一个优势,DBA 有机会优化它们。经验丰富的数据库开发人员也可以优化视图。他们可以重写视图以使其更高效,包括查询提示和应用程序生成的代码无法实现的技巧。
大多数数据库服务器都有视图设计器,如果您是 SQL 初学者,请使用它们。它们可以帮助您创建视图。然而,视图设计器不支持所有 SQL 语言功能,您可以通过编写 SQL 代码创建所有可能的视图,但使用设计器则无法做到。

EntityLite Nuget 包
要将 EntityLite 包含在您的项目中,您需要安装 EntityLite Nuget 包。它包括 T4 代码生成器和核心库(EntityLite.Core
Nuget 包)。只需在 Package Manager Console 中输入以下内容即可安装最新的预发布版本:
PM>Install-Package EntityLite -Pre
要安装最新的稳定版本,请输入以下内容:
PM>Install-Package EntityLite
当您安装 EntityLite Nuget 包时,以下内容会添加到您的项目中:
- 对 inercya.EntityLite.dll 的引用。这是核心库。它需要 .NET 3.5 或更高版本。
- 一个名为 EntityLite.ttinclude 的文件夹。此文件夹包含多个 T4 包含文件。
- DataLayer.tt T4 模板。您可以在此处定义要生成的实体以及用于访问数据库和控制代码生成过程的多个属性。
您可能希望创建一个库类项目来放置您的数据层,以及一个引用该库类项目的 UI 项目。然后,UI 项目必须引用 EntityLite.Core
程序集。
PM>Install-Package EntityLite.Core
附加示例数据库
在生成数据层之前,您需要一个数据库。EntityLite 毕竟是一个数据库优先的微型 ORM。示例代码包含一个 SQL Server 2012 数据库,您需要附加它。它还包含 AttachDb.SQL
脚本。从 Visual Studio 或 SQL Server Management Studio 在本地 SQL Server 2012 LocalDb 或常规实例上执行该脚本以附加数据库。根据需要更改 Northwind.mdf
数据库文件路径。您还可以将数据库附加到远程 SQL Server 2012 实例。在这种情况下,您需要将数据库文件(*.mdf 和 *.ldf)复制到远程计算机,并更改连接字符串以指向远程 SQL Server 2012 实例。
CREATE DATABASE Northwind
ON (FILENAME = 'C:\Projects\EntityLite\Samples\Northwind.mdf')
FOR ATTACH;
生成数据层
要生成数据层,您需要修改 DataLayer.tt 文件。更改连接字符串并包含您想要的实体和过程。
下面是一个 DataLayer.tt 示例。它使用 SqlClient
连接到 SQL Server LocalDb
默认实例上的 Northwind 数据库。生成的类放置在 Samples.Entities
命名空间中。它生成一个名为“NorthwindDataService
”的数据服务类、多个实体以及一个用于轻松调用 RaiseProductPrices
存储过程的方法。
<#@ include file ="EntityLite.ttinclude\EntityLite.ttinclude" #>
<#
var generation = new DataLayerGeneration
{
ProviderName = "System.Data.SqlClient",
ConnectionString = @"Data Source=(LocalDb)\V11.0;Initial Catalog=Northwind",
DefaultSchema = "dbo",
RootNamespace = "Samples.Entities",
DataServiceName = "NorthwindDataService",
EntitySettings = new List<EntitySetting>
{
new EntitySetting
{
BaseTableName = "Products",
EntityName = "Product"
},
new EntitySetting
{
BaseTableName = "Categories",
EntityName = "Category"
},
/* ....................... */
new EntitySetting
{
BaseTableName = "Orders",
EntityName = "Order"
}
},
ProcedureSettings = new List<ProcedureSetting>
{
new ProcedureSetting
{
ProcedureName = "RaiseProductPrices",
ResultSetKind = ProcedureResultSetKind.None,
RelatedEntityName = "Product"
}
}
};
Render(generation);
#>
如果一切正常,当您保存 DataLayer.tt 文件或右键单击它并选择 Run Custom Tool 时,将生成 DataLayer.cs 文件。
下图显示了生成的数据层:

如您所见,每个实体有三个类:
- 表示实体的
POCO
类。例如Category
。 Repository
类。例如CategoryRepository
,它允许您查询、保存和删除类别。Fields
类。例如,CategoryFields
,它为每个Category
属性提供一个常量string
字段。
生成的 NorthwindDataService
类是数据层的入口点。它管理与数据库的连接以及事务。它允许您执行所有受支持的数据访问操作。为了使这更容易,NorthwindDataService
为每个存储库类都有一个属性。
生成数据层可能出错的地方
生成数据层时可能会出错,通常是:
- 您使用的 ADO.NET 提供程序未正确注册。程序集必须在全局程序集缓存中,并且提供程序必须在
Machine.config
的DbProviderFactories
部分中注册。 - 您使用的 ADO.NET 提供程序在 64 位下工作,但在 32 位下不工作。您需要提供程序在 32 位下工作,因为 Visual Studio 在 32 位下运行。
- 您连接数据库有问题。连接字符串可能错误,或者您可能没有连接权限。
- 数据库对象(表和存储过程)中的拼写错误。
- 某些视图和存储过程可能无效。例如,您删除了或重命名了表列,但忘记更新引用它的视图。
查询
EntityLite 中有几种执行查询的方式:
- 通过主键获取实体。这是通过使用
Repository.Get
方法完成的。 - 基于表或视图创建查询,然后添加排序和过滤。您可以通过使用
Repository.Query
方法来实现。 - 基于表值函数创建查询,然后添加排序和过滤。
FunctionQueryLite
类用于此目的。 - 基于 T4 运行时模板创建查询,然后添加排序和过滤。这是通过使用
TemplatedQueryLite
类完成的。 - 执行返回结果集的存储过程。您可以调用存储过程
Repository
自动生成的方法。 - 执行从返回结果集的 T4 运行时模板构建的查询。在这种情况下,使用
TemplatedCommand
类。
按主键获取实体
要通过主键获取实体,您可以使用存储库的 Get
方法。Get
方法将投影作为第一个参数,它可以是 inercya.EntityLite.Projection
枚举值之一(标准投影)或投影名称。
以下代码片段演示了 Get
方法的使用:
// "Norhtwind" is the application configuration file connection string name
using (var ds = new NorthwindDataService("Northwind"))
{
// reaads a category from the database by CategoryId
// SELECT * FROM dbo.Categories WHERE CategoryId = 1
Category c = ds.CategoryRepository.Get(Projection.BaseTable, 1);
// Loads the product with ProductId = 2 from the database
// SELECT CategoryName, ProductName FROM Product_Detailed WHERE ProductId = 2
Product p = ds.ProductRepository.Get(Projection.Detailed, 2, ProductFields.CategoryName, ProductFields.ProductName);
}
QueryLite 对象
要基于基表和实体视图查询实体,您可以使用存储库的 Query
方法。此方法返回一个 QueryLite
对象,并将投影作为第一个参数,它可以是 inercya.EntityLite.Projection
枚举值之一(标准投影)或投影名称。
在以下示例中,您可以看到如何使用不同的投影创建 QueryLite
对象:
using (var ds = new NorthwindDataService("Northwind"))
{
// this query is based on the dbo.Categories table
IQueryLite<Category> query1 = ds.CategoryRepository.Query(Projection.BaseTable);
// this query is based on the dbo.Product_Detailed view
IQueryLite<Product> query2 = ds.ProductRepository.Query(Projection.Detailed);
// this query is based on the dbo.ProductSale_Quarter view
IQueryLite<ProductSale> query3 = ds.ProductSaleRepository.Query("Quarter");
}
您可以使用 Fields
扩展方法指定从实体基表或实体视图中检索的列。如果您不指定,则会检索所有列。您还可以进行过滤和排序。要进行过滤,您可以使用 Where
、And
和 Or
扩展方法。要进行排序,您可以使用 OrderBy
和 OrderByDesc
扩展方法。要执行查询,您可以调用 ToEnumerable()
、ToList()
或 FirstOrDefault()
方法。以下代码片段向您展示了一个示例:
using (var ds = new NorthwindDataService("Northwind"))
{
IEnumerable<Product> products = ds.ProductRepository.Query(Projection.Detailed)
.Fields(ProductFields.CategoryName, ProductFields.ProductName)
.Where(ProductFields.Discontinued, false) // equals is the default operator
.And(ProductFields.SupplierId, OperatorLite.In, new int[] {2, 3}) // the value for OperatorLite.In is an enumerable
.And(ProductFields.UnitsInStock, OperatorLite.Greater, 0)
.OrderBy(ProductFields.CategoryName, ProductFields.ProductName)
.ToEnumerable();
foreach (Product p in products)
{
Console.WriteLine("CategoryName: {0}, ProductName: {1}", p.CategoryName, p.ProductName);
}
}
括号通过子过滤器实现。例如:
using (var ds = new NorthwindDataService("Northwind"))
{
var subFilter = new FilterLite<product>()
.Where(ProductFields.SupplierId, 1)
.Or(ProductFields.SupplierId, OperatorLite.IsNull);
// SELECT * FROM dbo.Products WHERE CategoryId = 1 AND (SupplierId = 1 OR SupplierId IS NULL)
IList<Product> products = ds.ProductRepository.Query(Projection.BaseTable)
.Where(ProductFields.CategoryId, 1)
.And(subFilter)
.ToList();
}
为了执行查询分页,ToList()
和 ToEnumerable()
方法有一个重载,其中包括 fromRowIndex
和 toRowIndex
参数,两者都从零开始。这些方法使用特定的数据库功能,例如 MySQL、SQLite 和 Postgre-SQL 的 LIMIT OFFSET
。SQL Server 使用 ROW_NUMBER()
,Oracle 使用 rownum
。
以下代码片段显示了分页的产品列表:
using (var ds = new Entities.NorthwindDataService("Northwind"))
{
const int PageSize = 10;
var query = ds.ProductRepository.Query(Projection.Detailed)
.Fields(ProductFields.CategoryName, ProductFields.ProductName)
.OrderBy(ProductFields.CategoryName, ProductFields.ProductName);
// SELECT COUNT(*) FROM ....
var productCount = query.GetCount();
var fromRowIndex = 0;
var toRowIndex = PageSize - 1;
while (fromRowIndex < productCount)
{
foreach (var product in query.ToEnumerable(fromRowIndex, toRowIndex))
{
Console.WriteLine("{0}\t{1}", product.CategoryName, product.ProductName);
}
Console.WriteLine("Press enter to view the next product page ...");
Console.ReadLine();
fromRowIndex = toRowIndex + 1;
toRowIndex += PageSize;
}
}
EntityLite 对子查询有部分支持。您可以将 QueryLite
对象作为 OperatorLite.In
和 OperatorLite.NotIn
运算符的值参数,如下例所示:
using (var ds = new NorthwindDataService("Northwind"))
{
IQueryLite<OrderDetail> orderDetailSubQuery = ds.OrderDetailRepository.Query(Projection.BaseTable)
.Fields(FieldsOption.None, OrderDetailFields.OrderId)
.Where(OrderDetailFields.ProductId, 11);
// SELECT OrderId, OrderDate, CustomerId
// FROM dbo.Orders
// WHERE OrderId IN (
// SELECT OrderId
// FROM dbo.OrderDetails
// WHERE ProductId = 11
// )
IQueryLite<Order> orderQuery = ds.OrderRepository.Query(Projection.BaseTable)
.Fields(OrderFields.OrderId, OrderFields.OrderDate, OrderFields.CustomerId)
.Where(OrderFields.OrderId, OperatorLite.In, orderDetailSubQuery);
foreach(var order in orderQuery.ToEnumerable())
{
Console.WriteLine("OrderId {0}, OrderDate {1}, CustomerId {2}",
order.OrderId, order.OrderDate, order.CustomerId);
}
}
表值函数
EntityLite 支持 SQL Server 表值函数。要使用它们,您需要创建一个 FunctionLite
对象。FunctionLite
实现了 IQueryLite
接口,因此您可以像往常一样指定列并添加过滤和排序。查询分页也可用。
给定以下使用递归 CTE 返回员工子树的内联表值函数:
CREATE FUNCTION GetEmployeeSubTree(@EmployeeId int)
RETURNS TABLE
AS
RETURN
WITH H
AS
(
SELECT E.EmployeeID, E.LastName, E.FirstName, E.ReportsTo, E.City
FROM
[dbo].[Employees] E
WHERE
E.EmployeeID = @EmployeeId
UNION ALL
SELECT E.EmployeeID, E.LastName, E.FirstName, E.ReportsTo, E.City
FROM
[dbo].[Employees] E
INNER JOIN H ON E.ReportsTo = H.EmployeeID
)
SELECT * FROM H
您可以使用以下代码片段获取直接或间接向 Andrew Fuller 先生汇报的所有伦敦员工:
using (var ds = new NorthwindDataService("Northwind"))
{
// Andrew Fuller EmployeeId is 2
// SELECT FirstName, LastName
// FROM GetEmployeeSubTree(2)
// WHERE City = 'London'
// ORDER BY FirstName, LastName
IQueryLite<Employee> query = new FunctionQueryLite<Employee>(ds, "dbo.GetEmployeeSubTree", 2)
.Fields(EmployeeFields.FirstName, EmployeeFields.LastName)
.Where(EmployeeFields.City, "London")
.OrderBy(EmployeeFields.FirstName, EmployeeFields.LastName);
foreach(var emp in query.ToEnumerable())
{
Console.WriteLine("FirstName: {0}, LastName: {1}", emp.FirstName, emp.LastName);
}
}
您可能希望将 FunctionQueryLite
对象的创建包含在 EmployeeRepository
类中,以实现同质化和重用。这很容易做到,因为所有生成的类都是 partial 类。下面是一个示例:
public partial class EmployeeRepository
{
public IQueryLite<Employee> EmployeeSubtreeQuery(int employeeId)
{
return new FunctionQueryLite<Employee>(this.DataService, "dbo.GetEmployeeSubTree", employeeId);
}
}
基于模板的查询
EntityLite 提供了一种高度动态和灵活的方式来执行基于运行时文本模板的查询。有关运行时文本模板的简要介绍,请参阅预处理的 T4 模板。
基于模板的查询由 TemplatedQueryLite
类实现。为了理解它是如何工作的,让我们从以下实体视图开始,该视图按季度返回所有产品销售额。
CREATE VIEW [dbo].[ProductSale_Quarter]
AS
SELECT
P.CategoryID, C.CategoryName, P.ProductID, P.ProductName,
DATEPART(year, O.OrderDate) AS [Year],
DATEPART(quarter, O.OrderDate) AS [Quarter],
SUM(OD.Quantity * OD.UnitPrice * (1 - OD.Discount)) AS Sales
FROM
dbo.Products P
LEFT OUTER JOIN dbo.Categories C
ON P.CategoryID = C.CategoryID
LEFT OUTER JOIN
(
dbo.Orders O
INNER JOIN dbo.OrderDetails OD
ON O.OrderID = OD.OrderID
) ON P.ProductID = OD.ProductID
GROUP BY
P.CategoryID, C.CategoryName, P.ProductID, P.ProductName,
DATEPART(year, O.OrderDate),
DATEPART(quarter, O.OrderDate)
GO
以下查询:
SELECT CategoryName, ProductName, Year, Quarter, Sales
FROM dbo.ProductSale_Quarter
WHERE
ProductID IN (1, 2)
AND Year = 1997
ORDER BY
CategoryName, ProductName, Year, Quarter
返回以下结果集:
CategoryName | ProductName | 年份 | 季度 | 销售 |
---|---|---|---|---|
饮料 | 柴 | 1997 | 1 | 705.60 |
饮料 | 柴 | 1997 | 2 | 878.40 |
饮料 | 柴 | 1997 | 3 | 1174.50 |
饮料 | 柴 | 1997 | 4 | 2128.50 |
饮料 | 常 | 1997 | 1 | 2435.80 |
饮料 | 常 | 1997 | 2 | 228.00 |
饮料 | 常 | 1997 | 3 | 2061.50 |
饮料 | 常 | 1997 | 4 | 2313.25 |
现在,想象一下您需要在屏幕上显示这些结果并实现以下要求:
- 用户必须能够查看所有员工的销售额或特定员工的销售额。
- 用户必须能够按年份过滤。
- 用户必须能够按类别或按产品获取分组销售额。
- 您必须实现查询分页。
- 用户必须能够按任何显示字段对结果进行排序。
要求 2、4 和 5 可以使用基于 ProductSale_Quarter
实体视图的 QueryLite
对象轻松实现。但对于要求 1,您需要一个内联表值函数,因为 EmployeeId
列不在 ProductSale_Quarter
视图中。要实现要求 3,您需要两个内联表值函数:一个按产品分组,另一个按类别分组。问题是您的数据库可能不支持表值函数,并且应该有比使用两个表值函数更好的方法(在更极端的情况下,您可能需要更多的表值函数)。这就是 TemplatedQueryLite
发挥作用的地方。
TemplatedQueryLite
可以做内联表值函数能做的事情,甚至更多,但使用起来稍微复杂一些。当您需要动态 SQL 生成或无法使用基于表或视图的 QueryLite
对象构建查询时,TemplatedQueryLite
非常有用。
要创建基于运行时模板的查询以实现上述要求,第一步是创建查询模板。向您的项目添加一个新的“运行时文本模板”项(VS 2010 中的预处理文本模板项),命名为 SalesQueryTemplate.tt,内容如下:
<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
SELECT
P.CategoryID, C.CategoryName,
<# if (Grouping == "Product") { #>
P.ProductID, P.ProductName,
<# } else { #>
NULL AS ProductID, NULL AS ProductName,
<# } #>
DATEPART(year, O.OrderDate) AS [Year],
DATEPART(quarter, O.OrderDate) AS [Quarter],
SUM(OD.Quantity * OD.UnitPrice * (1 - OD.Discount)) AS Sales
FROM
dbo.Products P
LEFT OUTER JOIN dbo.Categories C
ON P.CategoryID = C.CategoryID
LEFT OUTER JOIN
(
dbo.Orders O
INNER JOIN dbo.OrderDetails OD
ON O.OrderID = OD.OrderID
) ON P.ProductID = OD.ProductID
<# if (EmployeeId.HasValue) { #>
WHERE
O.EmployeeID = $(EmployeeId)
<# } #>
GROUP BY
P.CategoryID, C.CategoryName,
<# if (Grouping == "Product") { #>
P.ProductID, P.ProductName,
<# } #>
DATEPART(year, O.OrderDate),
DATEPART(quarter, O.OrderDate)
请注意,SalesQueryTemplate.tt 类似于 ProductSale_Quarter
视图。但 SalesQueryTemplate.tt 使用 Grouping
和 EmployeeId
模板属性动态构建 SQL 语句以实现要求 1 和 3。EmployeeId
也是一个查询参数。EntityLite 对查询模板参数使用特殊符号。参数用括号括起来,前面加上 $ 符号,例如示例中的 $(EmployeeId)
。此特殊符号在运行时会被当前 ADO.NET 提供程序使用的正确参数符号替换。
查询模板必须只生成一个完整的 SELECT
语句。对于动态的、多语句的、不只是 SELECT 的查询,您可以使用 TemplatedCommand
对象。
第二步是扩展从 SalesQueryTemplate.tt
运行时模板生成的 SalesQueryTemplate
部分类。查询模板类必须实现 ISqlTemplate
接口并定义模板属性。作为查询参数的模板属性必须用 DbParameter
属性修饰。例如,您可以添加一个名为 SalesQueryTemplate.partial.cs 的文件来扩展 SalesQueryTemplate
类,其内容如下(为简洁起见,省略了命名空间):
public partial class SalesQueryTemplate : ISqlTemplate
{
public string Grouping { get; set; }
[DbParameter(DbType= DbType.Int32)]
public int? EmployeeId { get; set; }
}
第三步是使用创建并返回 TemplatedQueryLite
对象的方法扩展存储库类。例如:
public partial class ProductSaleRepository
{
public IQueryLite<ProductSale> TemplatedQuery(string grouping, int? employeeId)
{
var template = new SalesQueryTemplate
{
EmployeeId = employeeId,
Grouping = grouping
};
return new TemplatedQueryLite<ProductSale>(this.DataService, template);
}
}
TemplatedQueryLite
实现了 IQueryLite
,因此您可以像往常一样添加过滤和排序。您还可以轻松执行查询分页。以下示例显示了 Andrew Fuller 在 1997 年的前 10 笔销售额,按产品分组,并按类别、产品、年份和季度排序。
using (var ds = new NorthwindDataService("Northwind"))
{
var salesQuery = ds.ProductSaleRepository
.TemplatedQuery("Product", 2)
.Where(ProductSaleFields.Year, 1997)
.OrderBy(ProductSaleFields.CategoryName, ProductSaleFields.ProductName)
.OrderBy(ProductSaleFields.Year, ProductSaleFields.Quarter);
foreach(var s in salesQuery.ToEnumerable(0, 9))
{
Console.WriteLine("{0}, {1}, {2}, {3}, {4}",
s.CategoryName, s.ProductName, s.Year, s.Quarter, s.Sales);
}
}
存储过程
EntityLite 支持 SQL Server、Oracle、MySQL 和 Postgre-SQL 的存储过程。以下存储过程包含在示例代码中:
CREATE PROCEDURE [dbo].[RaiseProductPrices] @rate numeric(5,4)
AS
UPDATE dbo.Products
SET UnitPrice = UnitPrice * (1 + @rate);
要为存储过程生成方法,您需要在 DataLayer.tt 文件中包含一个 StoredProcedureSetting
对象。该方法放置在 RelatedEntity
存储库类中。在示例 DataLayer.tt 中,ProcedureName
为“RaiseProductPrices
”,RelatedEntity
为“Product
”,因此在 ProductRepository
类中生成一个名为 RaiseProductPrices
的方法。该方法的签名如下:
public void RaiseProductPrices(Decimal? rate)
方法的返回类型由 StoredProcedureSetting
对象的 ResultSetKind
属性确定。它可以是以下值之一:
ProcedureResultSetKind.None
。过程不返回任何结果集。方法返回类型为void
。ProcedureResultSetKind.Scalar
。结果集仅包含一行和一列。方法的返回类型是StoredProcedureSetting
对象的ScalarReturnType
属性的值。ProcedureResultSetKind.SingleRow
。结果集仅包含一行和多列。方法的返回类型是相关的实体 POCO 类。ProcedureResultSetKind.MultipleRows
。结果集包含多行和多列。方法返回相关实体列表。
不支持多个结果集。
生成的方法为每个存储过程参数都有一个参数。IN/OUT 参数声明为 ref
参数。
调用存储过程就像调用方法一样简单。例如:
using (var ds = new NorthwindDataService("Northwind"))
{
ds.ProductRepository.RaiseProductPrices(0.10m);
}
基于模板的命令
EntityLite 提供了一种高度动态且灵活的方式,可根据运行时 T4 模板执行多语句 SQL 命令。这是由 TemplatedCommand
类实现的。基于模板的命令与基于模板的查询相似。但基于模板的命令可以包含非 SELECT 语句,它们可以包含任意数量的有效 SQL 语句。缺点是 TemplatedCommand
不实现 IQueryLite
,因此您无法添加额外的过滤和排序,并且查询分页也不那么容易。
基于模板的命令主要用于执行基于集合修改的多语句 SQL 命令。例如,您可以使用它们执行 Transact-SQL 批处理和 PL/SQL 匿名块。它们是存储过程的动态客户端替代方案。如果您使用的数据库服务器不支持存储过程,请不用担心,您可以使用基于模板的命令做同样的事情。如果您有一个使用服务器端动态 SQL 的存储过程,基于模板的命令可能是一个更好的替代方案。
以下是 RaiseProductPricesTemplate.tt 示例文件的内容,它在 Visual Studio 术语中是一个“运行时文本模板”,在 EntityLite 术语中是一个命令模板。
<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
UPDATE <#= SchemaPrefix #>Products
SET UnitPrice = UnitPrice * (1 + $(Rate))
<# if (CategoryId.HasValue) { #>
WHERE CategoryId = $(CategoryId)
<# } #>
生成的命令模板 RaiseProductPricesTemplate
部分类必须以与查询模板相同的方式进行扩展。它必须实现 ISqlTemplate
接口并定义模板属性。作为命令参数的模板属性必须用 DbParameter
属性修饰。
public partial class RaiseProductPricesTemplate : ISqlTemplate
{
public string DefaultSchema { get; set; }
public string SchemaPrefix
{
get { return string.IsNullOrEmpty(DefaultSchema) ? string.Empty : DefaultSchema + "."; }
}
[DbParameter(DbType= DbType.Int32)]
public int? CategoryId { get; set; }
[DbParameter(DbType = DbType.Decimal, Precision=5, Scale=4)]
public decimal Rate { get; set; }
}
存储库类 ProductRepository
应扩展一个执行基于模板的命令的方法,如下所示:
public partial class ProductRepository
{
public int RaiseProductPrices(int? categoryId, decimal rate)
{
var template = new RaiseProductPricesTemplate
{
CategoryId = categoryId,
DefaultSchema = this.DataService.EntityLiteProvider.DefaultSchema,
Rate = rate
};
var cmd = new TemplatedCommand(this.DataService, template);
return cmd.ExecuteNonQuery();
}
}
TemplatedCommand
对象具有以下执行方法:
ExecuteNonQuery()
ExecuteScalar()
FirstOrDefault<T>()
ToEnumerable<T>()
ToList<T>()
ExecuteReader()
基于模板的命令支持输出参数。要包含输出参数,您需要在命令模板部分类中添加一个与参数同名的属性。该属性必须用 DbParameter
属性修饰,并指定 Direction.Output
或 Direction.InputOutput
。您可以在命令执行后读取命令模板的属性来获取输出参数值。
写入数据库
每个 Repository
类都有强类型方法,用于在数据库上 insert
、update
和 delete
表行。这些方法如下:
Insert(EntityType entity)
。向实体基表插入新行。如果表有自动生成的主键列,例如自增或 identity 列,或 ORACLE 序列驱动列,EntityLite 会在插入时设置相应的实体属性。在 ORACLE 中,序列必须命名如下:COLUMNNAME_SEQ
。Guid
列也被视为自动生成。Update(EntityType entity)
。更新实体基表上对应的行。此方法有一个额外的重载,您可以在其中指定要更新的列。Save(EntityType entity)
。保存实体。这是一个方便的方法,如果行是新的则插入,如果不是则更新。它仅在表具有自动生成的主键列时有效。如果映射到自动生成列的属性等于零,则该行被认为是新的。Delete(EntityType entity)
。删除实体基表上对应的行。此方法有一个重载,将主键作为参数而不是实体。
以下代码片段展示了一个插入、更新和删除产品的示例:
using (var ds = new Entities.NorthwindDataService("Northwind"))
{
ds.BeginTransaction();
var p = new Entities.Product
{
CategoryId = 2,
ProductName = "New Product",
QuantityPerUnit = "2",
ReorderLevel = 50,
SupplierId = 2,
UnitPrice = 10,
UnitsInStock = 1,
UnitsOnOrder = 0
};
// inserts the new product
ds.ProductRepository.Save(p);
Console.WriteLine("Inserted product id:" + p.ProductId);
p.ProductName = "Another Name";
// updates the product
ds.ProductRepository.Save(p);
// Retrieves the product from the database and shows the product category name
p = ds.ProductRepository.Get(Projection.Detailed, p.ProductId);
Console.WriteLine("CategoryName:" + p.CategoryName);
// deletes the product
ds.ProductRepository.Delete(p.ProductId);
ds.Commit();
}
这些修改方法是虚拟的,因此您可以更改特定实体的 insert
、update
或 delete
默认行为。如果您想全局更改所有实体的行为,可以在 DataService
类上重写这些方法。
EntityLite 支持事务。为此,DataService
类有 BeginTransaction()
、Commit()
和 Rollback()
方法。也支持嵌套事务,您可以多次调用 BeginTransaction()
而不调用 Commit()
或 Rollback()
。如果您调用 BeginTransaction
三次,您必须调用 Commit
三次才能真正提交事务。如果您调用 Rollback
,无论之前调用了多少次 BeginTransaction()
,事务都将回滚。
性能
EntityLite 非常快,与它的微型 ORM 兄弟一样快。Frans Bouma (LLBLGen Pro 的创建者) 不久前写了这篇博客文章,比较了几个 ORM 的获取性能。它不包括 EntityLite,因为 EntityLite 是新的,目前很少有人使用。但是我 fork 了基准测试代码所在的 github 仓库,并将 EntityLite 包含在基准测试中。结果在这里。
其他特性
我认为这篇文章足够长了,感谢您阅读到这里。但是,在结束之前,我不想不列举 EntityLite 的其他一些功能:
- 更新时的乐观并发。只需向您的表添加一个名为
EntityRowVersion
的整数列即可启用它。 - 自动审计字段。
CreatedDate
、ModifiedDate
、ModifiedBy
和CreatedBy
列由 EntityLite 自动管理。审计字段名称可以通过DataService.SpecialFieldNames
属性更改。您必须设置DataService.CurrentUserId
才能允许 EntityLite 设置ModifiedBy
和CreatedBy
列。 - 本地化。EntityLite 支持两种实现本地化的方式。最简单的方式是拥有名为
MyColumn_Lang1
、MyColumn_Lang2
等的列。当您访问MyEntity.MyColumn
时,EntityLite 会根据线程的当前文化选择正确的列。这在过滤器中也有效。 - 自动查询重试。EntityLite 会自动重试失败的查询。
- 错误记录到 NLog。
- 查询分析。只需实现
IProfilerLite
接口并设置ProfilerLite.Current
属性即可。我将在 Nuget 上发布一个将查询执行记录到 SQLite 数据库的实现。 - 命名约定。EntityLite 将列名转换为 Pascal 命名约定属性名。例如,名为
PRODUCT_PRICE
的表列将映射到名为ProductPrice
的属性。 - 支持 ORACLE Ref Cursors 用于存储过程。
- 支持 SQL Server 空间类型和 hierarchyId。