写点什么

使用实体框架、Dapper 和 Chain 的仓储模式实现策略

2016 年 11 月 06 日

关键要点:

  • Dapper 这类微 ORM(Micro-ORM)虽然提供了最好的性能,但也需要去做最多的工作。
  • 在无需复杂对象图时,Chain 这类 Fluent ORM 更易于使用。
  • 对实体框架(Entity Framework)做大量的工作后,其性能可显著提高。
  • 为获得数据库的最大性能,需要采用可能会有些繁琐的投影(Projection)操作。
  • ORM 整体上的局部更新可能会存在问题。

在现代企业开发中,可采用多种方法构建数据存取层(data access layer ,DAL)。使用 C#做开发时,DAL 的最底层几乎总是使用 ADO.NET。但这时常会形成一个笨重的库,所以通常会在 DAL 的底层之上再部署一个 ORM 层。为允许模拟和隐藏 ORM 的细节,整个 DAL 包装在存储内。

在这一系列的文章中,我们将审视三种使用不同类型 ORM 构建仓储模式的方法,分别是:

  • 实体框架:一种传统的“全特性”或“OOP”类型的 ORM。
  • Dapper :一种主要专注结果集映射的轻量级微 ORM。
  • Tortuga Chain :一种基于函数式编程理念的 Fluent ORM。

本文将侧重于开发人员可在典型仓储中用到的那些基本功能。在本系列文章的第二部分,我们将着眼于那些开发人员基于实际情况而实现的高级技术。

插入(Insert)操作

对于任何 CRUD 操作集,通常会首先实现基本的插入操作,进而可用插入操作对其它的操作进行测试。

Chain

Chain 使用列名和属性名间的运行时匹配。对于在数据库中并不存在的对象,除非启用了严格模式(strict model),否则将忽略该对象上的属性。类似地,没有匹配属性的列不能成为生成 SQL 的组成部分。

复制代码
public int Insert(Employee employee)
{
return m_DataSource.Insert("HR.Employee", employee).ToInt32().Execute();
}

Dapper

没有第三方扩展时,Dapper 需要编程人员手工指定所需的 SQL,其中包括了特定于数据库的逻辑,用于返回新创建的主键。

复制代码
public int Insert(Employee employee)
{
const string sql = @"INSERT INTO HR.Employee
(FirstName,
MiddleName,
LastName,
Title,
ManagerKey,
OfficePhone,
CellPhone
)
VALUES (@FirstName,
@MiddleName,
@LastName,
@Title,
@ManagerKey,
@OfficePhone,
@CellPhone
);
{1}
SELECT SCOPE_IDENTITY()
";
using (var con = new SqlConnection(m_ConnectionString))
{
con.Open();
return con.ExecuteScalar<int>(sql, employee);
}
}

实体框架

实体框架使用编译阶段映射在运行时生成 SQL。需将任何没有匹配列的属性标记为 NotMapped,否则将会产生错误。

复制代码
public int Insert(Employee employee)
{
using (var context = new CodeFirstModels())
{
context.Employees.Add(employee);
context.SaveChanges();
return employee.EmployeeKey;
}
}

更新(Update)操作

Chain

Chain 缺省使用数据库中所定义的主键。但是在设置了适当的插入选项后,它将在模型中使用 Key 属性。

复制代码
public void Update(Employee employee)
{
m_DataSource.Update("HR.Employee", employee).Execute();
}

Dapper

与插入操作一样,纯 Dapper 需用户手工编写必要的 SQL 语句。

复制代码
public void Update(Employee employee)
{
const string sql = @"UPDATE HR.Employee
SET FirstName = @FirstName,
MiddleName = @MiddleName,
LastName = @LastName,
Title = @Title,
ManagerKey = @ManagerKey,
OfficePhone = @OfficePhone,
CellPhone = @CellPhone
WHERE EmployeeKey = @EmployeeKey
";
using (var con = new SqlConnection(m_ConnectionString))
{
con.Open();
con.Execute(sql, employee);
}
}

实体框架(初学者)

实体框架为 UPDATE 语句查找 Key 属性,以生成 WHERE 语句。

复制代码
public void Update(Employee employee)
{
using (var context = new CodeFirstModels())
{
var entity = context.Employees.Where(e => e.EmployeeKey == employee.EmployeeKey).First();
entity.CellPhone = employee.CellPhone;
entity.FirstName = employee.FirstName;
entity.LastName = employee.LastName;
entity.ManagerKey = employee.ManagerKey;
entity.MiddleName = employee.MiddleName;
entity.OfficePhone = employee.OfficePhone;
entity.Title = employee.Title;
context.SaveChanges();
}
}

