在现代企业开发中,可采用多种方法构建数据存取层(data access layer ,DAL)。使用C#做开发时,DAL的最底层几乎总是使用ADO.NET。但这时常会形成一个笨重的库,所以通常会在DAL的底层之上再部署一个ORM层。为允许模拟和隐藏ORM的细节,整个DAL包装在存储内。
在这一系列的文章中,我们将审视三种使用不同类型ORM构建仓储模式的方法,分别是:
实体框架:一种传统的“全特性”或“OOP”类型的ORM。
Dapper:一种主要专注结果集映射的轻量级微ORM。
Tortuga Chain:一种基于函数式编程理念的Fluent ORM。
本文将侧重于开发人员可在典型仓储中用到的那些基本功能。在本系列文章的第二部分,我们将着眼于那些开发人员基于实际情况而实现的高级技术。
插入(Insert)操作
对于任何CRUD操作集,通常会首先实现基本的插入操作,进而可用插入操作对其它的操作进行测试。
Chain
Chain使用列名和属性名间的运行时匹配。对于在数据库中并不存在的对象,除非启用了严格模式(strict model),否则将忽略该对象上的属性。类似地,没有匹配属性的列不能成为生成SQL的组成部分。
public int Insert(Employee employee)
{
return m_DataSource.Insert("HR.Employee", employee).ToInt32().Execute();
}Dapper
没有第三方扩展时,Dapper需要编程人员手工指定所需的SQL,其中包括了特定于数据库的逻辑,用于返回新创建的主键。
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; }
إخلاء المسؤولية: الآراء الواردة هنا تعبر فقط عن رأي الكاتب، ولا تمثل الموقف الرسمي لـ Followme. لا تتحمل Followme مسؤولية دقة أو اكتمال أو موثوقية المعلومات المُقدمة، ولا تتحمل مسؤولية أي إجراءات تُتخذ بناءً على المحتوى، ما لم يُنص على ذلك صراحةً كتابيًا.
