在现代企业开发中,可采用多种方法构建数据存取层(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 不對內容的準確性、完整性或可靠性作出任何保證,對於基於該內容所採取的任何行為,不承擔任何責任,除非另有書面明確說明。