实体框架(中级用户)

使用实体框架时,初学者常会在执行更新操作上犯错误。将实体添加到上下文中很容易就能实现它,而这种模式应成为中级使用者的常识。这里给出使用实体状态“Modified”修正后的例子。

复制代码
public void Update(Employee employee)
{
using (var context = new CodeFirstModels())
{
context.Entry(employee).State = EntityState.Modified;
context.SaveChanges();
}
}

读取全部(Read All)操作

读取全部操作在实体框架和 Chain 中是十分相似的,不同之处在于在实体框架中实现需要编写更多行的代码,而在 Chain 中实现需要编写更长的代码行。

Dapper 当然是最为繁琐的,因为它需要未经加工的 SQL 语句。即使如此,仍可以通过使用 SELECT * 语句替代手工地指定列名而在一定程度上降低 Dapper 的开销。这在存在返回额外数据的风险的情况下,降低了出现类与 SQL 语句不匹配的可能性。

Chain

在 Chain 中,ToObject 连接生成一系列所需的列。通过匹配所需列表与可用列的列表,From 连接生成 SQL 语句。

复制代码
public IList<Employee> GetAll()
{
return m_DataSource.From("HR.Employee").ToCollection<Employee>().Execute();
}

Dapper

Dapper 是最为繁琐的,因为它需要原始未经加工的 SQL 语句。虽然这令人皱眉头,但仍可以通过使用 SELECT * 语句替代手工地指定列名而在一定程度上降低 Dapper 的开销,这样是不太可能漏掉列的,虽然存在返回额外数据的风险。

复制代码
public IList<Employee> GetAll()
{
using (var con = new SqlConnection(m_ConnectionString))
{
con.Open();
return con.Query<Employee>("SELECT e.EmployeeKey, e.FirstName, e.MiddleName, e.LastName, e.Title, e.ManagerKey, e.OfficePhone, e.CellPhone, e.CreatedDate FROM HR.Employee e").AsList();
}
}
{1}

实体框架

像以前一样,实体框架使用编译期信息确定如何生成 SQL 语句。

复制代码
public IList<Employee> GetAll()
{
using (var context = new CodeFirstModels())
{
return context.Employees.ToList();
}
}

按标识符获取(Get by Id)操作

需要注意的是,随每个例子的语法稍作修改就可表明只返回一个对象。同样的基本过滤技术可用于返回多个对象。

Chain

Chain 严重依赖于“过滤对象”。这些对象直接被转义成参数化的 WHERE 语句,语句中的每个属性间具有“AND”操作符。

复制代码
public Employee Get(int employeeKey)
{
return m_DataSource.From("HR.Employee", new { @EmployeeKey = employeeKey }).ToObject<Employee>().Execute();
}

Chain 也允许用参数化的字符串表示 WHERE 语句,虽然这个功能很少被用到。

如果主键是标量,即主键中只有一列,那么可使用简化的语法。

复制代码
public Employee Get(int employeeKey)
{
return m_DataSource.GetByKey("HR.Employee", employeeKey).ToObject<Employee>().Execute();
}

Dapper

下例中,可以看到 Dapper 手工指定了 SQL 语句。该语句与 Chain 和实体框架所生成的 SQL 语句在本质上是一致的。

复制代码
using (var con = new SqlConnection(m_ConnectionString))
{
con.Open();
return con.Query<Employee>("SELECT e.EmployeeKey, e.FirstName, e.MiddleName, e.LastName, e.Title, e.ManagerKey, e.OfficePhone, e.CellPhone, e.CreatedDate FROM HR.Employee e WHERE e.EmployeeKey = @EmployeeKey", new { @EmployeeKey = employeeKey }).First();
}
{1}

实体框架

实体框架将表名和首个 ToList 或 First 操作间的所有内容看作为一个表达式树。在运行时评估该树以生成 SQL 语句。

复制代码
public Employee Get(int employeeKey)
{
using (var context = new CodeFirstModels())
{
return context.Employees.Where(e => e.EmployeeKey == employeeKey).First();
}
}

删除(Delete)操作

Chain

Chain 期待包括主键的参数对象。而参数对象中的其它特性将被忽略(该语法不支持批量删除)。

复制代码
public void Delete(int employeeKey)
{
m_DataSource.Delete("HR.Employee", new { @EmployeeKey = employeeKey }).Execute();
}

如果有标量主键,可使用简化的语法。

复制代码
public void Delete(int employeeKey)
{
m_DataSource.DeleteByKey("HR.Employee", employeeKey).Execute();
}

Dapper

复制代码
public void Delete(int employeeKey)
{
using (var con = new SqlConnection(m_ConnectionString))
{
con.Open();
con.Execute("DELETE FROM HR.Employee WHERE EmployeeKey = @EmployeeKey", new { @EmployeeKey = employeeKey });
}
}

实体框架(初学者)

初学者一般会取回一个记录然后迅速删除,丢弃所有返回的信息。

复制代码
public void Delete(int employeeKey)
{
using (var context = new CodeFirstModels())
{
var employee = context.Employees.Where(e => e.EmployeeKey == employeeKey).First();
context.Employees.Remove(employee);
context.SaveChanges();
}
}

实体框架(中级用户)

可使用内嵌 SQL 避免数据库的往返交互操作。

复制代码
public void Delete(int employeeKey)
{
using (var context = new CodeFirstModels())
{
context.Database.ExecuteSqlCommand("DELETE FROM HR.Employee WHERE EmployeeKey = @p0", employeeKey);
}
}

投影(Projection)操作

投影是中间层开发中的一个重要部分。在取回了比实际所需更多的数据时,数据库常会完全失去使用覆盖索引或索引的能力,这将导致严重的性能影响。

Chain

同上,Chain 将仅选取指定对象类型所需的所有列。

复制代码
public IList<EmployeeOfficePhone> GetOfficePhoneNumbers()
{
return m_DataSource.From("HR.Employee").ToCollection<EmployeeOfficePhone>().Execute();
}

Dapper

鉴于 Dapper 是显式的,所以是由开发人员确保只选取必需的列。

复制代码
public IList<EmployeeOfficePhone> GetOfficePhoneNumbers()
{
using (var con = new SqlConnection(m_ConnectionString))
{
con.Open();
return con.Query<EmployeeOfficePhone>("SELECT e.EmployeeKey, e.FirstName, e.LastName, e.OfficePhone FROM HR.Employee e").AsList();
}
}

实体框架

实体框架需要额外的操作步骤,这些步骤常因为有些繁琐而被忽视。

通过在调用 ToList 前就包括了额外的选择语句,实体架构可生成正确的 SQL 语句,并避免从数据库返回过多的信息。

复制代码
public IList<EmployeeOfficePhone> GetOfficePhoneNumbers()
{
using (var context = new CodeFirstModels())
{
return context.Employees.Select(e => new EmployeeOfficePhone()
{
EmployeeKey = e.EmployeeKey,
FirstName = e.FirstName,
LastName = e.LastName,
OfficePhone = e.OfficePhone
}).ToList();
}
}

使用投影做更新操作

固然,在存在投影对象时直接从投影对象更新数据库是一种好的方法。该方法在 Chain 和 Dapper 的基本模式中是天然存在的。而在实体框架中,则必须要在手工拷贝属性和编写 Dapper 风格的内嵌 SQL 这两种方法间做出选择。

Chain

注意,任何未在投影类上具有匹配属性的列将不受到影响。

复制代码
public void Update(EmployeeOfficePhone employee)
{
return m_DataSource.Update("HR.Employee", employee).Execute();
}

Dapper

复制代码
public void Update(EmployeeOfficePhone employee)
{
const string sql = @"UPDATE HR.Employee
SET FirstName = @FirstName,
LastName = @LastName,
OfficePhone = @OfficePhone
WHERE EmployeeKey = @EmployeeKey
";
using (var con = new SqlConnection(m_ConnectionString))
{
con.Open();
con.Execute(sql, employee);
}
}

实体框架

复制代码
public void Update(EmployeeOfficePhone employee)
{
using (var context = new CodeFirstModels())
{
var entity = context.Employees.Where(e => e.EmployeeKey == employee.EmployeeKey).First();
entity.FirstName = employee.FirstName;
entity.LastName = employee.LastName;
entity.OfficePhone = employee.OfficePhone;
context.SaveChanges();
}
}

反射插入(Reflexive Insert)

现在我们来看一些更有意思的用例。反射插入意味着返回被插入的对象。做反射插入通常是为了获得默认的和计算的域。

模型

注意,实体框架和 Chain 需要对属性进行注释,这样库才会知道该域将由数据库予以设置。

复制代码
[DatabaseGenerated(DatabaseGeneratedOption.Computed)] //Needed by EF
[IgnoreOnInsert, IgnoreOnUpdate] //Needed by Chain
public DateTime? CreatedDate { get; set; }

Chain

Chain 允许将 ToObject 附加到任何插入或更新操作上。

复制代码
public Employee InsertAndReturn(Employee employee)
{
return m_DataSource.Insert("HR.Employee", employee).ToObject<Employee>().Execute();
}

Dapper

使用 Dapper 的反射插入,可以使用特定于数据库的功能实现,例如 OUTPUT 语句。

复制代码
public Employee InsertAndReturn(Employee employee)
{
const string sql = @"INSERT INTO HR.Employee
(FirstName,
MiddleName,
LastName,
Title,
ManagerKey,
OfficePhone,
CellPhone
)
OUTPUT
Inserted.EmployeeKey,
Inserted.FirstName,
Inserted.MiddleName,
Inserted.LastName,
Inserted.Title,
Inserted.ManagerKey,
Inserted.OfficePhone,
Inserted.CellPhone,
Inserted.CreatedDate
VALUES (@FirstName,
@MiddleName,
@LastName,
@Title,
@ManagerKey,
@OfficePhone,
@CellPhone
);";
using (var con = new SqlConnection(m_ConnectionString))
{
con.Open();
return con.Query<Employee>(sql, employee).First();
}
}

如果一并考虑初学者级别模式,更典型的做法是仅在 Get 方法之后调用 Insert 方法。

复制代码
public Employee InsertAndReturn_Novice(Employee employee)
{
return Get(Insert(employee));
}

实体框架

使用前面提及的 DatabaseGenerated 属性,你可以插入一个新的实体并读回它的计算的和 / 或默认的列。

复制代码
public Employee InsertAndReturn(Employee employee)
{
using (var context = new CodeFirstModels())
{
context.Employees.Add(employee);
context.SaveChanges();
return employee;
}
}

受限更新 / 局部更新

有时应用并没有打算对每个列做更新,尤其是当模型是直接源自于 UI 并可能混合了可更新域和不可更新域时。

Chain

在 Chain 中,使用 IgnoreOnInsert 和 IgnoreOnUpdate 属性去限制插入和更新操作。为允许用数据库作为默认取值,典型的做法是将这两个属性都置于 CreatedDate 类型的列中。为避免更新操作过程中的意外改变,通常将 IgnoreOnUpdate 属性置于 CreatedBy 之类的列上。

Dapper

就显式编写的插入和更新语句而言,Dapper 最具灵活性。

实体框架

除了计算列(列值为表达式),实体框架并未给出一种简单的方法可声明某一列不参与插入或删除操作,但可使用更新操作的“读 - 拷贝 - 写”(read-copy-write)模式模拟该行为。

更新或插入(Upsert)操作

经常需要作为一个单一操作完成记录的插入或者更新,尤其是在使用自然主键(natural key)时。

Chain

在 Chain 中,Upsert 操作的实现使用了与插入和删除相同的设计。所生成的 SQL 随数据库引擎不同而各异(例如:SQL Server 使用了 MERGE,SQLit 使用了一系列语句)。

复制代码
public int Upsert(Employee employee)
{
return m_DataSource.Upsert("HR.Employee", employee).ToInt32().Execute();
}

Dapper

在 Dapper 中,Upsert 操作的实现需要多轮的来回交互,或是需要比较复杂的特定于数据库的 SQL 语句。本文对此不作阐述。

实体框架

在实体框架中,这(过程?函数?都可以用“这”指代)仅作为被改进的更新操作的一个变体。

复制代码
public int Upsert(Employee employee)
{
using (var context = new CodeFirstModels())
{
if(employee.EmployeeKey == 0)
context.Entry(employee).State = EntityState.Added;
else
context.Entry(employee).State = EntityState.Modified;
context.SaveChanges();
return employee.EmployeeKey;
}
}

性能

虽然本文所采用的主要基准测试是代码量和易用性,但是对实际性能的考虑也是非常有用的。

所有的性能基准测试中都包括了预热过程,其后是对主循环做 1000 次迭代操作。每次测试中都使用了同样的模型,模型使用实体框架的代码优先(Code First)技术从数据库代码生成器产生。所有迭代都相当于共计 13 个基本 CRUD 操作,其中包括创建、读取、更新和删除操作。

我要澄清的是,这里所做的仅是一些粗略的测试,使用了任何人在刚开始接触这些库时通常就会看到的代码类型。当然一些高级技术可以改进每个测试的性能,有时甚至是极大地改进。

BenchmarkDotNet 计时

  • Chain:平均 3.4160 毫秒,标准偏差为 0.2764 毫秒;
  • 未使用经编译的物化器(Compiled Materializers)的 Chain:平均 3.0955 毫秒,标准偏差 0.1391 毫秒;
  • Dapper:平均 2.7250 毫秒,标准偏差 0.1840 毫秒;
  • 实体框架(初学者):平均 13.1078 毫秒,标准偏差 0.4649 毫秒;
  • 实体框架(中级用户):平均 10.11498 毫秒,标准偏差 0.1952 毫秒;
  • 实体框架(未使用 AsNoTracking 的中级用户):平均 9.7290 毫秒,标准偏差 0.3281 毫秒。

