Kerosene ORM 动态记录深入分析
Kerosene ORM 动态记录和其他库核心概念的深入教程
引言
Kerosene ORM
是一个动态、免配置、自适应的 ORM 库,它克服了其他 ORM 解决方案中的许多限制和细微差别。它完全支持 POCO 对象,无需任何映射或配置文件,无需使用属性污染代码,也无需继承任何特定于 ORM 的基类——从而促进了关注点的清晰分离。它也不使用任何约定,因此我们可以按照自己想要的方式编写类并命名其成员以及数据库中的列和表,而不是按照别人为我们决定的方式。它让我们完全控制将要执行的 SQL 代码:只编写我们写下的,不多也不少。它遵循 Repository 和 Unit Of Work 模式,尽管以动态方式实现,这让我们获得了极大的灵活性。
所有这些特性使 Kerosene ORM
成为迭代和敏捷开发场景的理想解决方案,尤其是在涉及多个不同团队(从开发人员到 IT 和数据库负责人)的情况下。Kerosene ORM 目前正在重客户端和 Web 应用程序以及数据密集型场景中使用。
本文探讨了 Kerosene ORM
的动态记录操作模式以及库的其他核心概念。请参阅介绍性文章 Kerosene ORM 介绍性文章 以获取更多背景信息,本文在此基础上阐述了其中引入的概念。
初步概念
命名空间
Kerosene.ORM.Core
命名空间包含库主要结构和接口的定义,出于所有实际目的都应导入它。Kerosene.ORM.Direct
命名空间包含这些元素,适用于“直接连接”场景,在这种场景中,我们主要通过连接字符串连接到数据库。WPF 包将反过来提供这些元素到 WPF 场景的适配,但这超出了本文的范围,此处不作讨论。
最后,Kerosene.ORM.Maps
命名空间包含 Kerosene ORM
实体映射操作模式中使用的接口和主要结构,顺便说一句,这是最常用的模式。请参阅“Kerosene ORM 实体映射深度解析”配套文章以获取更深入的详细信息。
数据引擎
数据引擎是实现 IDataEngine
接口的对象,Kerosene ORM
使用它来表示给定数据库引擎或版本的特性。开箱即用,核心库内置支持通用 ODBC、OLEDB、ORACLE 和 SQL SERVER 引擎。
这些开箱即用的引擎本质上是其关联数据库的通用表示,但它们没有考虑任何特定的方言语法或数据库功能。为了表示这些变体,可以构建继承自这些基本引擎的自定义引擎。例如,附加的 Kerosene.ORM.SqlServer
包提供对 SQL SERVER 2008、2012 和 2014 版本的支持。其代码也可以用作模板来开发您的自定义引擎以支持您喜欢的数据库或版本。
定位和注册数据引擎
我们始终可以使用其类的构造函数创建引擎。但首选的方法是使用 EngineFactory
静态类,其 Locate()
方法将允许我们找到适合我们场景的引擎。
var engine = EngineFactory.Locate();
当不使用任何参数时,此方法将在应用程序的配置文件中查找默认连接字符串条目要使用的详细信息,该条目的名称由 <keroseneORM>
配置部分中的以下行指定
<keroseneORM>
<connectionString name="MyFavoriteDB" />
</keroseneORM>
此名称应引用一个有效的连接字符串条目,其 providerName
属性将用于查找要使用的相应引擎。例如,以下条目将用于查找适用于 SQL Server 数据库的引擎
<connectionStrings>
<add name="MyFavoriteDB"
providerName="System.Data.SqlClient"
connectionString="..." />
</connectionStrings>
如果我们在使用自己的自定义引擎,我们可以手动注册它们,以便它们可用。但最简单的方法是也将其指定在配置文件中,如下所示。例如,要注册 Kerosene.ORM.SqlServer
包提供的自定义引擎,我们只需添加以下行
<keroseneORM>
<customEngines>
<add
id="SqlServer2008"
type="Kerosene.ORM.SqlServer.v2008.Concrete.DataEngine"
assembly="Kerosene.ORM.SqlServer.dll" />
<add
id="SqlServer2012"
type="Kerosene.ORM.SqlServer.v2012.Concrete.DataEngine"
assembly="Kerosene.ORM.SqlServer.dll" />
</customEngines>
</keroseneORM>
如果我们不想使用默认条目,而是想查找特定的引擎,我们可以使用 Locate()
方法的附加参数
name
:如果不为空,它可以是要使用的 ADO.NET 提供程序的固定名称(如System.Data.SqlClient
)或其尾部(SqlClient
),甚至是配置文件中我们要使用的具体连接字符串条目的名称。
minVersion
和maxVersion
:当它们不为空时,它们的值指定要查找的引擎的最小和最大可接受版本。
validator
:如果不为空,它是一个委托,用于与候选引擎一起调用,以决定我们是否满意。它主要由库内部使用,但值得一提,以防我们有朝一日需要它。
settings
:如果不为空,它应该是一个名称-值字典,其条目将用于修改所定位引擎的属性值,这些属性的名称与字典中的任何条目匹配,因此我们不必满足于它们的默认值。
例如,要查找为 SQL Server 数据库注册的引擎,其服务器版本为“11”(2012)或更高版本,我们可以使用
var engine = EngineFactory.Locate( name = "SqlClient", minVersion = "11");
数据引擎的标准属性
引擎带有一些标准属性,这些属性维护它们所引用的底层物理数据库引擎的主要特性。例如,InvariantName
维护与其关联的 ADO.NET 提供程序的名称,ServerVersion
维护引擎构建的服务器版本,CaseSensitiveNames
维护数据库中表和列的名称是否区分大小写,SupportsNativeSkipTake
维护数据库是否支持实现跳过/获取功能的本机语法,等等。
数据类型转换器
数据引擎还维护数据类型转换器集合,当我们使用自定义类型作为命令参数或命令逻辑的一部分时,即使底层数据库不理解这些类型,这些转换器也将帮助我们处理它们。
例如,假设我们的应用程序使用自定义的 CalendarDate
类型来表示日期,并且我们正在查询命令中使用它
var cmd = link
.Where<Employee>(x => x.BirthDate >= new CalendarDate(1968, 1, 1));
使用的值将被捕获并存储在一个参数中,以便在执行命令时注入。但是,显然我们的 SQL 数据库不了解我们的自定义类型。因此,要告诉 Kerosene ORM
在这种情况下该怎么做,我们只需为该类型注册一个转换器即可
var engine = ...;
engine.AddTransformer<CalendarDate>(x => x.ToDateTime());
engine.AddTransformer<ClockTime>(x => x.ToString());
第一种变体将自定义类型转换为 ADO.NET 可以理解的 CLR 类型。第二种是一种备用方法,其中注册为自定义类型转换器的委托仅返回其值的字符串表示形式。
CalendarDate
和 ClockTime
都是支持性 Kerosene.Tools
库中定义的自定义类型。它们各自的转换器默认在 Kerosene.ORM
提供的引擎中注册。您可以根据需要决定是否使用它们。
数据链接
链接是一个实现 IDataLink
接口的对象,Kerosene ORM
使用它来表示与底层类似数据库服务的无关连接。实际上,这种连接可以使用连接字符串与常规数据库连接,在这种情况下被称为“直接”连接,或者可以使用 WPF 代理隐藏底层数据库与我们的应用程序的连接,或者与……任何符合接口的服务连接。
链接还负责代表我们打开、关闭和处置底层连接。它们还负责根据需要管理其他受管结构的生命周期。我们的应用程序无需担心所有这些细节。
创建链接
在直接连接场景中,创建链接最简单的方法是使用静态 LinkFactory
类的 Create()
方法,不带任何参数
var link = LinkFactory.Create();
在这种情况下,它将在配置文件中找到默认连接字符串条目,如前所述。我们也可以显式使用其引擎和模式参数,如下所示
using Kerosene.ORM.Direct;
var engine = ...;
var mode = NestableTransactionMode.Database;
var link = LinkFactory.Create(engine, mode);
link.ConnectionString = "...";
mode 参数指定链接将使用的事务对象的默认模式(下文将详细介绍)。另请注意,当以这种方式使用此方法时,我们必须在使用新链接之前,将其 ConnectionString
属性设置为适当的内容。此属性可用是因为此实例是“直接”实例——其他变体可能有自己的属性。
可嵌套事务
我们的链接实例的 Transaction
属性维护一个 INestableTransaction
对象实例,Kerosene ORM
用它来管理与该链接关联的可嵌套事务。
- 它们的
Start()
方法用于启动新的物理事务,或者,如果此类事务已经存在,则增加其嵌套级别。其IsActive
属性在底层物理事务处于活动状态时返回 true,其Level
属性将返回嵌套级别(0 表示不活动)。
- 同样,它们的
Commit()
方法会降低嵌套级别,并在需要时提交物理事务。
- 最后,它们的
Abort()
方法无条件地中止任何可能处于活动状态的物理事务。这意味着无论事务嵌套到哪个级别,它都将被完全取消。
事务也具有与其关联的模式(来自 NestableTransactionMode
枚举),存储在 Mode
属性中,其值可以是 Database
或 GlobalScope
。前者指示事务使用传统的面向数据库的方法,而后者使用现代的全局范围方法。
使用事务
在动态记录操作模式下,库在设计上永远不会自动启动事务。请记住,在此模式下,我们主要是在低级别操作,Kerosene ORM
不会干扰您的应用程序行为。您需要明确地启动、提交和中止它们。
相反,如果使用实体映射操作模式,那么 Kerosene ORM
将在所有挂起的更改操作针对数据库执行时将其包装在单个事务中。这样做的原因是为了遵循工作单元模式,即所有这些操作作为一个单元成功或失败。
命令参数
当 Kerosene ORM
在我们的表达式中遇到看起来像值的东西时,它将被捕获并从该表达式中提取出来,并替换为创建的用于保存该值的参数的名称。通过这种方式,默认情况下可以防止 SQL 注入攻击。方法调用的结果也被视为值并相应地捕获。
参数是一个实现 IParameter
接口的对象,Kerosene ORM
使用它来表示一个与任何特定数据库类型或结构无关的无定形参数。当捕获参数的命令执行时,Kerosene ORM
将在需要时将其值转换为底层数据库可以理解的值,并创建并注入相应的低级 ADO.NET 参数实例到命令中。
为了完整起见,IParameterCollection
接口表示参数集合。此集合保存在每个命令附带的 Parameters
属性中。除非您真正了解自己在做什么,否则建议不要随意修改它。
解析器
尽管我们的应用程序很少直接使用解析器,但值得讨论这些对象,因为它们是 Kerosene ORM
动态和自适应能力的核心。解析器是一个实现 IParser
接口的对象,Kerosene ORM
使用它来表示解析任意对象并将其转换为底层数据库可以理解的 SQL 语法的能力。
解析器与引擎密切相关,因为它们维护必须拦截哪些特定结构以适应其关联数据库的特性和功能。每个链接都带有一个 Parser
属性,该属性维护与它一起使用的适当解析器。
关于解析器的其他考虑
解析器只有一个有趣的公共方法:Parse()
方法。它的参数可以是,如前所述,任何任意对象,从空引用、对象实例的任何有效值,到动态 lambda 表达式。
var s1 = link.Parser.Parse(null);
var s2 = link.Parser.Parse(7, pars);
var s3 = link.Parser.Parse(x => x.Id == "007", pars);
var s4 = link.Parser.Parse(cmd);
第一行将解析为 NULL 或任何适合底层数据库 NULL 值的其他表示。在比较中使用时,NULL 值也可以翻译为适当的“IS NULL”或“IS NOT NULL”结构。
第二行将捕获作为其第一个参数传递的值,将其提取到存储在其作为其第二个参数传递的参数集合中的新参数中。Parse()
方法将返回创建的新参数的名称。如果未使用参数集合,则返回值的字符串表示形式。
第三行解析一个动态 lambda 表达式。解析器将它翻译成底层数据库理解的适当语法,如果需要的话,还会捕获它的参数……并且如果指定了参数集合的话。
最后,第四行接收一个命令作为其参数。在这种情况下,使用命令的文本,并且其参数(如果有)被捕获并注入到作为其参数传递的参数集合中,在一般情况下,这将是被接收命令使用的参数集合。
解析器和动态 Lambda 表达式
动态 Lambda 表达式 (DLE) 被定义为其中至少一个参数是 C# 动态参数的 lambda 表达式。Kerosene ORM
通常使用 Func<dynamic, object>
当编写 DLE 时,C# 编译器不会在编译时尝试绑定为这些动态参数指定的成员和方法。相反,此绑定将延迟到运行时,届时 Kerosene ORM
解析器将执行表达式一次以捕获这些操作和参数。此信息将反过来翻译成底层数据库期望的相应 SQL 语法。
该执行使用一些特殊技术来避免表达式依赖于任何特定数据类型。结合延迟绑定机制,这就是为什么我们可以编写像 'x => x.LastName >= "P"
' 这样的表达式,使用自然的类似 SQL 的语法比较两个类似字符串的对象。
这也是解析器拦截与其底层引擎关联的特定 SQL 结构的原因。例如,默认的基本解析器拦截 x.Not(…)
并将其翻译为 SQL 等效的 ‘(NOT …)
’,将 x.Name.As(x.Alias)
翻译为 ‘Name AS Alias
’,等等。SQL 自定义解析器拦截 x.BirthDate.Year()
并将其翻译为 ‘DATEPART(YEAR, BirthDate)
’,以及它所知道的许多其他扩展。
当给定结构未被解析器拦截时,其名称将被注入到返回的字符串中,然后其参数(如果有)也将被解析并注入——例如,这正是入门文章中讨论的 COUNT 示例发生的情况。
在哪里可以找到更多信息
动态 Lambda 表达式解析机制的内部细节是一个有趣但复杂的主题,其解释超出了本文的范围。有关更多信息,请参阅“DynamicParser:如何解析委托和动态 Lambda 表达式并将其转换为表达式树”一文。
Commands
Kerosene ORM
使用实现 ICommand
接口的对象来表示可针对底层数据库执行的不同命令。IQueryCommand
、IInsertCommand
、IDeleteCommand
、IUpdateCommand
和 IRawCommand
接口表示这些命令类型。创建这些命令最简单的方法是使用链接实例的相应方法,这些方法将确保为它们创建适当的命令。
var cmd1 = link.Query();
var cmd2 = link.Insert(...);
// etc..
命令带有 CanBeExecuted
属性,如果由于某种原因命令状态不允许其执行,则返回 false:当其语法尚未完成时就是这种情况。否则,此属性返回 true。请注意,Kerosene ORM
从不检查命令语法的有效性:它将使用您编写的任何语法(直接或通过命令方法)。
可枚举命令
可枚举命令实现 IEnumerableCommand
接口,执行时将返回数据库生成的记录。除了是可枚举实例外,它们还提供 ToList()
、ToArray()
、First()
和 Last()
方法。最后两个方法如果命令执行未生成任何记录则返回 null。
提供 Last()
方法作为回退机制,因为它将检索并丢弃所有可用记录,直到找到最后一个。如果可能,强烈建议修改命令逻辑以改用 First()
方法。
标量命令
标量命令实现 IScalarCommand
接口,执行时将返回一个整数作为结果。此整数可以是受命令执行影响的记录数,或者在一般情况下,是命令决定返回的任何值。标量命令通过调用其 Execute()
方法执行。
记录
如前所述,可枚举命令的执行将返回数据库生成的记录。Kerosene ORM
将使用实现 IRecord
接口的对象来保存其内容和元数据。
记录是动态对象,它们会自动适应内容的任何结构:无论数据库返回多少列,或者它们的类型是什么,我们都能够以统一的方式管理所有这些场景。如果我们需要了解该结构的详细信息,每个记录都带有 Schema
属性,正是为此目的,我们将在本文档后面讨论它。
重现入门文章中的讨论,我们可以使用动态和索引两种方式访问其内容
foreach (dynamic rec in cmd)
Console.WriteLine("\n- Country: {0}, Employee: {1}, {2}",
rec.Ctry.Id,
rec["Emp", "LastName"],
rec.FirstName);
- 由于我们对枚举变量使用了动态关键字,因此我们能够动态访问其内容,如“
rec.Ctry.Id
”。请注意,在这种情况下,我们指定了我们感兴趣的表和列,但如果没有歧义,我们也可以只使用列名(如“rec.FirstName
”)。
- 请注意,我们在示例中使用了表别名,但如果愿意,也可以使用表名:'
rec.Countries.Id
'。记录会跟踪使用的别名,因此这两种形式是等效的。
- 最后,动态访问内容虽然很好,但会带来一些性能损失。如果我们不想付出这个不太大的代价,我们也可以使用索引方法,在该方法中我们可以指定表名和列名,或者只指定列名。
记录在设计上不实现 INotifyPropertyChanged
接口:它们不打算用于用户界面,而基本上是数据库和我们的应用程序之间的信鸽——一种 DTO(数据传输对象)。
要生成用于这些场景的对象,最好使用 Convert 机制创建具体实例(稍后会详细介绍),或者使用实体映射操作模式。
模式
如上所述,Kerosene ORM
使用实现 ISchema
接口的对象来维护从数据库检索到的记录的结构和元数据。每个模式都包含 ISchemaEntry
条目的集合,这些条目又最终携带着与给定表中给定列关联的元数据。
它们的 TableName
、ColumnName
、IsPrimaryKeyColumn
、IsUniqueValuedColumn
和 IsReadOnlyColumn
属性维护其名称所暗示的值。这些值在每次执行命令时由 Kerosene ORM
自动获取,因此我们无需提前或在任何配置文件或映射文件中指定它们。
请注意,如果查询中只使用一个表,或者当该表被视为使用模式的场景中的“默认”表时,TableName
属性可以为 null。
除了这些标准属性之外,每个模式条目还带有一个字典,其中包含从数据库检索到的完整元数据集合,可以使用 Metadata
属性或使用 this[metadataName]
语法进行枚举。此元数据通常包括列使用的 SQL 类型、其大小或其他限制等。此信息对于实现验证规则很有用,例如。
用例
接下来的讨论将使用入门文章中提出的业务场景,此处不再重复。
查询命令
查询命令用于表示对数据库的“SELECT”操作。获取此类对象最简单的方法是使用链接实例的任何 Query()
、From()
或 Select()
方法
var cmd = link.From(x => x.Employees)...;
Query 方法
此方法仅为我们的链接(引擎)实例实例化一个适当类型的查询命令。
From 方法
From()
方法允许我们指定要从哪个表检索内容。我们可以在同一个命令中使用任意数量的 From()
方法,以便同时查询多个表。
var cmd = link
.From(x => x.Employees.As(x.Emp))
.From(x => x.Countries.As(c.Ctry))
...;
此命令到目前为止将转换为
SELECT * FROM Employees AS Emp, Countries AS Ctry ...
请注意,由于 Employees
和 Countries
表都包含 Id
列,因此 SQL 语法要求我们指定别名来消除它们之间的歧义。我们使用了附加到每个表名称的 Alias()
虚拟扩展方法来完成此操作。
也可能发生 FROM 子句引用另一个查询或命令的情况。由于我们需要为这些内容提供别名,实现它的最简单方法是使用 Kerosene ORM
的“转义语法”
var otherCmd = ...; // Any kind of command
var cmd = link
.From(x => x(otherCmd).As(x.Whatevername))
...;
我们不仅限于只使用名称或其他命令,我们还可以在 FROM 方法中使用任何有效的 SQL 代码,只要底层数据库引擎能够理解它。
Where 方法
显然,几乎所有查询命令都需要使用 WHERE 子句来过滤要检索的记录
var cmd = link
.From(x => x.Employees.As(x.Emp))
.Where(x => x.Emp.Id >= "007" || x.Emp.ParentId == null);
Where()
方法的参数是一个动态 lambda 表达式,我们可以在其中编写任何我们需要的任意逻辑。我们还可以链接多个 Where()
方法;在这种情况下,每个方法包含的逻辑默认使用 AND 运算符连接。如果您希望改用 OR 运算符,那么您可以使用 Or()
虚拟扩展方法,如下所示
var cmd = link
.From(x => x.Employees.As(x.Emp))
.Where(x => x.Emp.Id >= "007")
.Where(x => x.Or(x.ParentId == null));
Select 方法
如果我们不使用 Select()
方法,那么我们将得到 'SELECT *' 子句,这通常不被认为是最佳实践。要指定我们感兴趣的列,我们可以随意链接任意数量的 Select()
方法
var cmd = link
.Select(x => x.Emp.Id, x => x.Emp.Name)
.Select(x => c.Ctry.Id);
在此示例中,我们从两个表中选择了三列。请注意,我们也可以使用多个动态 lambda 表达式作为单个 Select()
方法的参数,或者根据需要自由混合两种方法。
如果我们 ever 需要指定我们对给定表中的所有列感兴趣,我们可以使用附加到表名或别名的 All()
虚拟扩展方法
var cmd = link
.Select(x => x.Emp.Id, x => x.Emp.Name)
.Select(x => c.Ctry.All());
Distinct 方法
是的,我们也可以使用 Distinct()
方法
var cmd = link
.From(x => x.Employees.As(x.Emp)).Select(x => x.Emp.All())
.Distinct();
被翻译成
SELECT DISTINCT Emp.* FROM Employees AS Emp
让我再次强调,我们使用方法的顺序或我们链式调用多少方法并不重要:Kerosene ORM
将注释相关内容并按正确的顺序放置它们以用于 SQL 命令。
In 和 NotIn 方法
这两个虚拟扩展方法都被解析器拦截并翻译成它们的 SQL 等价物,如下所示
var cmd = link
.From(x => x.Employees)
.Where(x => x.Id.In("007", "008", "009"));
此命令被翻译成
SELECT * FROM Employees WHERE Id IN (@0, @1, @2)
其 Parameters
集合将存储我们使用的具体值。
Join 方法
显然,我们也可以使用 join,通过 Join()
方法
var cmd = link
.From(x => x.Employees.As(x.Emp))
.Join(x => x.Countries.As(x.Ctry).On(x.Ctry.Id == x.Emp.CountryId));
作为其参数的动态 lambda 表达式同时具有 As()
和 On()
虚拟扩展方法,我们可以在其中指定连接表的别名和要使用的连接条件。本示例中的命令被翻译为
SELECT * FROM Employees AS Emp
JOIN Countries AS Ctry ON (Ctry.Id = Emp.CountryId)
我们不局限于只使用标准 JOIN 子句。如果我们要使用其任何变体,我们可以使用以下语法
var cmd = ...
.Join(x => x("LEFT JOIN").Regions.As(x.Super).On(x.Super.Id == x.Reg.ParentId));
在这种情况下,我们只需使用带有我们要使用的 JOIN 变体的转义语法,并将其添加到表名前面(即 x("LEFT JOIN")
部分)。在其字符串参数中,我们可以基本编写任何我们想要的内容,只要它对我们正在使用的 SQL 引擎有意义即可。
GroupBy 和 Having 方法
我们也可以使用 GROUP BY 和 HAVING 子句,如下所示
var cmd = ...
.GroupBy(x => x.WhatEver)
.Having(x => ...);
Having()
的参数遵循与 Where()
相同的规则。
OrderBy 方法
不出所料,我们也可以使用 OrderBy()
方法
var cmd = link...
.OrderBy(x => x.Id)
.OrderBy(x => x.Name.Desc());
其参数是动态 lambda 表达式,用于指定列名。默认情况下,顺序将是升序,但我们可以改用 Desc()
或 Descending()
虚拟扩展方法。
Top、Skip 和 Take 方法
查询命令支持 Top()
方法,其语法我们可以现在推断出来
var cmd = link...Top(3);
它们也支持 Skip()
和 Take()
方法,语法相同,但有一个需要注意的警告:许多(主要是旧的)数据库引擎没有标准化的语法来实现此功能。如果是这种情况,Kerosene ORM
仍然支持此语法,但会通过检索和丢弃记录直到达到跳过数量来模拟此功能,这可能导致非最佳性能。
实际上,引擎带有 SupportsNativeSkipTake
属性,用于查询它们是否支持此本机功能。如果其值为 true,则 Kerosene ORM
将使用适当的数据库语法。如果不是……您可以依赖模拟,或者,如果您愿意,您也可以始终改用原始命令。
CTE 表达式
对版本 7 API 中的 With()
方法的支持已移除。解决方法是使用 Raw 命令编写数据库支持的具体语法。
Insert、Delete 和 Update 命令
这三个命令的语法实际上比查询命令简单得多,我们基本上将在此处重述入门文章中包含的讨论。
要插入新记录数据库,我们可以使用
var cmd = link
.Insert(x => x.Employees)
.Columns(
x => x.Id = "007",
x => x.FirstName = "James",
x => x.LastName = "Bond",
x => x.CountryId = "uk");
Insert()
方法的参数,同样,是一个动态 lambda 表达式,它解析为我们感兴趣的表名。然后我们使用它的 Columns()
方法,该方法接受可变数量的动态 lambda 表达式,每个表达式指定受影响的列及其值,其中此值可以解析为任何有效的 SQL 语句,以及我们可以从周围的 C# 代码中获得的任何引用或值。
更新记录遵循相同的模式。我们只需要定位我们感兴趣的记录,然后指定要修改的列。
var cmd = link
.Update(x => x.Employees)
.Where(x => x.Id == "007")
.Columns(
x => x.Id = "008",
x => x.FirstName = x.FirstName + "_Surrogate");
请注意,WHERE 子句不需要只解析为一条记录,也可以解析为一组记录,如果我们要在一个操作中更新多条记录。
我们现在可以推断出如何从数据库中删除一条或多条记录了
var cmd = link
.Delete(x => x.Employees)
.Where(x => x.Id == "007");
请注意,如果我们没有使用 WHERE 子句,那么我们最终将尝试从我们正在处理的表中删除所有记录。请记住,Kerosene ORM
将我们视为成熟的开发人员,并假定我们知道自己在做什么。
原始命令
有时,我们可能希望执行一些标准命令无法完全涵盖的逻辑。例如,我们可能想使用 CTE 表达式,或者我们想调用存储过程。对于这种情况,Kerosene ORM
为我们提供了专门的“Raw”命令。让我们看一个例子
var cmd = link.Raw(
"EXEC employee_insert @FirstName = {0}, @LastName = {1}",
"James", "Bond");
我们作为“Raw”命令的参数编写的任何文本都将针对数据库执行:在此示例中,我们正在调用“employee_insert”存储过程。有两点值得注意:
- 第一点是我们可以使用标准的括号 '
{n}
' 语法指定命令参数。Kerosene ORM
将捕获这些参数以避免 SQL 注入攻击。
- 第二点是,Raw 命令既可以枚举,也可以作为标量命令执行,具体取决于我们使用的具体逻辑。因此,在第一种情况下,我们将收到与任何标准命令一样生成的记录,并获得与它们关联的元数据。
将记录转换为实体
处理 POCO 实体的标准方法是使用 Kerosene ORM
的实体映射操作模式。但在某些场景下这并非必需,例如,当这些实体没有接收类,因为它们只在应用程序的特定或独立部分中使用时。
对于这些场景,动态记录操作模式支持一种方便的机制,通过使用所谓的“转换器”,将记录转换为实体,甚至是匿名类的实例。用于执行可枚举命令的 Kerosene ORM
枚举器具有 Converter
属性,它是一个委托,其签名是 Func
var cmd = link.From(x => x.Employees);
foreach (var obj in cmd.ConvertBy(rec =>
{
dynamic r = rec;
return new { r.Id, Name = string.Format("{0}, {1}", r.LastName, r.FirstName) };
}))
Console.WriteLine("\n> Object: {0}", obj);
此示例将从数据库检索到的记录转换为 ConvertBy()
方法中定义的在线转换器中定义的匿名类型的实例。此方法是实例化新枚举器并一次性设置其转换器的便捷方式。
其他有用的功能
Kerosene ORM
独特的动态方法使其支持非传统场景,其中包括我们将在下面讨论的两个示例。
嵌套读取器
假设我们的 Country
业务类有一个名为 Employees
的属性,它是一个员工列表。您希望在枚举返回这些国家的命令时填充该列表,以及类的其他成员。
如果我们不使用实体映射操作模式(这是推荐的方法),我们可以使用嵌套转换器(或读取器),如下所示
var cmdCtry = link.From(x => x.Countries.As(x.Ctry));
foreach (Country ctry in cmdCtry.ConvertBy(recCtry =>
{
dynamic dinCtry = recCtry;
Country objCtry = new Country();
objCtry.Id = dinCtry.Id;
objCtry.Name = dinCtry.Name;
objCtry.RegionId = dinCtry.RegionId;
var cmdEmp = link.From(x => x.Employees).Where(x => x.CountryId == objCtry.Id);
foreach (Employee emp in cmdEmp.ConvertBy(recEmp =>
dynamic dinEmp = recEmp;
Employee objEmp = new Employee();
objEmp.Id = dinEmp.Id; objEmp.BirthDate = dinEmp.BirthDate;
objEmp.FirstName = dinEmp.FirstName; objEmp.LastName = dinEmp.LastName;
objEmp.ManagerId = dinEmp.ManagerId; objEmp.CountryId = dinEmp.CountryId;
return objEmp;
})) ;
return objCtry;
})) ;
唯一需要记住的注意事项是,我们的连接需要配置为支持多个同时结果集。实际上,这也是“实体映射”操作模式的推荐设置。
嵌套更改操作和数据库约束
我们也可以使用类似的方法来执行嵌套更新或更改操作。例如,修改我们某个国家的主键(这在其他 ORM 解决方案中很难实现,在级联场景中更难),我们可以编写
var raw = link.Raw();
raw.Set("ALTER TABLE Countries NOCHECK CONSTRAINT ALL"); raw.Execute();
raw.Set("ALTER TABLE Employees NOCHECK CONSTRAINT ALL"); raw.Execute();
var cmdCtry = link
.Update(x => x.Countries)
.Where(x => x.Id == "es")
.Columns(x => x.Id = "es#");
foreach(Country ctry in cmdCtry.ConvertBy(recCtry =>
{
dynamic c = recCtry; Country objCtry = new Country() {
Id = c.Id,
Name = c.Name,
RegionId = c.RegionId
};
var empCmd = link
.Update(x => x.Employees)
.Where(x => x.CountryId == "es")
.Columns(x => x.CountryId = "es#");
foreach(Employee emp in cmdEmp.ConvertBy(recEmp =>
{
dynamic e = recEmp; Employee objEmp = new Employee() {
Id = e.Id,
FirstName = e.FirstName,
LastName = e.LastName,
CountryId = e.CountryId
};
objCtry.Employees.Add(objEmp);
return objEmp;
}));
}))
Console.WriteLine("\n> Country = {0}", ctry);
raw.Set("ALTER TABLE Countries CHECK CONSTRAINT ALL"); raw.Execute();
raw.Set("ALTER TABLE Employees CHECK CONSTRAINT ALL"); raw.Execute();
基本思想是在国家记录的外部循环中对员工记录执行嵌套循环。我们使用原始命令围绕这些操作,以停用约束,然后再重新激活它们。为了节省资源,我重复使用了该原始命令实例四次,但如果您愿意,每次都可以使用全新的对象。最后,请注意,我在此示例中没有包含任何与事务相关的代码,但在生产场景中绝对需要这些代码。
还有什么?
既然您已经获得了关于“动态记录”操作模式的完整信息,您可能希望查看讨论“实体映射”操作模式的类似文章。为此,请参阅入门文章中提供的链接。