这篇文章展示了如何查询highly normalised SQL database,并将结果映射到一组高度嵌套的 C# POCO 对象。
成分:
- 8 行 C#。
- 一些相当简单的 SQL 使用了一些连接。
- 两个很棒的库。
让我解决这个问题的洞察力是将MicroORM 与mapping the result back to the POCO Entities 分开。因此,我们使用两个独立的库:
本质上,我们使用Dapper 查询数据库,然后使用Slapper.Automapper 将结果直接映射到我们的POCO。
优势
-
简单。它的代码不到 8 行。我发现这更容易理解、调试和更改。
-
更少的代码。即使我们有一个复杂的嵌套 POCO(即 POCO 包含
List<MyClass1> 反过来又包含 List<MySubClass2> 等),几行代码就是 Slapper.Automapper 需要处理的所有内容。
-
速度。这两个库都具有大量的优化和缓存,使其运行速度几乎与手动调整的 ADO.NET 查询一样快。
-
关注点分离。我们可以将 MicroORM 更改为不同的,映射仍然有效,反之亦然。
-
灵活性。 Slapper.Automapper 处理任意嵌套的层次结构,它不限于几层嵌套。我们可以轻松地进行快速更改,并且一切正常。
-
调试。我们首先可以看到 SQL 查询工作正常,然后我们可以检查 SQL 查询结果是否正确映射回目标 POCO 实体。
-
易于使用 SQL 进行开发。我发现使用
inner joins 创建扁平化查询以返回扁平化结果比创建多个选择语句(在客户端进行拼接)要容易得多。
-
SQL 中的优化查询。在高度规范化的数据库中,创建平面查询允许 SQL 引擎对整体应用高级优化,如果构建和运行许多小的单个查询,这通常是不可能的。
-
信任。 Dapper 是 StackOverflow 的后端,而且,Randy Burden 有点像超级明星。还需要我说吗?
-
开发速度。我能够做一些非常复杂的查询,有很多层次的嵌套,而且开发时间很短。
-
更少的错误。我写过一次,它很有效,现在这项技术正在帮助一家 FTSE 公司。代码太少,不会出现意外行为。
缺点
-
返回超过 1,000,000 行。在返回 inner join 将其展平(这会带回重复),我们应该使用多个
select 语句和在客户端将所有内容重新拼接在一起(请参阅此页面上的其他答案)。
-
这种技术是面向查询的。我没有使用这种技术来写入数据库,但我确信 Dapper 完全有能力通过一些额外的工作来做到这一点,因为 StackOverflow 本身使用 Dapper 作为其数据访问层 (DAL)。
性能测试
在我的测试中,Slapper.Automapper 为 Dapper 返回的结果增加了一点开销,这意味着它仍然比 Entity Framework 快 10 倍,并且 组合仍然非常接近 SQL 的理论最大速度+ C# 能够。
在大多数实际情况下,大部分开销将出现在不太理想的 SQL 查询中,而不是在 C# 端对结果进行一些映射。
性能测试结果
总迭代次数:1000
-
Dapper by itself:每次查询 1.889 毫秒,使用 3 lines of code to return the dynamic。
-
Dapper + Slapper.Automapper:每次查询 2.463 毫秒,使用额外的 3 lines of code for the query + mapping from dynamic to POCO Entities。
工作示例
在这个例子中,我们有Contacts的列表,每个Contact可以有一个或多个phone numbers。
POCO 实体
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; } // foreign key
public string Number { get; set; }
}
SQL 表TestContact
SQL 表TestPhone
请注意,该表有一个外键ContactID,它引用TestContact 表(这对应于上面POCO 中的List<TestPhone>)。
产生平面结果的 SQL
在我们的 SQL 查询中,我们使用尽可能多的 JOIN 语句来获取我们需要的所有数据,在 flat, denormalized form 中。是的,这可能会在输出中产生重复,但是当我们使用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
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 = // -- Insert SQL connection string here.
using (var conn = new SqlConnection(connectionString))
{
conn.Open();
// Can set default database here with conn.ChangeDatabase(...)
{
// Step 1: Use Dapper to return the flat result as a Dynamic.
dynamic test = conn.Query<dynamic>(sql);
// Step 2: Use Slapper.Automapper for mapping to the POCO Entities.
// - IMPORTANT: Let Slapper.Automapper know how to do the mapping;
// let it know the primary key for each POCO.
// - Must also use underscore notation ("_") to name parameters in the SQL query;
// see Slapper.Automapper docs.
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);
}
}
}
}
输出
POCO 实体层次结构
查看 Visual Studio,我们可以看到 Slapper.Automapper 正确填充了我们的 POCO 实体,即我们有一个 List<TestContact>,每个 TestContact 都有一个 List<TestPhone>。
注意事项
Dapper 和 Slapper.Automapper 都在内部缓存所有内容以提高速度。如果您遇到内存问题(不太可能),请确保偶尔清除它们的缓存。
确保为返回的列命名,使用 underscore (_) notation 为 Slapper.Automapper 提供有关如何将结果映射到 POCO 实体的线索。
确保为每个 POCO 实体的主键提供 Slapper.Automapper 线索(请参阅行 Slapper.AutoMapper.Configuration.AddIdentifiers)。为此,您还可以在 POCO 上使用Attributes。如果你跳过这一步,那么它可能会出错(理论上),因为 Slapper.Automapper 不知道如何正确地进行映射。
2015-06-14 更新
成功地将这项技术应用于一个包含 40 多个规范化表的大型生产数据库。它完美地将具有超过 16 个 inner join 和 left join 的高级 SQL 查询映射到适当的 POCO 层次结构(具有 4 级嵌套)。查询速度非常快,几乎与在 ADO.NET 中手动编码一样快(查询通常需要 52 毫秒,从平面结果映射到 POCO 层次结构需要 50 毫秒)。这确实不是什么革命性的东西,但它在速度和易用性方面确实胜过 Entity Framework,尤其是当我们所做的只是运行查询时。
2016 年 2 月 19 日更新
代码已在生产环境中完美运行了 9 个月。最新版本的Slapper.Automapper 包含我为解决与 SQL 查询中返回的空值相关的问题而应用的所有更改。
2017 年 2 月 20 日更新
代码已在生产环境中完美运行 21 个月,并已处理来自 FTSE 250 公司数百名用户的持续查询。
Slapper.Automapper 也非常适合将 .csv 文件直接映射到 POCO 列表中。将 .csv 文件读入 IDictionary 列表,然后将其直接映射到 POCO 的目标列表中。唯一的技巧是您必须添加一个属性int Id {get; set},并确保它对于每一行都是唯一的(否则自动映射器将无法区分行)。
2019-01-29 更新
小幅更新以添加更多代码 cmets。
见:https://github.com/SlapperAutoMapper/Slapper.AutoMapper