DBMapper - 一个新的 ORM 工具






4.78/5 (11投票s)
DBMapper -
引言
本文介绍 DBMapper
,一个我设计和开发的新 ORM 工具。它使用 C# 3.5 编写,并在 Oracle 10g / 11g, MS SQL Server 2005 / 2008 / 2012 上进行了测试。
DBMapper
的主要特性
- 无供应商锁定。您可以使用相同的源代码/配置同时支持 Oracle 和 SQL Server 以及多个版本,无需更改任何内容。它提供了独立于供应商的映射,用于处理依赖于供应商的行为,如序列、自增、时间戳/行版本、触发器、获取回写命令语法、参数命名和列/参数类型,甚至可以在不重启应用程序的情况下在线从 Oracle 切换到 SQL Server(如提供的示例代码所示)。
- 易于使用的“流畅”接口,并通过 Visual Studio 智能感知进行最小化配置。
- 运行时映射配置更改,无需重新构建或重启应用程序。
- 能够根据查询参数缓存查询结果,可配置的逻辑删除和独立于供应商的乐观锁定(时间戳)行为,
- 您可以将实体属性组织在无限深度的嵌套类中。
- POCOs(您无需从框架基类派生实体类或在类上标记属性,您的类保持持久性无关),因此您可以将任何具有
public
属性的类,甚至是第三方或系统类映射到数据库对象。 - 如果您想使用 ActiveRecord 模式/语法,它也提供了一个
ActiveRecord
基类。 - 您可以在 .NET 端托管您的日志记录/授权等触发器,而不是使用数据库触发器(无需使用 PL/SQL 或 T/SQL)。
- 高性能,高效的算法,使用内部缓存和
Code.Emit
原生代码生成以提高性能(不使用慢速的反射 API) - 它是开源的,具有干净且可扩展的代码架构,可在必要时进行扩展或修改。
那么什么是 ORM?它是从头到尾编写相同的数据库访问代码的工具。使用 ORM,您可以将类映射到表/视图/查询,并轻松地将实体集合从数据库对象填充,然后将它们保存回数据库表。您还可以使用类似于调用原生 .NET 方法的语法调用存储过程和数据库函数。
映射连接
那么让我们开始映射。首先,我们定义我们的连接。以下代码定义了我们名为 OracleTest
和 SqlServerTest
的连接的供应商信息。我们在这里为查询中使用的标准数据库参数前缀提供“:
”。我们可以使用任何特殊字符,因为它在 Oracle 上运行查询时将被内部替换为 :
,而在 SQL Server 连接上将被替换为 @
。
Connections
.Add(MyConnection.OracleTest, DBVendor.Oracle, ":")
.Add(MyConnection.SqlServerTest, DBVendor.Microsoft, ":");
请注意,对于所有字符串定义(如连接、模式、表/列、SP 名称等),DBMapper
API 要求您传递一个 Enum
类型来获得智能感知名称,避免输入错误,并减少输入量。我们在下面的 enum
中声明了我们的连接名称。
public enum MyConnection {
OracleTest,
SqlServerTest
}
在 App.config 文件中,我们还提供了我们的连接字符串(连接字符串也可以使用您在网上找到的标准 .NET 方法进行加密)。
那么我们在哪里映射我们的连接和实体呢?我们在实现 IMapConnection
接口的类的 MapConnection
方法中映射我们的连接。我们在实现 IMapEntity
接口的任意数量的类的 MapEntity
方法中映射我们的实体。App.config 中有两个 DLL 路径的设置,一个用于我们的 IMapConnection
实现类,另一个用于我们的 IMapEntity
实现类。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="OracleTest"
connectionString="User Id=leas;Password=leas;Data Source=localhost" />
<add name="SqlServerTest" connectionString="Integrated Security=SSPI;Data Source=
localhost\SQLEXPRESS;Initial Catalog=DeneDB;Max Pool Size = 10000;Pooling = True" />
</connectionStrings>
<appSettings>
<add key="IMapConnectionDllPath" value="..\..\..\Entity\bin\Debug\Common.dll"/>
<add key="IMapEntityDllPath" value="..\..\..\Entity\bin\Debug\Entity.dll"/>
</appSettings>
</configuration>
映射一个用于查询的基本类
接下来,我们定义我们的实体类。实体类不需要任何特定的基类,并且可以拥有嵌套类或内部类,在嵌套类或内部类中包含映射的属性。下面是两个示例实体类:Test
和 TestReport
。Test
是一个表实体,具有表和查询映射的属性,以及其内部类的非映射属性。TestReport
是一个仅查询的实体(在此框架中称为视图实体)。让我们先看看基本的 TestReport
实体
public class TestReport {
public string A { get; set; }
public string B { get; set; }
}
我们在下面的映射语句中提供连接名称、作为 C# 委托的触发器操作、属性到列的映射以及 SQL select
查询及其参数名称
ViewEntity<TestReport>
.Connection(CommonDefinitions.Connection)
.TriggerActions
.AddForBeforeSelectCommand(authorizationTrigger)
.Add(When.AfterSelectForEachRow, logTrigger)
.ViewColumns
.Add(x => x.A, TEST_REPORT.A)
.Add(x => x.B, TEST_REPORT.B)
.Queries
.Add(Query.SelectReport,
@"SELECT :a AS A, GETDATE() AS B
FROM DUAL
WHERE :a = :aaa
AND 1 in (:list)", true,
new InputParameterNameType("a", typeof(string)),
new InputParameterNameType("aaa", typeof(string)),
new InputParameterNameType("list", typeof(string))
);
带有嵌套属性和 ActiveRecord 的实体
这是一个包含内部类、表和视图映射列、主键、时间戳和其他列的自动设置列值提供程序的庞大的表实体示例等等。请注意,如果您不支持两个数据库供应商,或者通过通用功能子集支持两个供应商,则不必提供所有这些信息。此示例庞大,因为它旨在演示和测试 DBMapper
的大多数功能。
public class Test : ActiveRecord {
public decimal Price { get; set; }
public int Quantity { get; set; }
public TestEnum ItemType { get; set; }
public DateTime? OrderDate { get; set; }
public string Name { get; set; }
public string Text { get; set; }
public TestInner Inner { get; set; }
public List<TestDetail> Details { get; set; }
// no column in table for this, but mapped to on some query columns
public int WhatIsThis { get; set; }
private DateTime What1; //not a public property, not mapped to db
public DateTime What2; //not a public property, not mapped to db
private string NonExistent1 { get; set; } //not a public property, not mapped to db
private string NonExistent2; //not a public property, not mapped to db
public static Test SelectByTestId(int testId) {
return Test.SelectFirst<Test>(Query.SelectByTestId, new InputParameter("testId", testId));
}
public static Test SelectByTestIdAndQuantity(int testId, int quantity) {
return Test.SelectFirst<Test>(Query.SelectByTestIdAndQuantity,
new InputParameter("testId", testId),
new InputParameter("quantity", quantity));
}
public static IList<Test> SelectAll() {
return Test.Select<Test>(Query.SelectAll);
}
}
public class TestInner {
public int TestId { get; set; }
public string InnerDescription1 { get; set; }
public string InnerDescription2 { get; set; }
public TestInnerInner InnerInner { get; set; }
}
public class TestInnerInner {
//this property need not exist, bu if it exists, it will be filled back on logical delete
public DateTime DeleteDate { get; set; }
//this property could be removed, bu if it exists, it will be filled back on insert
public string InsertOSUserName { get; set; }
//this property could be removed, bu if it exists, it will be filled back on update
public string UpdateOSUserName { get; set; }
//this property could be removed, bu if it exists, it will be filled back on logical delete
public string DeleteOSUserName { get; set; }
public byte[] Timestamp { get; set; }
public string InnerDescription { get; set; }
}
public class TestDetail {
public int TestDetailId { get; set; }
public int TestId { get; set; }
public string DetailDescription { get; set; }
}
public enum TestEnum {
Type1 = 1,
Type2 = 2
}
映射一个用于所有 CRUD 操作和触发器操作的类
然后我们开始将这些类和 public
属性映射到表和查询,如下所示(首先,我们定义几个函数委托,用于自定义触发器或作为自动值提供程序)
Func<object> userNameFunction = delegate() { return Environment.UserName; };
Func<object> nowFunction = delegate() { return DateTime.Now; };
Func<object> dbNowFunction = delegate() { return DB.CallFunction<DateTime>(
CommonDefinitions.Connection, CommonDefinitions.Schema, DBFunction.FGET_DATE); };
Func<object> dbTimestampFunction = delegate() { DateTime dbNow = DB.CallFunction<DateTime>(
CommonDefinitions.Connection, CommonDefinitions.Schema, DBFunction.FGET_DATE);
if (CommonDefinitions.Connection == MyConnection.OracleTest) return dbNow;
else return BitConverter.GetBytes(dbNow.Ticks); };
Func<object> newTestIdFunction = delegate() { return DB.CallFunction<int>(
CommonDefinitions.Connection, CommonDefinitions.Schema, DBFunction.FTEST_ID_NEXTVAL); };
这是 table-entity
映射
TableEntity<test>>
.Table(Table.TEST2, CommonDefinitions.Schema, CommonDefinitions.Connection,
CommonDefinitions.PrimaryKeyValueProvider, CommonDefinitions.TimestampValueProvider)
.PrimaryKeys
.Add(TEST.TEST_ID, x => x.Inner.TestId, Sequence.S_TEST,
CommonDefinitions.Schema, DBFunction.FTEST_ID_NEXTVAL, null)
.Timestamp(TEST.LOCK_TIMESTAMP, x => x.Inner.InnerInner.Timestamp, _
null, null, dbTimestampFunction)
.AutoSetColumns
.Add(Before.Insert, TEST.INS_DATE, CommonDefinitions.Schema,
DBFunction.FGET_DATE, null, AutoSetValueProvider.DBFunction)
.Add(Before.Insert, TEST.INS_OS_USER, null, null,
userNameFunction, AutoSetValueProvider.AppFunctionDelegate)
.Add(Before.Update, TEST.UPD_DATE, CommonDefinitions.Schema,
DBFunction.FGET_DATE, null, AutoSetValueProvider.DBFunction)
.Add(Before.Update, TEST.UPD_OS_USER, null, null, userNameFunction, _
AutoSetValueProvider.AppFunctionDelegate)
.Add(Before.LogicalDelete, TEST.DEL_DATE, CommonDefinitions.Schema,
DBFunction.FGET_DATE, null, AutoSetValueProvider.DBFunction)
.Add(Before.LogicalDelete, TEST.DEL_OS_USER, null, null,
userNameFunction, AutoSetValueProvider.AppFunctionDelegate)
.TriggerActions
.AddForBeforeSelectCommand(authorizationTrigger)
.Add(When.AfterSelectForEachRow, logTrigger)
.Add(When.BeforeInsertForEachRow, authorizationTrigger)
.Add(When.AfterInsertForEachRow, logTrigger)
.Add(When.BeforeUpdateForEachRow, authorizationTrigger)
.Add(When.AfterUpdateForEachRow, logTrigger)
.Add(When.BeforeDeleteForEachRow, authorizationTrigger)
.Add(When.AfterDeleteForEachRow, logTrigger)
.TableColumns
.Add(x => x.Name, TEST.NAME)
.Add(x => x.Text, TEST.TEXT)
.Add(x => x.Price, TEST.PRICE)
.Add(x => x.Quantity, TEST.QUANTITY)
.Add(x => x.OrderDate, TEST.ORDER_DATE, true)
.Add(x => x.ItemType, TEST.ITEM_TYPE)
.Add(x => x.Inner.InnerInner.InnerDescription, TEST.DESCRIPTION)
//this mapping need not exist, bu if it exists, it will be filled back on logical delete
.Add(x => x.Inner.InnerInner.DeleteDate, TEST.DEL_DATE)
//this mapping need not exist, bu if it exists, it will be filled back on insert
.Add(x => x.Inner.InnerInner.InsertOSUserName, TEST.INS_OS_USER)
//this mapping need not exist, bu if it exists, it will be filled back on update
.Add(x => x.Inner.InnerInner.UpdateOSUserName, TEST.UPD_OS_USER)
//this mapping need not exist, bu if it exists, it will be filled back on logical delete
.Add(x => x.Inner.InnerInner.DeleteOSUserName, TEST.DEL_OS_USER)
.ViewColumns
.Add(x => x.Inner.InnerDescription1, TEST.DESCRIPTION1)
.Add(x => x.Inner.InnerDescription2, TEST.DESCRIPTION2)
.Add(x => x.WhatIsThis, TEST.WHAT_IS_THIS)
.Queries
.Add(Query.SelectAll,
@"SELECT 1 AS WHAT, T.QUANTITY, -99 AS WHAT_IS_THIS, T.DESCRIPTION,
T.DESCRIPTION AS DESCRIPTION1, T.DESCRIPTION AS DESCRIPTION2,
T.TEST_ID, T.TEXT, T.PRICE, T.LOCK_TIMESTAMP, T.ORDER_DATE, T.ITEM_TYPE
FROM TEST2 T
WHERE TEST_ID <= 200"
)
.Add(Query.SelectByTestId,
@"SELECT T.UPD_OS_USER, T.QUANTITY, -99 AS WHAT_IS_THIS, T.DESCRIPTION,
T.DESCRIPTION AS DESCRIPTION1, T.DESCRIPTION AS DESCRIPTION2,
T.TEST_ID, T.TEXT, T.PRICE, T.LOCK_TIMESTAMP, T.ORDER_DATE, T.ITEM_TYPE
FROM TEST2 T
WHERE TEST_ID = :testId", true,
new InputParameterNameType("testId", typeof(int))
)
.Add(Query.SelectByTestIdForFoo,
@"SELECT T.QUANTITY, -99 AS WHAT_IS_THIS, T.DESCRIPTION,
T.TEST_ID, T.TEXT, T.PRICE, T.LOCK_TIMESTAMP
FROM TEST2 T
WHERE TEST_ID = :testId", true,
new InputParameterNameType("testId", typeof(int))
)
.Add(Query.SelectByTestIdAndQuantity,
@"SELECT *
FROM TEST2
WHERE TEST_ID = :testId
AND QUANTITY = :quantity",
new InputParameterNameType("testId", typeof(int)),
new InputParameterNameType("quantity", typeof(int))
)
.Add(Query.SelectWithPaging,
@"WITH r AS
(
SELECT
ROW_NUMBER() OVER (ORDER BY t1.name desc) AS row_number,
t2.TEST_ID,t2.INS_DATE,t2.NAME,t2.TEXT
FROM test2 t1, test2 t2
where t1.TEST_ID = t2.TEST_ID and t1.INS_DATE = t2.INS_DATE
and t1.LOCK_TIMESTAMP = t2.LOCK_TIMESTAMP
)
SELECT * FROM r
where row_number between :pageSize*(:pageNumber-1) + 1 and :pageSize*:pageNumber
ORDER BY name desc",
new InputParameterNameType("pageSize", typeof(int)),
new InputParameterNameType("pageNumber", typeof(int))
);
public enum ValueProvider {
App = 1,
AppFunctionDelegate = 2,
Sequence = 3,
DBFunction = 4,
DBTriggerredAutoValue = 5
}
您可以看到所有数据库对象名称字符串都由 enum
提供。我认为这比在代码中到处散布字符串更易于管理,而且比声明和初始化字符串常量打字更少。
主键、timestamp
和 autoset
列可以通过以下 ValueProvider
选项进行设置
如果类属性将在您的应用程序代码中设置,并且在数据库命令执行后不取回,则使用 App
选项。
如果属性将在数据库命令执行之前由您提供的函数委托设置,则使用 AppFunctionDelegate
选项。
如果列将从序列获取值,并在数据库命令执行后取回以填充类属性,则使用 Sequence
选项。
DBFunction
与 Sequence
选项类似,不同之处在于该列将从数据库函数获取其值。
如果列是在数据库端填充的(在触发器内或作为 SQL Server 中的自增列),则使用 DBTriggerredAutoValue
选项。此列不会被插入/更新,而只会在数据库命令执行后取回以填充类属性。
如果您在映射中提供自动设置列,它们将在 insert
、update
或 delete
之前被填充。如果存在用于逻辑删除的自动设置列映射,那么 delete
语句将变成逻辑删除(即,更新像 delete_date
这样的列,而不是删除记录)。
如果您提供时间戳映射,更新(以及 delete
和逻辑 delete
)将检查您的版本是否是当前版本,如果不是则失败(即,您读取记录,其他人更新该记录,当您尝试更新它时,您将收到一个错误,提示您在更新之前必须重新查询,以确保数据一致性。
并非所有数据库供应商和同一数据库的每个版本都支持相同的特性集,例如 Oracle 没有自增列,SQL Server 2005 和 2008 没有序列(而 2012 有),此外,timestamp
类型在 SQL Server 中是一个二进制计数器,而在 Oracle 中则具有日期/时间语义。因此,表映射部分有一个全局主键值提供程序和 Timestamp
列值提供程序“策略”部分,可以轻松地根据您或您的客户想要使用的数据库的供应商/版本进行更改。然后,您可以为该列定义所有备用值提供程序,并从一个地方更改全局策略。我正在考虑的一件事是将此值提供程序策略选项移至连接定义中,作为实体默认值,而不是为每个实体映射重复它。我将要添加的另一件事是,映射常见基实体类的属性,以便这些映射将被所有子实体类“继承”。这样,您就不会重复所有常见的自动设置列映射(假设这些列名在映射到子实体的所有表中都相同)。
CRUD(创建/读取/更新/删除)语句
一个示例 select
语句
IList<TestReport> testReportList = DB.Select<TestReport>(Query.SelectReport,
new InputParameter("a", "x"),
new InputParameter("aaa", "x"), new InputParameter("list", list));
或者,如果您使用 ActiveRecord
作为基类,则为类似这样的内容
public static Test SelectByTestId(int testId) {
return Test.SelectFirst<Test>(Query.SelectByTestId, new InputParameter("testId", testId));
}
一个示例 insert
语句
DB.Insert(test);
或者,如果您使用 ActiveRecord
作为基类
test.Insert();
更新
DB.Update(test, Query.SelectByTestId);
或者 ActiveRecord
版本
test.Update(Query.SelectByTestId);
Delete
类似。调用数据库函数或存储过程(最后一个使用输出参数返回值);
DateTime dbNow = DB.CallFunction<DateTime>(
CommonDefinitions.Connection, CommonDefinitions.Schema, DBFunction.FGET_DATE);
//Console.WriteLine("dbNow:" + dbNow);
DB.CallProcedure(CommonDefinitions.Connection, CommonDefinitions.Schema, StoredProcedure.PTEST);
OutputParameter[] outputParameters = new OutputParameter[] {
new OutputParameter("P_SOUT", null, typeof(string)) };
DB.CallProcedure(CommonDefinitions.Connection, CommonDefinitions.Schema,
StoredProcedure.PTEST1, outputParameters, new InputParameter("P_NIN", 123));
//Console.WriteLine("p_sout output:" + outputParameters[0].Value);
只有表列被插入和更新,视图列则不。为了数据一致性,只有查询过的列才应该被更新,因此 Update
语句只更新给定查询中的表列。查询只填充与表列或视图列映射和查询列名匹配的属性。如果查询中存在一个不存在于表列或视图列映射中的列,那么自然,它不会填充任何属性。要使用示例测试项目,您需要访问 SQL Server 和 Oracle 数据库,以及以下数据库对象:SQL Server(同一表的两个版本,带或不带自增(identity)以及不同的时间戳类型(具有计数器语义的 timestamp/rowversion 与具有日期/时间语义的二进制,如 Oracle))
CREATE TABLE [dbo].[TEST2](
[TEST_ID] [numeric](9, 0) NOT NULL,
[NAME] [nchar](50) NULL,
[TEXT] [text] NULL,
[PRICE] [numeric](22, 2) NULL,
[QUANTITY] [numeric](9, 0) NULL,
[ORDER_DATE] [datetime] NULL,
[LOCK_TIMESTAMP] [varbinary](8) NOT NULL,
[DEL_DATE] [datetime] NULL,
[DESCRIPTION] [nvarchar](100) NULL,
[ITEM_TYPE] [numeric](2, 0) NULL,
[INS_DATE] [datetime] NULL,
[UPD_DATE] [datetime] NULL,
[INS_OS_USER] [nvarchar](50) NULL,
[UPD_OS_USER] [nvarchar](50) NULL,
[DEL_OS_USER] [nvarchar](50) NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
CREATE TABLE [dbo].[TEST](
[TEST_ID] [numeric](9, 0) IDENTITY(1,1) NOT NULL,
[NAME] [nchar](50) NULL,
[TEXT] [text] NULL,
[PRICE] [numeric](22, 2) NULL,
[QUANTITY] [numeric](9, 0) NULL,
[ORDER_DATE] [datetime] NULL,
[LOCK_TIMESTAMP] [timestamp] NOT NULL,
[DEL_DATE] [datetime] NULL,
[DESCRIPTION] [nvarchar](100) NULL,
[ITEM_TYPE] [numeric](2, 0) NULL,
[INS_DATE] [datetime] NULL,
[UPD_DATE] [datetime] NULL,
[INS_OS_USER] [nvarchar](50) NULL,
[UPD_OS_USER] [nvarchar](50) NULL,
[DEL_OS_USER] [nvarchar](50) NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
CREATE NONCLUSTERED INDEX [I_TEST2_TEST_ID] ON [dbo].[TEST2]
(
[TEST_ID] ASC
)
此外,DUAL 表用于 SQL Server 和 Oracle 的相同 select
语句,在我们的查询中
CREATE TABLE [dbo].[DUAL](
[DUMMY] [nvarchar](1) NULL
) ON [PRIMARY]
CREATE PROCEDURE [dbo].[PTEST]
AS
BEGIN
select 1
END
CREATE PROCEDURE [dbo].[PTEST1]
@P_NIN numeric,
@P_SOUT nvarchar(100) output
AS
BEGIN
SELECT @P_SOUT = 'SSS' + cast(@P_NIN as nvarchar(30))
END
CREATE FUNCTION [dbo].[FGET_DATE]()
RETURNS DATETIME
AS
BEGIN
RETURN GETDATE();
END
CREATE FUNCTION [dbo].[FTEST_ID_NEXTVAL]()
RETURNS int
AS
BEGIN
DECLARE @i int;
SELECT @i = max(TEST_ID) from TEST2;
RETURN isnull(@i, 0) + 1;
END
对于 Oracle
create table test2 (
TEST_ID NUMBER(9) NOT NULL,
NAME VARCHAR2(50),
TEXT CLOB,
PRICE NUMBER(22,2),
QUANTITY NUMBER(9),
ORDER_DATE DATE,
LOCK_TIMESTAMP TIMESTAMP(6) NOT NULL,
DEL_DATE DATE,
DESCRIPTION VARCHAR2(100),
ITEM_TYPE NUMBER(2),
INS_DATE DATE,
UPD_DATE DATE,
INS_OS_USER VARCHAR2(50),
UPD_OS_USER VARCHAR2(50),
DEL_OS_USER VARCHAR2(50)
)
create function fget_date return date as
begin return sysdate; end;
create function ftest_id_nextval return number is
begin
return s_test.nextval;
end;
create function getdate return date as
begin return sysdate; end;
create procedure ptest as
begin null; end;
create procedure ptest1(p_nin number, p_sout out varchar2) as
begin null; p_sout := 'sss'||to_char(p_nin); end;
CREATE INDEX I_TEST2_TEST_ID ON TEST2 (TEST_ID);
对于 Oracle 和 SQL Server 2012(只有 2012 版本有序列)
create sequence s_test;
我在测试代码中使用的模式名称是 Oracle 的 leas,SQL Server 的 dbo。您可以创建这些对象并运行代码来测试 DBMapper
。
重要提示:使用同一记录进行多线程(并发命令)测试 update
和逻辑 delete
。通常,DBMapper
会抛出“记录已更改,请在保存前重新查询
”异常以确保数据一致性,但为了更真实地进行测试,我已注释掉抛出此异常的行。该方法是 ConnectionMapping
类上的 ExecuteNonQuery
//if (rowCount == 0 && checkRowCount)
// throw new RecordNotFoundOrRecordHasBeenChangedException(
// "Record not found or record has been updated since your last query. " +
// "Please re-query this record and try again.");
在将其用于实际生产系统之前,您应该删除此 if
块上的注释并抛出此异常!最后一点,DBMapper
使用 Oracle Data Access Components for .NET 的版本 2.112.2.0,请记住在您的机器上安装 ODAC,否则您将无法构建或使用 DBMApper
。
关注点
在编写这个 ORM 工具的过程中,我学到了很多。我变得更擅长 T-SQL、性能分析、重构、面向对象设计和模式。这个项目最初是作为测试我在网上找到的动态属性getter/setter代码的一个非常基础的原型,并在接下来的几个月里,主要利用业余时间,发展成了一个具有许多特性的多供应商 ORM。起初它变成了一个混乱的过程,然后我使用模板方法和抽象工厂模式对其进行了重构和重新设计,从那时起,它变得易于管理且错误更少。直到最后几周,它都没有经过多线程(并发用户)模式的测试,并且是惰性生成内部缓存(在第一次需要该缓存时,而不是之前)。然而,这意味着我不得不在这些缓存上使用大量的读写锁,时间间隔很长,这在并发模式下导致了巨大的性能下降。然后,我将内部缓存/命令/Code Emit 生成器转移到在映射完成后在启动时执行所有工作,这样我就不再需要锁定任何东西,然后并发性能得到了提升。在其当前形式下,数百个并发命令的执行速度与单个命令执行一样快。
源代码组织
提供的 DBMapper.zip 文件包含整个 DBMapper
引擎的源代码以及运行基准测试和演示用法的测试项目。
- DBMapper 项目包含
DBMapper
引擎及其公共映射和命令 API 的所有类。 - Fluent.cs 包含流畅的映射 API,Mapper.cs 包含从流畅接口调用的
private
内部映射 API。 - Mappings.cs 包含运行时命令执行使用的映射类、字典和缓存,还包含运行时更改映射的公共 API。
- DB.cs 包含
public
CRUD/SP 调用命令 API。 - Command.cs 包含在 CRUD 和 SP 调用上执行的模板命令类。
- CommandFactory.cs 包含一些 CRUD 命令的供应商特定重写命令类。
- CommandGenerator.cs 类似于 Command.cs,但只包含用于生成命令类所需的必要信息/缓存/
Code.Emit
原生代码的类。 - CommandGeneratorFactory.cs 类类似于 CommandFactory.cs,同样为供应商特定命令生成信息/缓存/
Code.Emit
原生代码。 - ActiveRecord.cs 是 CRUD 命令 API 的基本包装器,如果您想使用它,可以用作基类。
- MultiKeyDictionary.cs 包含实现多个键的
Dictionary
类。 - Entity 项目包含用于测试的实体类,以及这些实体的映射类,实现了
IMapEntity
接口。 - Business 项目包含使用这些测试实体类进行 CRUD 操作和调用 SP 的类。
- 最后,
Test
项目加载实现了IMApConenction
和IMApEntity
的类,在这些类上运行MapConnection
和MapEntity
方法,然后运行GenerateCommands
方法以在系统使用之前进行准备。最后,它在 business 项目上运行测试。
这些测试演示了使用 public
API 对实体对象运行 CRUD 数据库命令,调用 SP,然后在运行时更改实体的模式、连接和数据库供应商,生成受影响实体的命令,然后重新运行基准测试和相同的测试。即使是切换数据库供应商也无需重新构建或重启,哇!!!
历史
这是 DBMapper
的一个完整的 Beta 版本。它尚未经过大量用户的测试。我已经模拟了重负载(数百个并发数据库命令/用户)对其进行了测试,并且性能非常出色。我很乐意为想要使用和测试它的人提供帮助。提供的源代码还附带了我用于测试的测试项目。如果您有任何问题或建议,或要报告错误,请在下方留言。