【AppBox】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(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的使用技巧:

  • 批量更新數據
  • 分頁與排序的簡單封裝
  • 插入與更新的簡單封裝
  • 事務(Transaction)
  • 插入后返回自增ID
  • 動態創建匿名參數
  • 子查詢
  • 多結果映射

 

最后,放幾張系統的截圖:

 

 

 

【續】5年后,我們為什么要從 Entity Framework 轉到 Dapper 工具?

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM