构建敏捷的 SQL 数据访问层






4.80/5 (5投票s)
一篇关于在完全独立的层中构建 C# SQL Server 数据访问层的文章。

引言
本文是继我之前关于 “内存中”数据访问 的文章之后,在那篇文章中,我详细介绍了一种将数据访问层与应用程序域层分离的架构。通过定义一组接口来设置检索和持久化对象的契约,从而实现了解耦。在那里展示的实现是对象的内存持久化,可用于模拟真实的数据持久化以供测试,并推迟数据库结构、脚本和查询的实现。
在本文中,我将展示实现这些用于真实数据库持久化的相同接口所需的类。我们将开发代码,用使用 ADO.NET SqlClient 库访问 SQL Server 实例的代码替换内存中的类(如本文顶部的图表所示,有关架构的完整解释,请参阅 此处)。
要实现的接口摘要
我将回顾实现架构中数据访问层所需的派生接口。
会话接口提供连接和事务管理,并充当管理单个实体数据访问的类的工厂。
public interface IOrdersSession: IDisposable
{
IDbTransaction BeginTransaction();
ICustomerDAL DbCustomers { get; }
void Save(Customer customer);
void Delete(Customer customer);
IOrderDAL DbOrders { get; }
void Save(Order order);
void Delete(Order order);
IOrderItemDAL DbOrderItems { get; }
void Save(OrderItem orderItem);
void Delete(OrderItem orderItem);
IProductDAL DbProducts { get; }
void Save(Product product);
void Delete(Product product);
}
作为单个数据访问接口的示例,我们将采用 Order
实体的数据访问。
/// method to call to instantiate objects
public delegate Order OrderDataAdapterHandler(Guid id, Guid idCustomer,
DateTime orderDate);
/// <summary>
/// Data access interface
/// </summary>
public interface IOrderDAL
{
List<Order> Search(Guid id, Guid idCustomer, DateTime? orderDate,
OrderDataAdapterHandler orderDataAdapter);
}
IOrdersSession
SQL 实现
SqlOrdersSession
类实现了 IOrdersSession
。它应该支持跨不同实体(以及因此,数据库表)的数据访问的事务和连接管理。为了实现这一目标,它包含两个实例变量来存储 SQL 连接和 SQL 事务。在实例化类时,会实例化 SQL 连接并打开它。由于 IOrdersSession
实现了 IDisposable
,因此此类提供了一个 Dispose
方法,该方法在事务未提交时回滚事务并关闭 SQL 连接。
public class SqlOrdersSession: IOrdersSession
{
private SqlConnection _sqlConnection;
private SqlTransaction _sqlTransaction;
public SqlOrdersSession(string connectionString)
{
OpenConnection(connectionString);
}
private void OpenConnection(string connectionString)
{
_sqlConnection = new SqlConnection(connectionString);
try
{
_sqlConnection.Open();
}
catch (Exception ex)
{
ApplicationException exp = new ApplicationException("Db connection error.",
ex);
throw exp;
}
}
#region IDisposable Members
/// <summary>
/// Clean up transaction if not committed and Sql connection
/// </summary>
public void Dispose()
{
if (_sqlTransaction != null && _sqlTransaction.Connection != null)
{
_sqlTransaction.Rollback();
}
if (_sqlConnection != null && _sqlConnection.State == ConnectionState.Open)
{
_sqlConnection.Close();
}
}
//...
该类实现了 IOrderSession
的 BeginTransaction()
方法,实例化一个 SQL 事务并将其存储在 _sqlTransaction
实例变量中。
public class SqlOrdersSession: IOrdersSession
{
//...
#region IOrdersSession Members
public IDbTransaction BeginTransaction()
{
IDbTransaction st = _sqlConnection.BeginTransaction();
_sqlTransaction = st as SqlTransaction;
return st;
}
它还为每个实体实现了一个“Db”属性,以允许访问实现相应数据访问接口的类的实例。在我们的示例中,考虑到 Order
实体,我们有一个 DbOrders
属性,它提供对实现 SqlOrderDAL
类的实例的访问。
public class SqlOrdersSession: IOrdersSession
{
private IOrderDAL _dbOrders;
//...
public IOrderDAL DbOrders
{
get
{
if (_dbOrders == null)
_dbOrders = new SqlOrderDAL(_sqlConnection, _sqlTransaction);
return _dbOrders as IOrderDAL;
}
private set { _dbOrders = value; }
}
public void Save(Order order)
{
(DbOrders as SqlOrderDAL).Save(order);
}
public void Delete(Order order)
{
(DbOrders as SqlOrderDAL).Delete(order);
}
IOrderDAL
SQL 实现
SqlOrderDAL
类实现了 IOrderDAL
。它有一个内部构造函数,因为我们不希望客户端直接实例化此类对象。构造函数以 SQL 连接和 SqlOrdersSession
对象拥有的 SQL 事务作为参数。
public class SqlOrderDAL: IOrderDAL
{
private SqlConnection _sqlConnection;
private SqlTransaction _sqlTransaction;
internal SqlOrderDAL(SqlConnection sqlConnection, SqlTransaction sqlTransaction)
{
_sqlConnection = sqlConnection;
_sqlTransaction = sqlTransaction;
}
//...
这样,所有单个实体的所有数据访问对象将在 SqlOrdersSession
生命周期内共享相同的 SQL 连接和 SQL 事务。因此,查询是事务性的。
DAL 中的对象创建
SqlOrderDAL
和其他实体 DAL 类不直接实例化对象,它们使用作为委托传递给从数据库检索对象的类的方法的业务逻辑工厂方法。在下面的代码片段中,实现搜索的方法使用 OrderDataAdapterHandler
委托来创建对象。通过这种方式,对象构造、验证或组合的逻辑不会被要求到数据访问层。
public List<Order> Search(Guid id, Guid idCustomer, DateTime? orderDate,
OrderDataAdapterHandler orderDataAdapter)
{
List<Order> lro = new List<Order>();
// ...
SqlCommand sqlCmd = new SqlCommand(sb.ToString(), _sqlConnection);
using (SqlDataReader dr = sqlCmd.ExecuteReader(CommandBehavior.CloseConnection))
{
while (dr.Read())
{
// Here the objects are instantiated with the factory method
// coming from the business logic through the delegate
Order ro = orderDataAdapter(
(Guid)dr["Id"],
(Guid)dr["IdCustomer"],
(DateTime)dr["OrderDate"]
);
lro.Add(ro);
}
}
// ...
return lro;
}
在业务逻辑方法中使用 DAL
在业务逻辑代码中,SqlOrdersSession
被用作 IOrdersSession
接口的实例,因此代码与模拟的内存实现(请参阅 上一篇文章)中的代码完全相同。
接下来是一个检索属于客户的订单的方法。使用语句用于确保会话正确关闭。
public List<Order> GetByCustomerId(Guid customerId)
{
List<Order> lis;
using (IOrdersSession sess = GBDipendencyInjection.GetSession())
{
lis = sess.DbOrders.Search(Guid.Empty, customerId, null, CreateOrder);
}
return lis;
}
代码通过 IOrdersSession
提供的实例调用 IOrderDAL
接口的 Search
方法,最后一个参数是 CreateOrder
,它是一个与 IOrderDAL
接口中声明的委托具有相同签名的 T_T方法。下面是方法代码,它是一个简单的工厂方法。它的作用是调用 CustomerLogic
来检索客户,验证它不为空,实例化一个新的订单并设置其值。如果需要实例化继承自抽象类或进行其他构造和组合逻辑的不同具体实现,可以在此处添加额外的逻辑。
public Order CreateOrder(Guid id, Guid idCustomer, DateTime orderDate)
{
Order order = new Order();
CustomerLogic custumerLogic = new CustomerLogic();
Customer customer = custumerLogic.GetById(idCustomer);
if (customer == null)
{
throw new ArgumentException("Customer not found.");
}
order.Id = id;
order.Customer = customer;
order.OrderDate = orderDate;
order.OrderItems = new List<OrderItem>();
return order;
}
最后一个代码片段是事务使用示例。在这里,订单与其订单行一起以事务方式保存。
public void SaveAllOrder(Order order)
{
using (IOrdersSession sess = GBDipendencyInjection.GetSession())
{
System.Data.IDbTransaction trans = sess.BeginTransaction();
sess.Save(order);
foreach (OrderItem orderItem in order.OrderItems)
{
sess.Save(orderItem);
}
trans.Commit();
}
}
结论
本文的目的是展示如何通过将数据访问代码与业务逻辑代码分离,并将它们依赖于一组共同的接口,而不是相互依赖,来分离对 SQL Server 数据库的数据访问。这种分离允许域代码忽略数据访问和持久化的细节和内部工作原理,数据访问代码忽略它从数据库检索的对象内部构造逻辑。因此,我们拥有只进行并且只了解数据库结构和查询的数据访问,以及不了解对象如何被持久化和检索的业务逻辑代码。
作为附带好处,我们能够使用替代数据访问代码而不更改域逻辑代码,这使得能够使用内存中(模拟)实现暂时模拟真实数据库访问,如 我之前的文章 中所示,或者使用模拟对象,从而允许在与数据库访问代码隔离的情况下测试应用程序,并推迟真实数据访问的开发。