65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (21投票s)

2014年4月2日

Apache

20分钟阅读

viewsIcon

83117

本文介绍了EntityLite及其使用方法

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 对象。您可以将实体视为数据库的非规范化投影或视图。一个实体可以映射到零或一个表,以及零或多个视图。当然,一个实体必须至少映射到一个表或视图。如果一个实体映射到一个表,则该实体是可更新的,您可以使用该实体在该表上执行 insertupdatedelete 操作。如果一个实体没有映射到任何表,则该实体是只读的,您可以查询它,但不能将其保存到数据库中。例如,实体 Product 映射到表 Products 和视图 Product_Detailed。这意味着您可以 insertupdatedelete 产品。这也意味着您可以基于 Products 表或 Product_Detailed 视图查询产品。

实体是简单的。它们没有复杂的属性,也没有集合属性。关系以外键的形式实现。实体属性映射到表列和视图列。映射表和视图的每个不同列都有一个属性。

EntityLite 没有将数据库抽象化并将其视为对象图持久化存储,而是拥抱关系模型,提供很少的抽象,并将其视为其本来的样子:一个关系型数据库。

如果实体映射到表,则其实体视图应符合以下关于主键的规则:

  • 基表的主键应存在于实体视图中。
  • 实体视图不应返回具有相同主键值的多于一行。

在 EntityLite 中,映射到实体的视图称为实体视图。它们必须遵循特定的命名约定。实体视图名称必须以实体名称开头,后跟下划线,并以投影名称结尾。例如,Product_DetailedProduct 实体的实体视图,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 代码创建所有可能的视图,但使用设计器则无法做到。

View designer

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 文件。

下图显示了生成的数据层:

Generated Data Layer

如您所见,每个实体有三个类:

  • 表示实体的 POCO 类。例如 Category
  • Repository 类。例如 CategoryRepository,它允许您查询、保存和删除类别。
  • Fields 类。例如,CategoryFields,它为每个 Category 属性提供一个常量 string 字段。

生成的 NorthwindDataService 类是数据层的入口点。它管理与数据库的连接以及事务。它允许您执行所有受支持的数据访问操作。为了使这更容易,NorthwindDataService 为每个存储库类都有一个属性。

生成数据层可能出错的地方

生成数据层时可能会出错,通常是:

  • 您使用的 ADO.NET 提供程序未正确注册。程序集必须在全局程序集缓存中,并且提供程序必须在 Machine.configDbProviderFactories 部分中注册。
  • 您使用的 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 扩展方法指定从实体基表或实体视图中检索的列。如果您不指定,则会检索所有列。您还可以进行过滤和排序。要进行过滤,您可以使用 WhereAndOr 扩展方法。要进行排序,您可以使用 OrderByOrderByDesc 扩展方法。要执行查询,您可以调用 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() 方法有一个重载,其中包括 fromRowIndextoRowIndex 参数,两者都从零开始。这些方法使用特定的数据库功能,例如 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.InOperatorLite.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

返回以下结果集:

CategoryNameProductName年份季度销售
饮料19971705.60
饮料19972878.40
饮料199731174.50
饮料199742128.50
饮料199712435.80
饮料19972228.00
饮料199732061.50
饮料199742313.25

现在,想象一下您需要在屏幕上显示这些结果并实现以下要求:

  1. 用户必须能够查看所有员工的销售额或特定员工的销售额。
  2. 用户必须能够按年份过滤。
  3. 用户必须能够按类别或按产品获取分组销售额。
  4. 您必须实现查询分页。
  5. 用户必须能够按任何显示字段对结果进行排序。

要求 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 使用 GroupingEmployeeId 模板属性动态构建 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.OutputDirection.InputOutput。您可以在命令执行后读取命令模板的属性来获取输出参数值。

写入数据库

每个 Repository 类都有强类型方法,用于在数据库上 insertupdatedelete 表行。这些方法如下:

  • Insert(EntityType entity)。向实体基表插入新行。如果表有自动生成的主键列,例如自增或 identity 列,或 ORACLE 序列驱动列,EntityLite 会在插入时设置相应的实体属性。在 ORACLE 中,序列必须命名如下:COLUMNNAME_SEQGuid 列也被视为自动生成。
  • 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();
}

这些修改方法是虚拟的,因此您可以更改特定实体的 insertupdatedelete 默认行为。如果您想全局更改所有实体的行为,可以在 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 的整数列即可启用它。
  • 自动审计字段CreatedDateModifiedDateModifiedByCreatedBy 列由 EntityLite 自动管理。审计字段名称可以通过 DataService.SpecialFieldNames 属性更改。您必须设置 DataService.CurrentUserId 才能允许 EntityLite 设置 ModifiedByCreatedBy 列。
  • 本地化。EntityLite 支持两种实现本地化的方式。最简单的方式是拥有名为 MyColumn_Lang1MyColumn_Lang2 等的列。当您访问 MyEntity.MyColumn 时,EntityLite 会根据线程的当前文化选择正确的列。这在过滤器中也有效。
  • 自动查询重试。EntityLite 会自动重试失败的查询。
  • 错误记录到 NLog
  • 查询分析。只需实现 IProfilerLite 接口并设置 ProfilerLite.Current 属性即可。我将在 Nuget 上发布一个将查询执行记录到 SQLite 数据库的实现。
  • 命名约定。EntityLite 将列名转换为 Pascal 命名约定属性名。例如,名为 PRODUCT_PRICE 的表列将映射到名为 ProductPrice 的属性。
  • 支持 ORACLE Ref Cursors 用于存储过程。
  • 支持 SQL Server 空间类型和 hierarchyId。
© . All rights reserved.