这篇文章展示了如何查询一个
高度规范化的SQL数据库,并将结果映射到一组高度嵌套的C# POCO对象中。
材料:
- 8行C#代码。
- 一些相对简单的SQL,使用了一些连接。
- 两个很棒的库。
让我解决这个问题的见解是将
MicroORM
与
将结果映射回POCO实体
分开。因此,我们使用两个不同的库:
-
Dapper作为MicroORM。
-
Slapper.Automapper用于映射。
本质上,我们使用Dapper查询数据库,然后使用Slapper.Automapper将结果直接映射到我们的POCOs。
优点
- 简洁性。代码不超过8行,更容易理解、调试和修改。
- 少量代码。只需几行代码,Slapper.Automapper 就可以处理任何你提供的内容,即使我们有一个复杂的嵌套 POCO(例如,POCO 包含
List<MyClass1>
,其中又包含 List<MySubClass2>
等)。
- 速度。这两个库都具有非常多的优化和缓存功能,使它们的运行速度几乎与手动调整的 ADO.NET 查询一样快。
- 关注点分离。我们可以更换 MicroORM 为其他 ORM,映射仍然有效,反之亦然。
- 灵活性。Slapper.Automapper 处理任意嵌套层次结构,不限于几个嵌套级别。我们可以轻松地进行快速更改,一切仍将正常工作。
- 调试。我们可以先查看 SQL 查询是否正常工作,然后再检查 SQL 查询结果是否正确映射回目标 POCO 实体。
- 在 SQL 中开发的便利性。我发现使用
inner join
创建扁平化查询以返回扁平结果比创建多个选择语句并在客户端上进行拼接更容易。
- 优化的 SQL 查询。在高度规范化的数据库中,创建一个扁平查询允许 SQL 引擎对整个查询应用高级优化,如果构建和运行许多小型单独的查询,则通常不可能实现这种优化。
- 信任。Dapper 是 StackOverflow 的后端,Randy Burden 也是一位超级明星。还需要说什么吗?
- 开发速度。我能够处理一些非常复杂的查询,包含多层嵌套,但开发时间相当短。
- 更少的错误。我只写了一次,它就正常工作了,而且这种技术现在正在帮助推动一个 FTSE 公司的发展。代码如此之少,没有出现意外行为。
缺点
- 返回超过1,000,000行时的扩展性问题。当返回 < 100,000 行时,效果很好。然而,如果我们要返回 >1,000,000 行,则为了减少我们与 SQL 服务器之间的流量,我们不应该使用
inner join
(它会带回重复数据),而应该使用多个 select
语句,并在客户端上将所有内容拼接在一起(请参见此页面上的其他答案)。
- 这种技术是面向查询的。我没有使用这种技术来写入数据库,但我相信 Dapper 可以通过更多额外的工作来做到这一点,因为 StackOverflow 本身使用 Dapper 作为其数据访问层(DAL)。
性能测试
在我的测试中,Slapper.Automapper 对 Dapper 返回的结果增加了一些小开销,这意味着它仍然比 Entity Framework 快10倍,并且组合仍非常接近 SQL + C# 的理论最大速度。
In most practical cases, most of the overhead would be in a less-than-optimum SQL query, and not with some mapping of the results on the C# side.
Performance Testing Results
Total number of iterations: 1000
- Dapper by itself: 1.889 milliseconds per query, using 3 lines of code to return the dynamic.
- Dapper + Slapper.Automapper: 2.463 milliseconds per query, using an additional 3 lines of code for the query + mapping from dynamic to POCO Entities.
Worked Example
In this example, we have a list of Contacts, and each Contact can have one or more phone numbers.
POCO Entities
public class TestContact
{
public int ContactID { get; set; }
public string ContactName { get; set; }
public List<TestPhone> TestPhones { get; set; }
}
public class TestPhone
{
public int PhoneId { get; set; }
public int ContactID { get; set; }
public string Number { get; set; }
}
SQL表 TestContact
![enter image description here](https://istack.dev59.com/SDfIL.webp)
SQL 表格 TestPhone
请注意,此表格具有一个外键 ContactID
,它引用了 TestContact
表格(这对应于上面 POCO 中的 List<TestPhone>
)。
![enter image description here](https://istack.dev59.com/L2yz2.webp)
SQL生成平面结果
在我们的SQL查询中,我们使用尽可能多的JOIN
语句来获取所有需要的数据,以平面、非规范化形式呈现。是的,这可能会在输出中产生重复项,但是当我们使用Slapper.Automapper将此查询的结果自动映射到我们的POCO对象映射时,这些重复项将自动消除。
USE [MyDatabase];
SELECT tc.[ContactID] as ContactID
,tc.[ContactName] as ContactName
,tp.[PhoneId] AS TestPhones_PhoneId
,tp.[ContactId] AS TestPhones_ContactId
,tp.[Number] AS TestPhones_Number
FROM TestContact tc
INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId
![enter image description here](https://istack.dev59.com/ORpKf.webp)
C#代码
const string sql = @"SELECT tc.[ContactID] as ContactID
,tc.[ContactName] as ContactName
,tp.[PhoneId] AS TestPhones_PhoneId
,tp.[ContactId] AS TestPhones_ContactId
,tp.[Number] AS TestPhones_Number
FROM TestContact tc
INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId";
string connectionString =
using (var conn = new SqlConnection(connectionString))
{
conn.Open();
{
dynamic test = conn.Query<dynamic>(sql);
Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestContact), new List<string> { "ContactID" });
Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestPhone), new List<string> { "PhoneID" });
var testContact = (Slapper.AutoMapper.MapDynamic<TestContact>(test) as IEnumerable<TestContact>).ToList();
foreach (var c in testContact)
{
foreach (var p in c.TestPhones)
{
Console.Write("ContactName: {0}: Phone: {1}\n", c.ContactName, p.Number);
}
}
}
}
输出
![enter image description here](https://istack.dev59.com/PxnmA.webp)
POCO实体层次结构
在Visual Studio中查看,我们可以看到Slapper.Automapper已经正确填充了我们的POCO实体,即我们有一个List<TestContact>
,每个TestContact
都有一个List<TestPhone>
。
![enter image description here](https://istack.dev59.com/enl7h.webp)
注意事项
Dapper和Slapper.Automapper都在内部缓存所有内容以提高速度。如果遇到内存问题(很少见),请确保定期清除它们的缓存。
请使用下划线 (_
) 符号命名返回的列,以便为Slapper.Automapper提供将结果映射到POCO实体的线索。
请为每个POCO实体提供主键的线索,例如在行Slapper.AutoMapper.Configuration.AddIdentifiers
中。您也可以在POCO上使用Attributes
来完成此操作。如果跳过此步骤,则可能会出现问题(理论上),因为Slapper.Automapper不知道如何正确进行映射。
更新于2015-06-14
成功地将这种技术应用于一个拥有超过40个规范化表的大型生产数据库。它完美地映射了一个包含16个内连接和左连接的高级SQL查询到正确的POCO层次结构中(包含4个嵌套级别)。这些查询速度极快,几乎和在ADO.NET中手动编码一样快(通常查询时间为52毫秒,从平面结果映射到POCO层次结构的时间为50毫秒)。这确实不是什么革命性的东西,但它肯定比Entity Framework更快、更易于使用,特别是如果我们只是运行查询。
更新2016-02-19
代码已经在生产环境中无缺陷地运行了9个月。最新版本的Slapper.Automapper
已经包含了我应用于修复与SQL查询返回空值相关的问题的所有更改。
更新2017-02-20
代码已经在生产环境中无缺陷地运行了21个月,并且已经处理了来自FTSE 250公司数百名用户的持续查询。
Slapper.Automapper
也非常适合将 .csv 文件直接映射到 POCO 列表。先将 .csv 文件读入 IDictionary 列表,然后将其直接映射到目标 POCO 列表。唯一的技巧是,您必须添加一个属性 int Id {get; set}
,并确保对于每一行它都是唯一的(否则 automapper 将无法区分行)。
2019-01-29 更新
微小的更新以添加更多代码注释。
请参阅:https://github.com/SlapperAutoMapper/Slapper.AutoMapper