结论

虽然可使用任何 ORM 框架去实现基本的仓储模式,但是各种实现的性能和所需的代码量具有显著的差异。选取实现方式时需要对这些因素进行平衡,此外还需考虑数据库可移植性、跨平台支持和开发人员经验等。

在该系列文章的第二部分,我们将着眼于那些不仅将仓储模式作为瘦抽象层的高级用例。

你可以在 GitHub 上获取本文的代码。

关于作者

Jonathan Allen的首份工作是在上世纪九十年代末做诊所的 MIS 项目,Allen 将项目逐步由 Access 和 Excel 升级到企业级的解决方法。在从事为财政部门编写自动交易系统代码的工作五年之后,他成为项目顾问,参与了包括机器人仓库 UI、癌症研究软件中间层、主要房地产保险企业的大数据需求等在内的各种行业项目。在闲暇时间,他喜欢研究源于 16 世纪的武术,并为其撰写文章。

查看英文原文: Implementation Strategies for the Repository Pattern with Entity Framework, Dapper, and Chain


感谢冬雨对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016 年 11 月 06 日 18:343836
用户头像

发布了 226 篇内容, 共 59.5 次阅读, 收获喜欢 14 次。

关注

评论

发布
暂无评论
发现更多内容

你竞争我得利之零售变革

孙苏勇

行业资讯

业务信息化操作系统(BIOS)——中台的核心产出物

孤岛旭日

中台 操作系统 企业信息化

高仿瑞幸小程序 08 创建第一个云函数

曾伟@喵先森

小程序 微信小程序 前端 移动

猿灯塔-Phaser 使用介绍

猿灯塔

初探Electron,从入门到实践

Geek_Willie

前端 Electron SpreadJS

《硅谷革命:成就苹果公司的疯狂往事》读后感

顾强

需求是被挖掘还是被创造出来的?

Neco.W

产品 互联网 需求

回“疫”录(15):在家SOHO,是你想要的工作方式吗?

小天同学

疫情 回忆录 现实纪录 纪实 远程办公

21天养不成习惯,28天也不行。不要痴心妄想。

赵新龙

TGO鲲鹏会 习惯养成

Dubbo集成Sentinel实现限流

Java收录阁

sentinel

Spring 中不同依赖注入方式的对比与剖析

Deecyn

spring

认识数据产品经理(三 成为数据产品经理)

马踏飞机747

大数据 数据中台 数据分析 产品经理

面向页面的移动端架构设计

稻子

flutter ios android 前端架构 架构模式

一杯茶的时间,上手 React 框架开发

图雀社区

Reac

终于有一款组件可以全面超越Apache POI

Geek_Willie

前后端分离 服务端 GrapeCity Documents

故障的传播方式与隔离办法

Wales Kuo

爱是恒久忍耐,又有恩慈

泰稳@极客邦科技

身心健康 心理

高效阅读,成就自我-《麦肯锡精英高效阅读法》读后感

顾强

读书笔记 读书 读书方式

有了容器为什么kubernetes还需要Pod?

架构师修行之路

Kubernetes 分布式 云原生 pod

游戏夜读 | 关卡设计为什么难?

game1night

油管博主路透 3080Ti 参数、黄教主烤箱中拿出 DGX A100 预热发布会

神经星星

人工智能 互联网巨头 gpu 互联网 英伟达

借助第一性原理开启中台建设

数字圣杯

数据中台 数字化转型

反对996,但是选择996是一个怎样的矛盾心态?

顾强

职场 加班

编程的门槛 - 抄作业的得与失

顿晓

编程门槛 编程思维 动手能力 抄作业

编写制度的几点实用建议

石君

制度 编写制度 安全管理

Flink Weekly | 每周社区动态更新

Apache Flink

大数据 flink 流计算 实时计算 大数据处理

回顾经典,Netflix的推荐系统架构

王喆

人工智能 学习 推荐系统 netflix

“字节”不断“跳动”,卡拉永远 OK?

无量靠谱

字节跳动 诺基亚 危机

从波音747学项目管理

顾强

项目管理 读书感悟 沟通

前浪的经验:区块链软件,一定也要去中心化

Michael Yuan

比特币 区块链 智能合约 以太坊 加密货币

如何快速更改qcow2镜像文件

奔跑的菜鸟

云计算

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

使用实体框架、Dapper和Chain的仓储模式实现策略-InfoQ