5年后,我们为什么要从 Entity Framework 转到 Dapper 工具?
前言
时间退回到 2009-09-26,为了演示开源项目 FineUI 的使用方法,我们发布了 AppBox(通用权限管理框架,包括用户管理、职称管理、部门管理、角色管理、角色权限管理等模块),最初的 AppBox 采用 Subsonic 作为 ORM 工具。
遗憾的是,Subsonic后来逐渐不再维护,我们于 2013-08-28 正式从 Subsonic 转到 Entity Framework,最初对 Entity Framework 接触只能用两个字来形容:惊艳!整个 AppBox 项目没有写一行 SQL 代码,甚至没有打开 SQLServer 数据库,全部代码用 C# 来完成,EF CodeFirst小心翼翼的帮组我们完成了从数据库创建,访问,查询,更新,删除等一系列操作。
AppBox的详细介绍:https://www.cnblogs.com/sanshi/p/4030265.html
5 年来,我们一直在津津乐道 Entity Framework 带来的好处,也许是情人眼里出西施,对于它的缺点文过饰非,大可用一句话搪塞:你要完整学习 Entity Framework 知识体系,方能事半功倍,俗话说:磨刀不误砍柴工。
一般来说,新手的问题无外乎如下几点:
1. 数据库在哪?怎么没有数据库初始脚本?
2. 怎么又出错了?到底执行的SQL语句是啥?
3. 怎么支持 MySQL 数据库?为什么SQLServer正常的查询,到MySQL就出错了?
4. 为啥突然数据库都清空了?好恐怖,幸好不是在服务器
5. 性能怎么样?大家都说EF的性能不好
6. 能不能先建数据库,然后生成模型类?
.....
这些问题,有些是可以解决的,有些是对EF不了解遇到的,有些的确是EF自身的问题。
比如对 MYSQL 的支持不好,这个问题在简单的查询时正常,一遇到复杂的查询,总会遇到各种问题。而数据库被清空那个则是不了解EF的 Data Migration机制。性能倒不是大问题,只要合理的查询,加上EF的持续优化,性能应该还是可预期的。
即使一切的问题都可以归纳到没有好好学学,那 Entity Framework 总归还是有一个大问题:入门容易,而知识体系有点复杂,学习曲线会比较陡峭!
为什么要转到Dapper?
如果你认为上面就是我们转到 Dapper 的原因,那你算错了。5年的时间,我们已经对 Entity Framework 有了足够的了解和掌握,因此上面的问题都已不是问题。真正出现问题的不是 Entity Framework,而是我们,好吧,就明说了吧:我们太想念 SQL 语句了!
Entity Framework是一个有益的尝试,尝试向开发人员隐藏 SQL 语句,所有的数据库查询操作都通过面向对象的 C# 语言来完成,可以想象,从关系型数据库抽象为面向对象的语言,这个扭曲力场不可谓不强大,而这个扭曲力会带来两个极端:
1. 简单的操作会更加简单
2. 复杂的操作会更加复杂
哪些是简单的操作呢?
比如创建数据库:
Entity Framework CodeFirst开发模式允许我们只写模型类,程序会在第一次运行时创建数据库,比如一个简单的用户角色关系,通过模型类可以这么定义:
public class Role : IKeyID
{
[Key]
public int ID { get; set; } [Required, StringLength()]
public string Name { get; set; } [StringLength()]
public string Remark { get; set; } public virtual ICollection<User> Users { get; set; } }
public class User : IKeyID
{
[Key]
public int ID { get; set; } [Required, StringLength()]
public string Name { get; set; } [Required, StringLength()]
public string Email { get; set; } [Required, StringLength()]
public string Password { get; set; } public virtual ICollection<Role> Roles { get; set; } }
然后通过C#代码定义模型关联:
modelBuilder.Entity<Role>()
.HasMany(r => r.Users)
.WithMany(u => u.Roles)
.Map(x => x.ToTable("RoleUsers")
.MapLeftKey("RoleID")
.MapRightKey("UserID"));
这里是意思是:
1. 一个角色可以有多个用户(HasMany)
2. 一个用户可以有多个角色(WithMany)
3. 将这种关联关系保存到数据库表 RoleUsers,对于两个外键:RoleID和UserID
上面的代码如果在MySQL数据库中直接创建,熟悉SQL语句的会感觉更加亲切:
CREATE TABLE IF NOT EXISTS `roles` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(50) CHARACTER NOT NULL,
`Remark` varchar(500) CHARACTER DEFAULT NULL,
PRIMARY KEY (`ID`),
UNIQUE KEY `ID` (`ID`)
); CREATE TABLE IF NOT EXISTS `users` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(50) CHARACTER NOT NULL,
`Email` varchar(100) CHARACTER NOT NULL,
`Password` varchar(50) CHARACTER NOT NULL,
`Enabled` tinyint(1) NOT NULL,
PRIMARY KEY (`ID`),
UNIQUE KEY `ID` (`ID`)
); CREATE TABLE IF NOT EXISTS `roleusers` (
`RoleID` int(11) NOT NULL,
`UserID` int(11) NOT NULL,
PRIMARY KEY (`RoleID`,`UserID`),
KEY `Role_Users_Target` (`UserID`),
CONSTRAINT `Role_Users_Source` FOREIGN KEY (`RoleID`) REFERENCES `roles` (`id`) ON DELETE CASCADE,
CONSTRAINT `Role_Users_Target` FOREIGN KEY (`UserID`) REFERENCES `users` (`id`) ON DELETE CASCADE
);
在表 roleusers 中,创建了两个约束,分别是:
1. Role_Users_Source:定义外键 RoleID,关联 roles 表的 ID 列,并使用 ON DELETE CASCADE 定义级联删除,如果roles 表删除了一行数据,那么roleusers 中一行或多行关联数据会被删除
2. Role_Users_Target:定义外键 UserID,关联 users 表的 ID 列,同样定义级联删除规则
再比如简单的CRUD操作:
获取指定ID的角色:
DB.Roles.Find(id)
更新某个角色:
Role item = DB.Roles.Find(id);
item.Name = tbxName.Text.Trim();
item.Remark = tbxRemark.Text.Trim();
DB.SaveChanges();
删除某个角色:
DB.Roles.Where(r => r.ID == roleID).Delete();
获取某个角色下的用户数:
DB.Users.Where(u => u.Roles.Any(r => r.ID == roleID)).Count();
这个C#代码虽然看着简单,不是 Entity Framework 生成的SQL语句看起来却不是很友好:
SELECT
[GroupBy1].[A1] AS [C1]
FROM ( SELECT
COUNT(1) AS [A1]
FROM [dbo].[Users] AS [Extent1]
WHERE EXISTS (SELECT
1 AS [C1]
FROM [dbo].[RoleUsers] AS [Extent2]
WHERE ([Extent1].[ID] = [Extent2].[UserID]) AND ([Extent2].[RoleID] = @p__linq__0)
)
) AS [GroupBy1]
可能是考虑到 C# 代码可能会比较复杂,从通用性的角度出发,EF为一个简单的查询生成了包含 3 个 SELECT 的 SQL 查询语句。
如果仔细观察上面的SQL代码,有效的只是如下部分:
SELECT
COUNT(1)
FROM [dbo].[Users]
WHERE EXISTS (SELECT
1 AS [C1]
FROM [dbo].[RoleUsers]
WHERE ([Users].[ID] = [RoleUsers].[UserID]) AND ([RoleUsers].[RoleID] = @p__linq__0)
)
而这个SQL的外层SELECT其实是多余的,简化后的SQL代码是这样的:
SELECT
COUNT(*)
FROM [dbo].[RoleUsers]
WHERE ([Users].[ID] = [RoleUsers].[UserID]) AND ([RoleUsers].[RoleID] = @p__linq__0)
可见,为了完成需要的操作,Entity Framework为我们封装了多余的SQL代码,这让我们有点担心,且不说多余的两个SELECT会不会对性能有印象(这里可能没有,复杂的情况就不一定了),EF总给人一种雾里看花的感觉,因为最终还是要落实到SQL语句上来。
完成同样的操作,用 Dapper 可能要稍微多写点代码,但是 SQL 语句让人看着心里更有谱:
获取指定ID的角色:
conn.QuerySingleOrDefault<Role>("select * from roles where ID = @RoleID", new { RoleID = roleID });
更新某个角色:
Role item = GetCurrentRole(id);
item.Name = tbxName.Text.Trim();
item.Remark = tbxRemark.Text.Trim(); conn.Execute("update roles set Name = @Name, Remark = @Remark where ID = @ID", item);
删除某个角色:
conn.Execute("delete from roles where ID = @RoleID", new { RoleID = roleID });
获取某个角色下的用户数:
conn.QuerySingle<int>("select count(*) from roleusers where RoleID = @RoleID", new { RoleID = roleID });
哪些是复杂的操作呢?
因为数据库是关系型,Entity Framework偏偏要用面向对象的 C# 来操作,遇到级联关系的更新时,EF就会变得有点复杂。
比如从某个角色中删除多个用户:
在 Entity Framework中,我们需要先获取这个角色以及属于这个角色的用户,然后才能执行删除操作。
int roleID = GetSelectedDataKeyID(Grid1);
List<int> userIDs = GetSelectedDataKeyIDs(Grid2); Role role = DB.Roles.Include(r => r.Users)
.Where(r => r.ID == roleID)
.FirstOrDefault(); foreach (int userID in userIDs)
{
User user = role.Users.Where(u => u.ID == userID).FirstOrDefault();
if (user != null)
{
role.Users.Remove(user);
}
} DB.SaveChanges();
从代码逻辑上讲,这个代码片段是很直观的:
1. 首先获取当前角色,由于后面要操作角色的用户列表,所以使用 Include 语句,这将导致生成SQL查询语句有点复杂:
SELECT
[Project2].[ID] AS [ID],
[Project2].[Name] AS [Name],
[Project2].[Remark] AS [Remark],
[Project2].[C1] AS [C1],
[Project2].[ID1] AS [ID1],
[Project2].[Name1] AS [Name1],
FROM ( SELECT
[Limit1].[ID] AS [ID],
[Limit1].[Name] AS [Name],
[Limit1].[Remark] AS [Remark],
[Join1].[ID] AS [ID1],
[Join1].[Name] AS [Name1],
CASE WHEN ([Join1].[RoleID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
FROM (SELECT TOP (1)
[Extent1].[ID] AS [ID],
[Extent1].[Name] AS [Name],
[Extent1].[Remark] AS [Remark]
FROM [dbo].[Roles] AS [Extent1]
WHERE [Extent1].[ID] = @p__linq__0 ) AS [Limit1]
LEFT OUTER JOIN (SELECT [Extent2].[RoleID] AS [RoleID], [Extent3].[ID] AS [ID], [Extent3].[Name] AS [Name]
FROM [dbo].[RoleUsers] AS [Extent2]
INNER JOIN [dbo].[Users] AS [Extent3] ON [Extent3].[ID] = [Extent2].[UserID] ) AS [Join1] ON [Limit1].[ID] = [Join1].[RoleID]
) AS [Project2]
ORDER BY [Project2].[ID] ASC, [Project2].[C1] ASC
2. 遍历需要删除的用户列表,并从当前角色的用户列表中删除,这将执行多个SQL语句:
exec sp_executesql N'DELETE [dbo].[RoleUsers]
WHERE (([RoleID] = @0) AND ([UserID] = @1))',N'@0 int,@1 int',@0=3,@1=45
go
exec sp_executesql N'DELETE [dbo].[RoleUsers]
WHERE (([RoleID] = @0) AND ([UserID] = @1))',N'@0 int,@1 int',@0=3,@1=46
go
exec sp_executesql N'DELETE [dbo].[RoleUsers]
WHERE (([RoleID] = @0) AND ([UserID] = @1))',N'@0 int,@1 int',@0=3,@1=47
go
。。。。。
上面的C#代码以及生成的SQL语句之所以这么复杂,归根到底是因为 Entity Framework 企图使用面向对象的方式操作关系型数据库,换句话说:模型类对数据库的 RoleUsers 表是一无所知的。
而使用 Dapper 代码,代码非常简单,因为我们可以直接操作 roleusers 表:
int roleID = GetSelectedDataKeyID(Grid1);
List<int> userIDs = GetSelectedDataKeyIDs(Grid2); conn.Execute("delete from roleusers where RoleID = @RoleID and UserID in @UserIDs", new { RoleID = roleID, UserIDs = userIDs });
再比如更新某个用户的角色列表:
在 Entity Framework中,我们需要先获取这个用户以及属于这个用户的角色,然后才能执行替换操作。
User item = DB.Users
.Include(u => u.Roles)
.Where(u => u.ID == id).FirstOrDefault(); int[] roleIDs = StringUtil.GetIntArrayFromString(hfSelectedRole.Text);
ReplaceEntities<Role>(item.Roles, roleIDs); DB.SaveChanges();
而 ReplaceEntities 是我们自定义的一个帮助函数:
protected void ReplaceEntities<T>(ICollection<T> existEntities, int[] newEntityIDs) where T : class, IKeyID, new()
{
if (newEntityIDs.Length == )
{
existEntities.Clear();
}
else
{
int[] tobeAdded = newEntityIDs.Except(existEntities.Select(x => x.ID)).ToArray();
int[] tobeRemoved = existEntities.Select(x => x.ID).Except(newEntityIDs).ToArray(); AddEntities<T>(existEntities, tobeAdded); existEntities.Where(x => tobeRemoved.Contains(x.ID)).ToList().ForEach(e => existEntities.Remove(e));
}
}
由于 Entity Framework 明确知道了删除哪些角色,以及添加哪些角色,所以会生成多条插入删除SQL语句,类似:
exec sp_executesql N'DELETE [dbo].[RoleUsers]
WHERE (([RoleID] = @) AND ([UserID] = @))',N'@ int,@ int',@0=3,@1=50
go
exec sp_executesql N'DELETE [dbo].[RoleUsers]
WHERE (([RoleID] = @) AND ([UserID] = @))',N'@ int,@ int',@0=23,@1=50
go
exec sp_executesql N'DELETE [dbo].[RoleUsers]
WHERE (([RoleID] = @) AND ([UserID] = @))',N'@ int,@ int',@0=33,@1=50
go
exec sp_executesql N'INSERT [dbo].[RoleUsers]([RoleID], [UserID])
VALUES (@, @)
',N'@ int,@ int',@0=4,@1=50
go
exec sp_executesql N'INSERT [dbo].[RoleUsers]([RoleID], [UserID])
VALUES (@, @)
',N'@ int,@ int',@0=6,@1=50
go
exec sp_executesql N'INSERT [dbo].[RoleUsers]([RoleID], [UserID])
VALUES (@, @)
',N'@ int,@ int',@0=7,@1=50
go
。。。。。。
而使用Dapper更加简单,我们无需知道此用户有哪些角色,可以直接操作 roleusers 数据库:
User item = DB.Users
.Include(u => u.Roles)
.Where(u => u.ID == id).FirstOrDefault(); int[] roleIDs = StringUtil.GetIntArrayFromString(hfSelectedRole.Text); conn.Execute("delete from roleusers where UserID = @UserID", new { UserID = userID });
conn.Execute("insert roleusers (UserID, RoleID) values (@UserID, @RoleID)", roleIDs.Select(u => new { UserID = userID, RoleID = u }).ToList());
这里的操作更加简单粗暴,一把删除用户的所有角色,然后再全部添加进去。
小结
从 Entity Framework 转到 Dapper,无关语言,无关性能,无关偏见。只因为心中对 SQL 语句的思念,对确定性和可掌握性的追求,当然也是为了更多代码量的简洁,多数据库的平等支持,以及未来更多调优的可能。
不可否认,Entity Framework作为一个极致(Duan)的封装,有他的受众和优点。但是,我更喜欢 Dapper 的简洁和 SQL 语句的确定性。
后记
1. 文中提到的 AppBox 不是免费软件,如果需要了解更多详情,请加入【三石和他的朋友们】知识星球下载源代码:http://fineui.com/fans/
2. 取决于本篇博文的受欢迎程度,我可能会写一个续篇,包含更多的升级细节和Dapper的使用技巧:
- 批量更新数据
- 分页与排序的简单封装
- 插入与更新的简单封装
- 事务(Transaction)
- 插入后返回自增ID
- 动态创建匿名参数
- 子查询
- 多结果映射
最后,放几张系统的截图:








【续】5年后,我们为什么要从 Entity Framework 转到 Dapper 工具?
5年后,我们为什么要从 Entity Framework 转到 Dapper 工具?的更多相关文章
- 【续】5年后,我们为什么要从 Entity Framework 转到 Dapper 工具?
前言 上一篇文章收获了 140 多条评论,这是我们始料未及的. 向来有争议的话题都是公说公的理,婆说婆的理,Entity Framework的爱好者对此可以说是嗤之以鼻,不屑一顾,而Dapper爱好者 ...
- Entity Framework Code First学习系列目录
Entity Framework Code First学习系列说明:开发环境为Visual Studio 2010 + Entity Framework 5.0+MS SQL Server 2012, ...
- Entity Framework Code First数据库连接
1. 安装Entity Framework 使用NuGet安装Entity Framework程序包:工具->库程序包管理器->程序包管理器控制台,执行以下语句: PM> Insta ...
- 关于Entity Framework使用的简单例子
一.创建Code First模型 1.创建工程,这里我使用的是以.NET 4.0为目标的实体Web应用程序 2.安装Entity Framework 确保已安装NuGet,选择NuGet套件管理员&g ...
- Entity Framework Code First学习系列
Entity Framework Code First学习系列目录 Entity Framework Code First学习系列说明:开发环境为Visual Studio 2010 + Entity ...
- 【转】MVC Model建模及Entity Framework Power Tool使用
MVC如使用Code-First代码优先约定,先建实体类,再根据实体类创建数据库. 在创建实体类后,新建一个数据上下文类,如下: publicclassMusicStoreDB : DbContext ...
- vs2012配置使用entity framework 6
项目中使用mysql作为数据库,想快速地实现一些数据服务,为了节省开发时间,提升开发效率,性能不是考虑的重点,所以选择了使用ORM框架:Entity Framework.指定了DB的table des ...
- Entity Framework Code First数据库连接 转载 https://www.cnblogs.com/libingql/p/3351275.html
Entity Framework Code First数据库连接 1. 安装Entity Framework 使用NuGet安装Entity Framework程序包:工具->库程序包管理器 ...
- 使用NuGet助您玩转代码生成数据————Entity Framework 之 Code First
[前言] 如果是Code First老鸟或者对Entity Framework不感兴趣,就不用浪费时间往下看了. 记得09年第一次接触ORM————Linq2Sql,从此对她的爱便一发不可收拾,一年后 ...
随机推荐
- gulp源码解析(一)—— Stream详解
作为前端,我们常常会和 Stream 有着频繁的接触.比如使用 gulp 对项目进行构建的时候,我们会使用 gulp.src 接口将匹配到的文件转为 stream(流)的形式,再通过 .pipe() ...
- 【深度学习篇】--Seq2Seq模型从初识到应用
一.前述 架构: 问题: 1.压缩会损失信息 2.长度会影响准确率 解决办法: Attention机制:聚焦模式 “高分辨率”聚焦在图片的某个特定区域并以“低分辨率”,感知图像的周边区域的模式.通过大 ...
- Kubernetes的污点和容忍(下篇)
背景 继上一篇<Kubernetes的污点和容忍(上篇)>,这是https://kubernetes.io/docs/concepts/configuration/taint-and-to ...
- 《HelloGitHub》第 31 期
公告 网站新增了 Web 服务器排行榜.数据库排行榜 <HelloGitHub>第 31 期 兴趣是最好的老师,HelloGitHub 就是帮你找到兴趣! 简介 分享 GitHub 上有趣 ...
- JavaScript一看就懂(3)数组
定义数组 var a = [1, 2, 3]; typeof a; //"object", 数组是对象 a.length; //数组长度 相关操作 a[0]; //下标访问 a.p ...
- Python:黑板课爬虫闯关第四关
第四关地址:http://www.heibanke.com/lesson/crawler_ex03/ 一开始看到的时候有点蒙,不知道啥意思,说密码需要找出来但也没说怎么找啊. 别急,随便输了个昵称和密 ...
- 并发系列(3)之 CLH、MCS 队列锁简介
这篇博客主要是作为 AbstractQueuedSynchronizer 的背景知识介绍:平时接触也非常的少,如果你不感兴趣可以跳过:但是了解一下能更加的清楚 AQS 的设计思路: 一.自旋锁简介 通 ...
- SLAM+语音机器人DIY系列:(二)ROS入门——3.在ubuntu16.04中安装ROS kinetic
摘要 ROS机器人操作系统在机器人应用领域很流行,依托代码开源和模块间协作等特性,给机器人开发者带来了很大的方便.我们的机器人“miiboo”中的大部分程序也采用ROS进行开发,所以本文就重点对ROS ...
- SLAM+语音机器人DIY系列:(四)差分底盘设计——1.stm32主控硬件设计
摘要 运动底盘是移动机器人的重要组成部分,不像激光雷达.IMU.麦克风.音响.摄像头这些通用部件可以直接买到,很难买到通用的底盘.一方面是因为底盘的尺寸结构和参数是要与具体机器人匹配的:另一方面是因为 ...
- Lumen框架-错误&日志
介绍 当你开始一个新的Lumen项目的时候,错误和异常功能,已经在框架中注入了.此外,Lumen还集成了Monolog日志函数,支持和提供多种强大的日志处理功能. 配置 错误详情 大量的错误信息在你的 ...