時間退回到 2009-09-26,爲了演示開源項目 FineUI 的使用方法,咱們發佈了 AppBox(通用權限管理框架,包括用戶管理、職稱管理、部門管理、角色管理、角色權限管理等模塊),最初的 AppBox 採用 Subsonic 做爲 ORM 工具。html
遺憾的是,Subsonic後來逐漸再也不維護,咱們於 2013-08-28 正式從 Subsonic 轉到 Entity Framework,最初對 Entity Framework 接觸只能用兩個字來形容:驚豔!整個 AppBox 項目沒有寫一行 SQL 代碼,甚至沒有打開 SQLServer 數據庫,所有代碼用 C# 來完成,EF CodeFirst當心翼翼的幫組咱們完成了從數據庫建立,訪問,查詢,更新,刪除等一系列操做。sql
AppBox的詳細介紹:http://www.javashuo.com/article/p-bgwawsvd-gx.html數據庫
5 年來,咱們一直在津津樂道 Entity Framework 帶來的好處,也許是情人眼裏出西施,對於它的缺點文過飾非,大可用一句話搪塞:你要完整學習 Entity Framework 知識體系,方能事半功倍,俗話說:磨刀不誤砍柴工。服務器
通常來講,新手的問題無外乎以下幾點:app
1. 數據庫在哪?怎麼沒有數據庫初始腳本?框架
2. 怎麼又出錯了?到底執行的SQL語句是啥?函數
3. 怎麼支持 MySQL 數據庫?爲何SQLServer正常的查詢,到MySQL就出錯了?工具
4. 爲啥忽然數據庫都清空了?好恐怖,幸虧不是在服務器post
5. 性能怎麼樣?你們都說EF的性能很差性能
6. 能不能先建數據庫,而後生成模型類?
.....
這些問題,有些是能夠解決的,有些是對EF不瞭解遇到的,有些的確是EF自身的問題。
好比對 MYSQL 的支持很差,這個問題在簡單的查詢時正常,一遇到複雜的查詢,總會遇到各類問題。而數據庫被清空那個則是不瞭解EF的 Data Migration機制。性能倒不是大問題,只要合理的查詢,加上EF的持續優化,性能應該仍是可預期的。
即便一切的問題均可以概括到沒有好好學學,那 Entity Framework 總歸仍是有一個大問題:入門容易,而知識體系有點複雜,學習曲線會比較陡峭!
若是你認爲上面就是咱們轉到 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(50)] public string Name { get; set; } [StringLength(500)] public string Remark { get; set; } public virtual ICollection<User> Users { get; set; } }
public class User : IKeyID { [Key] public int ID { get; set; } [Required, StringLength(50)] public string Name { get; set; } [Required, StringLength(100)] public string Email { get; set; } [Required, StringLength(50)] 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 == 0) { 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] = @0) AND ([UserID] = @1))',N'@0 int,@1 int',@0=3,@1=50 go
exec sp_executesql N'DELETE [dbo].[RoleUsers] WHERE (([RoleID] = @0) AND ([UserID] = @1))',N'@0 int,@1 int',@0=23,@1=50 go
exec sp_executesql N'DELETE [dbo].[RoleUsers] WHERE (([RoleID] = @0) AND ([UserID] = @1))',N'@0 int,@1 int',@0=33,@1=50 go
exec sp_executesql N'INSERT [dbo].[RoleUsers]([RoleID], [UserID]) VALUES (@0, @1) ',N'@0 int,@1 int',@0=4,@1=50 go
exec sp_executesql N'INSERT [dbo].[RoleUsers]([RoleID], [UserID]) VALUES (@0, @1) ',N'@0 int,@1 int',@0=6,@1=50 go
exec sp_executesql N'INSERT [dbo].[RoleUsers]([RoleID], [UserID]) VALUES (@0, @1) ',N'@0 int,@1 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的使用技巧:
最後,放幾張系統的截圖: