【摘錄】使用實體框架、Dapper和Chain的倉儲模式實現策略

如下文章摘錄來自InfoQ,是一篇不錯的軟問,你們細細的品味

關鍵要點:

  • Dapper這類微ORM(Micro-ORM)雖然提供了最好的性能,但也須要去作最多的工做。
  • 在無需複雜對象圖時,Chain這類Fluent ORM更易於使用。
  • 對實體框架(Entity Framework)作大量的工做後,其性能可顯著提升。
  • 爲得到數據庫的最大性能,須要採用可能會有些繁瑣的投影(Projection)操做。
  • ORM總體上的局部更新可能會存在問題。

在現代企業開發中,可採用多種方法構建數據存取層(data access layer ,DAL)。使用C#作開發時,DAL的最底層幾乎老是使用ADO.NET。但這時常會造成一個笨重的庫,因此一般會在DAL的底層之上再部署一個ORM層。爲容許模擬和隱藏ORM的細節,整個DAL包裝在存儲內。git

在這一系列的文章中,咱們將審視三種使用不一樣類型ORM構建倉儲模式的方法,分別是:github

  • 實體框架:一種傳統的「全特性」或「OOP」類型的ORM。
  • Dapper:一種主要專一結果集映射的輕量級微ORM。
  • Tortuga Chain:一種基於函數式編程理念的Fluent ORM。
 

本文將側重於開發人員可在典型倉儲中用到的那些基本功能。在本系列文章的第二部分,咱們將着眼於那些開發人員基於實際狀況而實現的高級技術。sql

插入(Insert)操做

對於任何CRUD操做集,一般會首先實現基本的插入操做,進而可用插入操做對其它的操做進行測試。數據庫

Chain

Chain使用列名和屬性名間的運行時匹配。對於在數據庫中並不存在的對象,除非啓用了嚴格模式(strict model),不然將忽略該對象上的屬性。相似地,沒有匹配屬性的列不能成爲生成SQL的組成部分。編程

相關廠商內容架構

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

Dapper

沒有第三方擴展時,Dapper須要編程人員手工指定所需的SQL,其中包括了特定於數據庫的邏輯,用於返回新建立的主鍵。app

 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
        );

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();
            }
        }

實體框架

像之前同樣,實體框架使用編譯期信息肯定如何生成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();
            }

實體框架

實體框架將表名和首個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

相關文章
相關標籤/搜索