Kerosene ORM 映射深入分析
深入探讨 Kerosene ORM 实体映射的操作模式,该模式为 POCO 对象提供完整的真实支持。
引言
Kerosene ORM
是一个动态的、无需配置的、自适应的 ORM 库,它克服了其他 ORM 解决方案中存在的许多限制和细微差别。它完全支持 POCO 对象,而无需任何映射或配置文件,不会用属性污染其代码,也无需它们继承任何特定于 ORM 的基类——从而促进了关注点的清晰分离。它也不使用任何约定,因此我们可以编写我们的类,并根据我们的意愿命名它们的成员以及数据库中的列和表,而不是别人为我们决定的方式。它让我们重新完全控制将执行的 SQL 代码:只执行我们编写的代码,不多不少。它遵循 Repository 和 Unit Of Work 模式,尽管以动态方式实现,这使我们能够获得极大的灵活性。
所有这些特性使得Kerosene ORM
成为迭代和敏捷开发场景的理想解决方案,甚至在涉及开发人员、IT 和数据库负责人等多个不同团队的场景中更是如此。
本文探讨了Kerosene ORM
的实体映射操作模式。请参阅入门文章Kerosene ORM 入门文章以获取更多上下文信息,因为本文是在其引入的概念上进行阐述的。
业务场景
在接下来的讨论中,我们将基本沿用入门文章中使用的业务场景。请记住,我们正在处理一个由三张表组成的极简人力资源系统:
Regions
表维护公司使用的区域层级结构,其ParentId
列维护当前区域所属的父区域,如果它是最顶层区域则为 null。Countries
表维护公司开展业务的国家,其RegionId
列包含该国家所属的区域。最后,Employees
表维护我们的员工,包含CountryId
列(不能为 null)和ManagerId
列,如果后者不为 null,则标识当前员工的经理。
初步概念
如入门文章所述,Kerosene ORM
实体映射操作模式负责将从数据库获取的记录动态映射到我们的业务级 POCO 类的实例,并跟踪其内容、状态和依赖关系。请记住,Kerosene ORM
不要求我们编写外部配置或映射文件,不使用任何预设约定,不使用属性污染我们的类,也不需要它们继承任何特定于 ORM 的基类。
更重要的是,Kerosene ORM
默认会尝试找出要使用的表以及如何在其列和 POCO 类的成员之间执行映射。只有当我们想要使用非标准表或列名,或者想要定义导航属性和依赖关系时,我们才需要以自定义映射的形式向Kerosene ORM
提供一些额外信息。
唯一需要注意的一点是,我们的 POCO 类型必须是类,而不是结构体或任何其他类型的对象。
存储库
存储库是一个实现IDataRepository
接口的对象,Kerosene ORM
使用它来维护底层数据库的状态和内容的视图,已显式或隐式注册到其中的映射,以对应表和 POCO 类,并访问托管实体的缓存。存储库还实现了 Repository 和 Unit Of Work 模式。
获取存储库最简单的方法是使用静态RepositoryFactory
类的Create()
方法,如下所示:
using Kerosene.ORM.Maps;
...
var link = ...;
var repo = RepositoryFactory.Create(link);
此处传递给该方法的链接对象可以通过入门文章中讨论的任何机制获取。请记住,该对象将管理与底层数据库的物理连接,根据需要打开、关闭和处置它——我们的应用程序无需为这些细节烦恼。
简单(表)映射
一旦我们获得了存储库实例,就可以直接使用它,无需任何额外的繁琐步骤。假设我们已经将Region
POCO 类作为我们感兴趣的数据库列的表示形式:
public class Region
{
public string Id { get; set; }
public string Name { get; set; }
public string ParentId { get; set; }
}
现在,要检索数据库中的区域列表,我们只需编写:
var cmd = repo.Query();
var list = cmd.ToList();
我们没有编写任何配置或映射文件。我们没有编写任何映射。我们没有用任何属性或 ORM 相关的东西污染我们的领域级代码。实际上,我们甚至没有告诉Kerosene ORM
要用于这些实体的表的名称。
默认情况下,Kerosene ORM
映射以简单(或表)模式运行,在此模式下,POCO 类中的成员会自动映射到名称与这些成员名称匹配的列。如果数据库中这些名称不区分大小写,Kerosene ORM
将遵循数据库规则,并且在查找这些匹配项时不会强制区分大小写。如果未找到匹配项,则不考虑相应的成员或列。
在这种情况下,当 POCO 类的类型第一次使用时,Kerosene ORM
将根据该类型的名称,通过一些有根据的猜测,尝试在数据库中找到合适的表。如果找到这样的表,那么将为我们创建一个“弱”映射,使用该类型成员与该表中发现的列之间的直接对应规则。
这些映射被称为“弱”映射,因为如果我们为该类型注册了一个显式映射,并且该弱映射已注册到存储库中,那么它将被丢弃。原因是给定类型在给定存储库中只能注册一次。
我们还可以使用任何符合我们正在解决的领域问题的逻辑来过滤要检索的实体:
var cmd = repo.Where(x => x.Id == "007");
var emp = cmd.First();
请注意,我们不受限于使用预设的FindXXX()
方法集,而是可以使用任何我们需要的逻辑。正如我们将在下面看到的,Kerosene ORM
在其查询命令中提供了广泛的方法,以支持非常规查询场景,例如,即使在这个 POCO 世界中,我们也可以同时查询或连接多个表。
要将新实体持久化到数据库中,我们只需使用Insert()
方法,并传入受影响的实体:
var emp = new Employee() { Id = "007", CountryId = "uk" };
...
var cmd = repo.Insert(emp);
cmd.Submit();
...
repo.ExecuteChanges();
Kerosene ORM
使用工作单元模式,因此我们必须创建与实体关联的命令,提交该命令,并在完成所有可能需要的更改(包括其他更改操作)之后,将它们作为一个单元执行。
我们现在可以更新我们的实体:
emp.FirstName = "James";
emp.LastName = "Bond";
...
repo.UpdateNow(emp);
Kerosene ORM
将自动找出实体自上次从数据库获取内容以来发生的变化,并创建一个只包含这些变化的更新命令。在此示例中,我们使用UpdateNow()
方法执行了更新操作:它基本上是一种方便的方式,可以在一次调用中提交并执行该命令以及任何其他已提交的命令。还有类似的InsertNow()
和DeleteNow()
方法可用。
最后,我们可以使用以下方法删除我们的实体:
repo.Delete(obj).Submit();
repo.ExecuteChanges();
或只是:
repo.DeleteNow(obj);
如果出于任何原因,我们对已注释到存储库中的更改不满意,我们可以使用存储库的DiscardChanges()
方法全部丢弃它们:
repo.DiscardChanges();
调用此方法时,所有待处理操作都会被丢弃。
只读列
Kerosene ORM
能够识别数据库中的只读列。作为安全网,即使它们在给定映射中使用,也不会被持久化回数据库,无论其对应成员在我们的领域模型中是如何使用的。此信息是从数据库中获取的,Kerosene ORM
不提供任何机制来规避它。
实体构造函数
其他 ORM 解决方案要求我们的业务类有一个无参数构造函数——Kerosene ORM
则不需要。请记住,其理念是不对我们开发和设计 POCO 类的方式施加任何限制。
如果我们的 POCO 类有这样一个构造函数,它将被捕获并出于性能原因使用。如果没有,那么Kerosene ORM
将在内存中创建一个未初始化的对象,而不调用任何构造函数,但至少我们将有一个可用的实例。
创建自定义映射。
当我们想要使用的表名是Kerosene ORM
无法自动识别的,或者我们的 POCO 类包含不应被映射的成员,或者这些成员的内容需要通过查询数据库、执行复杂计算甚至访问外部系统来获取,又或者我们在 POCO 类中使用导航属性并希望考虑这些依赖关系时,我们就需要创建自定义映射。
推荐的方法是创建一个继承自DataMap<T>
public class RegionMap : DataMap<Region>
{
public RegionMap(DataRepository repo) : base(repo, x => x.Regions)
{ ... }
}
DataMap<T>
构造函数接受两个参数。第一个是新映射实例将注册到的存储库,RepositoryFactory
类返回的对象可以安全地转换为DataRepository
实例。第二个是一个动态 lambda 表达式,解析为数据库中主表的名称。
鉴别器
我们的表可能用于维护我们将映射到不同 POCO 类型的记录。例如,假设我们的领域中有Employee
类,但也有Director
类来表示那些没有关联经理的员工。我们可以使用映射的Discriminator
属性来表达这种情况:
public class Director : Employee { ... }
public class DirectorMap : DataMap<Director>
{
public DirectorMap(DataRepository repo) : base(repo, x => x.Employees)
{
Discriminator = x => x.ManagerId == null;
...
}
}
此属性的签名为Func<dynamic, object>
,如果它不为空,则在需要时将被解析并注入到 WHERE 子句中。
行版本
如果我们的表有一列用于跟踪行版本,我们可以通过使用其VersionColumn
属性告诉映射在执行更新或删除操作时考虑此列:
VersionColumn.SetName(x => x.MyVersionColumnName);
即使我们的 POCO 类中没有对应的成员,Kerosene ORM
也会跟踪从数据库检索到的最新值,并在执行这些操作之前将其与最新值进行比较。如果值已更改,则会抛出ChangedException
异常。
请注意,我们无需指定该列所维护值的类型。默认情况下,Kerosene ORM
将使用与类型无关的标准化字符串表示形式来执行这些比较。如果出于任何原因您希望修改这些值的比较方式,您可以设置VersionColumn
的ValueToString
属性,它是一个委托,接受表示值的对象并返回一个字符串表示形式:
VersionColumn.ValueToString = x => x.ToString();
请注意,修改此属性并非必要,因为在几乎所有可能的情况下,默认机制都足够了。
排除列
我们的 POCO 类可能有一个成员的名称与数据库中的列名匹配,但出于某种原因,我们不希望该列-成员组合参与到映射中。很简单,我们只需要通过在映射的Columns
集合中添加一个条目,并指定该列需要被排除,来告知映射:
Columns.Add(x => x.MyColumn).SetExcluded(true);
急切和惰性成员
急切(Eager)和惰性(Lazy)成员是指那些其内容不是从主表获取,而是通过查询数据库、执行复杂计算甚至(如果需要)访问外部系统来获取的成员。它们也用于表达依赖关系。
例如,假设我们的Country
类有一个BusinessValue
成员,我们希望在从数据库加载记录实体时填充其内容(是的,我们也可以使用其 getter,但让我以此为例继续)。这可能涉及相当复杂的运算,查询其他数据库,或者访问外部系统。我们只需要告诉映射如何在完成该值时进行操作,如下所示:
Members.Add(x => x.BusinessValue)
.OnComplete((rec, obj) => {
obj.BusinessValue = ...;
});
我们使用了映射的Members
集合的Add()
方法,为我们希望映射在需要时完成的成员添加了一个新条目。其OnComplete()
方法是一个委托,它接受两个参数:第一个是从主表获得的最后一条记录(我们可以用它来获取对我们可能相关的某些列的值),第二个是对宿主实体本身的引用。
如果BusinessValue
成员是一个至少具有可访问的 getter 或 setter 的虚拟属性,那么该成员被称为“惰性”成员。如果它是一个字段或非虚拟属性,那么它被称为“急切”成员。急切成员的内容在从数据库获取主记录后立即填充,而惰性成员的内容仅在其 getter 被使用时才填充。
一般情况下,惰性成员优先于急切成员。原因是,在使用依赖项时,急切成员会尝试将这些依赖项加载到内存中,这可能会级联并加载完整的对象图,并在这些操作完成时经历延迟。另一方面,我们可能无法访问 POCO 类的源代码(例如,当它定义在我们不允许修改的外部程序集中时),对于这些场景,急切成员非常方便。
无论如何,我们可以根据需要随意混合简单、惰性和急切成员。
依赖项和导航属性
现在我们假设我们希望在 POCO 类中使用依赖项和导航属性。我们可以像这样编写我们的Region
类:
public class Region
{
public string Id { get; set; }
public string Name { get; set; }
virtual public Region Parent { get; set; }
virtual public List Childs { get; private set; }
virtual public List Countries { get; private set; }
}
我们这里使用的是虚拟属性,因此是惰性成员,但我们也可以使用急切成员,而无需更改以下讨论。
Countries
属性将维护属于我们区域的国家列表:它是一个“子”依赖项。我们可以这样将这个事实告诉映射:
Members.Add(x => x.Countries)
.OnComplete((rec, obj) =>
{
obj.Countries.Clear();
obj.Countries.AddRange(
Repository.Where(x => x.RegionId == obj.Id).ToList());
})
.SetDependencyMode(MemberDependencyMode.Child);
在我们已经熟悉的OnComplete()
方法中,我们指示映射首先清除该列表,出于健全性考虑,然后查询数据库以获取其最新内容。由于我们在映射的构造函数中定义此依赖项,为了简单起见,我们可以使用其Repository
属性。最后,我们将依赖项模式设置为“子”。
我们的Region
POCO 类还有一个Parent
属性,它是对当前区域的父区域的引用。我们可以这样定义这个依赖项:
Members.Add(x => x.Parent)
.WithColumn(x => x.ParentId, col =>
{
col.OnWriteRecord(
obj => { return obj.Parent == null ? null : obj.Parent.Id; });
})
.OnComplete((rec, obj) =>
{
obj.Parent = Repository.FindNow(x => x.Id == rec["ParentId"]);
})
.SetDependencyMode(MemberDependencyMode.Parent);
主要区别在于我们希望确保ParentId
列被映射考虑在内,因为它在数据库中用于引用父记录。为此,我们使用WithColumn()
方法,它接受两个参数:第一个是解析为该列名称的动态 lambda 表达式,第二个是一个委托,它接受添加到内部集合的新列,并允许我们自定义其行为。
在我们的例子中,我们不需要告诉映射如何读取该列的值,因为它会使用其名称自动完成(并且该值将存储在与每个实体关联的元数据中维护的记录中)。我们只需要告诉映射在需要时如何将该列的值持久化回去:我们使用OnWriteRecord()
方法,它必须返回要写回数据库的值。在示例中,如果没有使用父引用,返回 null 就足够了,否则返回其Id
属性的值。
还要注意我们是如何编写其OnComplete()
方法的:出于性能考虑,我们没有查询数据库,而是使用了FindNow()
方法,该方法将尝试在内存缓存中查找有效的实体,并且只有在缓存中找不到时才去数据库中查找。如果缓存或数据库中未找到任何实体,此方法将返回 null,这正是我们在本例中所需的。最后,我们将其依赖模式设置为“父”。
级联依赖和聚合根
只有那些模式设置为“子”或“父”的依赖项,在执行更改操作(插入、删除或更新)时才会级联。例如,在这种情况下,Kerosene ORM
会插入尚未持久化到数据库的父依赖项,或者确保在删除其父实体之前删除子依赖项。
实际上,这个功能让我们能够自然地使用 C# 代码处理聚合根,而无需用 ORM 操作污染我们的领域级代码。例如:
var root = repo.Query.Where(...).First();
...
root.Countries.RemoveAt(0);
root.Countries.Add(new Country() { Id = "ZZZ" });
...
repo.UpdateNow(root);
在此示例中,我们正在领域级别上移除和添加Countries
属性中的条目。只有稍后,当我们完成了所有需要的更改后,我们只需将宿主实体持久化回去,Kerosene ORM
就会找出它所经历的更改,并将这些更改具体化到数据库中。
Kerosene ORM
会向每个托管实体注入一个元数据包,除其他目的外,该元数据包用于跟踪实体可能发生的状态和变化。此机制的内部原理将在本文档后面讨论。当给定依赖项是类似集合的依赖项时,此元数据将保留该集合的原始内容,并且在适当时,Kerosene ORM
会将原始内容与当前内容进行比较。如果它不是类似集合的依赖项,则使用当前实体的状态来决定如何进行。
高级概念
本节将进一步探讨实体映射操作模式,讨论Kerosene ORM
的一些高级概念和内部原理。为此,我们将稍微扩展我们的业务场景,增加两个额外的表:Talents
(维护我们的 HR 朋友感兴趣的才能)和EmployeeTalents
(基本上是员工与分配给他们的才能之间的连接表):
非常规查询
假设我们的Talent
POCO 类如下所示:
public class Talent
{
public string Id { get; set; }
public string Description { get; set; }
virtual public List<Employee> Employees { get; }
...
}
这里我们有一个Employees
属性,它是一个列表(由类构造函数设置),我们希望填充它:我们需要在连接表中找到所有Id
与人才Id
相关联的员工。在这种情况下,我们可以定义如下依赖关系:
Members.Add(x => x.Employees)
.OnComplete((rec, obj) => {
obj.Employees.Clear();
obj.Employees.AddRange(
Repository
.Where(x => x.Emp.Id == x.Temp.EmployeeId)
.MasterAlias(x => x.Emp)
.From(x =>
x(Repository.Where(y => y.TalentId == obj.Id))
.As(x.Temp))
.ToList()
);
})
.SetDependencyMode(MemberDependencyMode.Parent);
这里值得注意的是,我们首先从EmployeeTalents
表中查询那些人才Id
与我们人才宿主实体的Id
匹配的记录,然后通过将这些结果注入主查询的 FROM 子句中来查找相关的员工。
由于我们同时使用了多个表,并且它们都有各自的Id
列,我们需要提供别名来消除歧义:对于内部查询,这很容易,因为我们可以使用As()
虚拟扩展方法,但是...我们如何在主映射查询中做到这一点呢?
答案是MasterAlias()
方法,它的参数是一个动态 lambda 表达式,解析为仅用于此查询的主表的别名。主表的名称是什么并不重要(请记住Kerosene ORM
可能已经自动找到了它):它的别名将是我们指定的别名。
有了这两个别名,Emp
和Temp
,现在编写我们需要查找感兴趣员工的主 WHERE 子句就变得非常容易了。请花一分钟回顾示例中的代码和上述解释,因为它听起来比实际复杂。其他 ORM 解决方案采取不同的方法,并尝试自动化此场景(其中一些在解决时遇到困难),但我们将受限于它们的假设和规则。
这种独特的Kerosene ORM
方法是一个高级特性,你可以选择使用或不使用,但它赋予了你更大的权力——事实上,即使在这个 POCO 世界中,Kerosene ORM 也支持Join()
、GroupBy()
和Having()
方法,以及我们可能需要引入来解决业务问题的几乎任何逻辑。是的,你需要编写一些类似 SQL 的代码,但它也让我们有机会优化代码,而不是使用自动生成的臃肿代码。
地图验证
我们现在知道,当创建映射实例时,它将自动注册到其构造函数中使用的存储库中。我们可以在派生类的构造函数中对其进行自定义(这是推荐的方法),或者直接使用其方法和属性。在自定义过程中,它将保持未验证状态。
现在,一旦映射用于任何有意义的目的,它将被验证:其结构和规则将对照数据库进行检查,如果发现任何不一致之处,则会抛出相应的异常。一旦映射经过验证,它就会被锁定,并且不能再进行任何自定义。
如前所述,这种验证会在需要时自动进行。可能由于某种原因,您希望锁定并验证您的映射——在这种情况下,您可以调用其Validate()
方法来执行此操作。顺便说一句,如果映射已经验证,此方法可以根据需要调用任意多次,而不会产生任何副作用。无论如何,映射的IsValidated
属性将返回 true(如果已验证),否则返回 false。
存储库映射集合
每个存储库都有一个Maps
属性,维护注册到其中的映射集合。您可以使用此集合,或多个GetMap()
方法重载,来查找与给定 POCO 类型关联的映射。
请注意,映射的注册基于 POCO 类型与注册映射所管理的实体类型之间的一对一对应关系。因此,为Employee
POCO 类注册的映射和为Manager
POCO 类注册的映射,即使后者继承自前者,也被认为是不同的映射。
当一个映射被处置时,它会从其存储库中移除。类似地,存储库有ClearMaps()
方法,它将移除并处置所有注册到其中的映射。
最后,存储库还具有RetrieveMap()
方法,该方法将返回给定类型的已注册映射,如果未注册任何映射,则会为该类型创建一个新映射。如果未使用任何参数,Kerosene ORM
将使用一系列经验法则和复数规则建议主表的名称,并且返回的映射将是“弱”映射。
提供此方法是为了以防我们不想创建自定义映射类,而是希望通过其属性和方法自定义一个实例。在这种情况下,建议相应地使用其IsWeakMap
和IsValidated
属性。
其Table
属性维护映射关联的主表的名称,无论是我们指定的名称还是 Kerosene ORM 自动找到的名称。
标识列
本质上,Kerosene ORM
不要求主表中存在主键列。它只需要一种唯一标识要与给定实体关联的记录的方法,为此,如果未定义主键列,它将尝试查找唯一值列。如果表中既不存在主键列也不存在唯一值列,则Kerosene ORM
在验证映射时将抛出异常。
请注意,我们无需标识哪些是这些标识列:Kerosene ORM
将自动从数据库的元数据中找出它们。
托管实体和元数据
Kerosene ORM
不要求我们用任何属性装饰我们的 POCO 类,它们也无需继承任何 ORM 特定的类。相反,它会向它管理的每个 POCO 实例注入一个元数据包,用于跟踪其状态、从数据库读取或持久化的最新记录以及其依赖项的状态等信息。
此包是一个实现IMetaEntity
接口的对象,可以使用静态EntityFactory
类的Locate()
方法获取:
var obj = ...;
var meta = EntityFactory.Locate(obj);
请注意,如果用作参数的对象不是类,此方法将抛出异常。值类型、枚举或结构体不被视为有效的Kerosene ORM
实体。
此元数据对象只有两个公共属性:第一个是Entity
,它是指向元数据所关联实体的引用;第二个是State
,它是一个枚举,返回此底层实体的状态。
Entity
属性的值也可以为 null。这种情况可能发生在我们获取了元数据引用之后,如果底层实体不再使用,它可能已被 CLR 垃圾收集器回收。实际上,为了避免锁定内存中的实体,元数据包只持有对它们的弱引用。
State
属性的值可以是Detached
(如果我们刚刚创建了实体且Kerosene ORM
尚未使用它),Collected
(如果底层实体已被 CLR 回收),Ready
(如果它已从数据库读取或持久化到数据库),或者ToInsert
、ToUpdate
或ToDelete
(如果已提交相应的待处理操作)。
出于性能原因,Kerosene ORM
没有维护任何类型的列表或类似结构,而是将该元数据包以运行时属性的形式注入到与其管理的任何对象相关联的 CLR 描述符中。是的,IMetaData
实例内部继承自Attribute
类。
实体收集器
Kerosene ORM
在内部不跟踪实体本身,而是跟踪与实体关联的元数据包,而元数据包又只维护对原始实体的弱引用。这样,当这些实体不再需要时,CLR 垃圾收集器就可以回收它们。
但这同样意味着一些元数据包将没有关联的实体。Kerosene ORM
存储库实现了一个内部收集器,它会定期触发以清理这些“僵尸”包。请注意,此功能不是IDataRepository
接口的一部分,而是由具体的DataRepository
实例提供的。
在一般情况下,您无需与此机制交互。但为了调试目的,您可能希望禁用它,对于这些场景,您可以使用以下方法:
repo.DisableCollector();
repo.EnableCollector();
您还可以使用IsCollectorEnabled
属性来查询存储库的内部收集器状态。EnableCollector()
方法还有一个重载,接受两个参数:收集器触发后的毫秒数(如果您想出于性能原因调整此间隔),以及一个布尔值,指定是否在触发前强制执行 CLR 垃圾回收——但后一个参数除了调试或非常专业的场景外,很少使用。
代理类型
尽管我们的应用程序将只处理自己的领域级 POCO 实例,但Kerosene ORM
从数据库返回的实体可能不是这些类型,而是从原始 POCO 类型继承的代理类型。
当使用惰性依赖项时就会出现这种情况:Kerosene ORM
将创建一个代理类型,其中惰性虚拟属性的 setter 和 getter 被覆盖(如果可能),以便注入以延迟方式加载其内容的逻辑。代理类型中还包含一些额外的字段和属性(其名称以“_Completed”或“_Source”结尾),但除此之外,它们的实例行为与原始实例相同。
NewEntity()
方法将返回原始 POCO 类的一个实例,或者,如果Kerosene ORM
为关联的映射创建了一个代理类型,则返回代理类型的一个实例:
var obj = repo.NewEntity();
请记住,如果原始 POCO 类有一个无参数构造函数,它将被使用。否则,将创建一个未初始化的新对象并返回,而不调用任何构造函数。
Kerosene ORM
生成这些代理类型的方式包括发出一些 IL 代码来添加上述额外的属性和字段,并覆盖惰性属性的虚拟 getter 和 setter。请参阅附带的文章了解更多详情。
控制工作单元异常
Kerosene ORM
遵循工作单元模式。它规定我们必须将所有感兴趣的更改操作提交(注释)到存储库中,并在完成时,将它们作为一个单一单元对底层数据库执行:
var region = new Region() { ... }; repo.Insert(region);
var ctry = new Country() { ... }; repo.Insert(ctry);
// etc...
repo.ExecuteChanges();
在内部,Kerosene ORM
将级联与我们已提交更改操作的实体相关联的依赖项,重新排序所有这些依赖项以满足其逻辑约束,然后在一个事务下逐个执行它们。
如果任何操作执行失败,事务将中止,使数据库恢复到原始状态,然后,默认情况下,会抛出描述失败的异常(这通常是数据库返回的异常)。我们的应用程序可以在 try-catch 块内执行ExecuteChanges()
方法,或者它可以将存储库的OnExecuteChangesError
属性设置为一个委托,该委托将在发生异常时被调用,并且以该异常作为其参数。在这种情况下,异常不会被抛出,而是作为该参数使用。这对于许多场景以及日志记录和跟踪目的非常方便。
强制列
当数据库中某个列没有对应的成员,但出于某种原因我们希望该列参与映射机制时,我们可以通过在映射的Columns
集合中添加一个条目来实现:
Columns.Add(x => x.MyColumnName)
.OnWriteRecord(entity => { ... })
.OnLoadEntity((value, entity) => { ... })
.OnMember(x => x.MyMemberName);
OnWriteRecord()
方法接受一个委托,该委托应在需要将该列的值持久化回数据库时返回该值。它以宿主实体作为参数,并且可以执行任何需要的操作。
类似地,OnLoadEntity()
方法接受一个委托,该委托将在从数据库读取关联记录时被调用。它接受列的值和对宿主实体的引用。同样,它可以执行任何需要的操作。
最后一个,OnMember()
,用于简化目的,当不需要调用复杂的运算时,我们只想将数据库中的列映射到类型中名称可能不匹配的给定成员。在这种情况下,其参数是一个动态 lambda 表达式,解析为该成员的名称。请记住,它可以是属性或字段,并且可以是公共、受保护或私有的。
一旦映射通过验证,其Columns
集合将包含所有考虑在内的列。在许多情况下,Kerosene ORM
会自动发现要映射的列。如果出现这种情况,它们的AutoDiscovered
属性将设置为 true。
还有什么?
本文是关于Kerosene ORM
实体映射操作模式的最后一个通用教程。接下来的文章将更短,并专注于Kerosene ORM
内部使用的技术的具体细节。