【摘录】使用实体框架、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

相关文章
相关标签/搜索