数据访问层是应用系统与数据库交互的桥梁,却经常因为代码耦合度高、难以测试而成为开发的痛点。ADO.NET在设计之初就落入了多种经典设计模式,只是平时我们往往只把它当成一组数据库API来使用,忽略了这些模式本身的价值。理解并善用这些模式,可以让数据访问层变得更加清晰、健壮,同时从容应对更换数据库、调整表结构、优化批量操作等常见需求。

一、Provider模式与工厂模式让数据库变更变得无感
很多项目中换数据库之所以痛苦,是因为代码里到处写着SqlConnection、SqlCommand这样的具体类型。ADO.NET很早就意识到了这个问题,因此引入了Provider模式和工厂模式,把数据库相关的对象创建过程抽象在最底层。开发者只需要面向DbConnection、DbCommand等抽象基类编程,再由DbProviderFactory根据配置动态创建真正适合当前数据库的实例。
这种设计的核心在于依赖倒置。业务层请求一个连接时,并不关心底层是SQL Server还是Oracle,只关心拿到的对象能执行命令、返回结果。DbProviderFactory采用工厂方法模式,每个数据库供应商都提供了一个具体的工厂类,比如SqlClientFactory、OracleClientFactory。配置文件中指定供应商名称后,代码就可以在运行时统一创建出正确的对象,极大的提升了系统的可替换性。
下面的例子展示了如何借助工厂模式创建数据库连接,整个过程完全不需要出现具体类型。
using System;
using System.Data.Common;
public class ConnectionFactory
{
private readonly string _providerInvariantName;
private readonly string _connectionString;
public ConnectionFactory(string providerInvariantName, string connectionString)
{
_providerInvariantName = providerInvariantName;
_connectionString = connectionString;
}
public DbConnection CreateConnection()
{
// DbProviderFactories.GetFactory 内部就是工厂模式
DbProviderFactory factory = DbProviderFactories.GetFactory(_providerInvariantName);
DbConnection connection = factory.CreateConnection();
if (connection != null)
{
connection.ConnectionString = _connectionString;
}
return connection;
}
}
通过这样的工厂,你可以在不修改任何业务代码的前提下,只调整配置项和连接字符串,就能让整个应用切换到另一套数据库。这个模式让数据访问层的边界变得非常清晰:高层的Repository拿到的是DbConnection,低层的Provider负责创建真实的连接实例。这种结构不仅减少了SQL Server与Oracle之间的代码差异,还方便在单元测试中注入模拟的DbConnection,从而验证SQL语句是否正确。
二、Repository模式把数据操作收敛到统一接口中
如果业务代码里直接拼SQL并调用Command对象,数据库操作逻辑就会散落在应用的各个角落。一旦表结构发生变更,就需要对所有调用点进行修改,这种维护成本显然不可接受。Repository模式采用集合式接口的概念,让业务层看起来就像是在操作内存中的对象集合,弱化了底层存储引擎的处理细节。对于一个用户表,你只需要定义UserRepository接口,对外暴露GetById、Save、Delete等方法,然后在实现类内部使用ADO.NET完成数据映射。
这种做法带来的一个直接好处就是可以单独对数据访问逻辑进行单元测试。你可以为UserRepository定义一个接口,再写一个完全跑在内存的Fake实现来验证业务逻辑,而不需要真实连接数据库。同时,当SQL优化、查询字段调整、增加索引时,修改范围被锁在Repository实现内部,不会波及其他模块。在长期维护的系统中,这种隔离能显著降低回归风险。
下面是一个使用ADO.NET实现UserRepository的简化示例,展示了如何将连接管理、参数化查询和结果映射集中在一个方法里。
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
public interface IUserRepository
{
User GetById(int id);
void Save(User user);
}
public class UserRepository : IUserRepository
{
private readonly DbProviderFactory _factory;
private readonly string _connectionString;
public UserRepository(string providerInvariantName, string connectionString)
{
_factory = DbProviderFactories.GetFactory(providerInvariantName);
_connectionString = connectionString;
}
public User GetById(int id)
{
using (DbConnection connection = _factory.CreateConnection())
{
connection.ConnectionString = _connectionString;
using (DbCommand command = connection.CreateCommand())
{
command.CommandText = "SELECT Id, Name, Email FROM Users WHERE Id = @Id";
command.CommandType = CommandType.Text;
DbParameter parameter = command.CreateParameter();
parameter.ParameterName = "@Id";
parameter.Value = id;
command.Parameters.Add(parameter);
connection.Open();
using (DbDataReader reader = command.ExecuteReader())
{
if (reader.Read())
{
return new User
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Email = reader.GetString(2)
};
}
}
}
}
return null;
}
public void Save(User user)
{
using (DbConnection connection = _factory.CreateConnection())
{
connection.ConnectionString = _connectionString;
using (DbCommand command = connection.CreateCommand())
{
command.CommandText = "UPDATE Users SET Name = @Name, Email = @Email WHERE Id = @Id";
DbParameter nameParam = command.CreateParameter();
nameParam.ParameterName = "@Name";
nameParam.Value = user.Name;
command.Parameters.Add(nameParam);
DbParameter emailParam = command.CreateParameter();
emailParam.ParameterName = "@Email";
emailParam.Value = user.Email;
command.Parameters.Add(emailParam);
DbParameter idParam = command.CreateParameter();
idParam.ParameterName = "@Id";
idParam.Value = user.Id;
command.Parameters.Add(idParam);
connection.Open();
command.ExecuteNonQuery();
}
}
}
}
这里采用的using语句体现了Dispose模式的价值。数据库连接和Command都实现了IDisposable,使用using可以确保即使出现异常,连接也会被归还到连接池。与手写try/catch/finally相比,这种写法更简洁,也不会遗漏资源释放。Repository模式下,每一个具体仓储类都只关心单一实体的持久化问题,充分符合单一职责原则,在项目规模变大时依然能保持结构稳定。
三、Unit of Work模式保证跨仓储操作的事务一致性
当你需要同时更新多个Repository,比如创建订单的同时扣减库存,任何一步失败都必须回滚全部操作。如果让每个Repository自己管理Connection对象,事务边界就会变得支离破碎。Unit of Work模式就是解决这个问题的一个经典思路:一个工作单元跟踪所有需要持久化的对象,并且在最后统一提交或回滚。在ADO.NET环境中,我们可以通过事务对象把多个Repository操作串联起来,形成一个原子操作。
核心思想是让工作单元持有唯一的DbConnection和DbTransaction,所有Repository在执行SQL时都使用这同一个连接,并在这个事务上下文里完成操作。简单的方式是定义一个WorkContext类,内部持有Connection和Transaction,并实现Commit和Rollback方法。Repository则不再自己创建Connection,而是从UnitOfWork当前上下文中获取。
下面示例演示了Unit of Work与事务包装的基本结构。
using System;
using System.Data.Common;
public sealed class UnitOfWork : IDisposable
{
private DbConnection _connection;
private DbTransaction _transaction;
private bool _disposed;
public UnitOfWork(DbProviderFactory factory, string connectionString)
{
_connection = factory.CreateConnection();
_connection.ConnectionString = connectionString;
_connection.Open();
_transaction = _connection.BeginTransaction();
}
public DbConnection Connection => _connection;
public DbTransaction Transaction => _transaction;
public void Commit()
{
_transaction?.Commit();
}
public void Rollback()
{
_transaction?.Rollback();
}
public void Dispose()
{
if (!_disposed)
{
_transaction?.Dispose();
_connection?.Dispose();
_disposed = true;
}
}
}
在实际实现中,可以让Repository接受UnitOfWork参数,在构造函数里保存其中的Connection和Transaction。这样多个Repository就可以共享同一个事务上下文了。需要注意的是,事务的隔离级别和连接超时设置也需要在启动工作单元时一并考虑,否则长事务可能引发数据库锁竞争。使用Unit of Work以后,业务代码只需要把数据操作放在一个工作单元内,最后调用Commit或Rollback,事务的粒度变得更可控,也更容易理解。
四、连接池与迭代器模式:提升数据访问吞吐量的隐蔽武器
每次创建数据库连接都要经过TCP握手、身份验证等过程,开销相当大。ADO.NET底层通过连接池来复用物理连接,这其实是享元模式的一种体现。当连接被关闭或Dispose时,并没有真正销毁,而是被返回到连接池中,后续请求可以从中取出现成的连接。不同的连接字符串会对应独立的池,过多不一样的连接字符串反而会削弱连接池的功能,所以推荐在配置中复用相同的关键参数。
另一个容易被忽视的模式是DataReader的迭代器模式。DataReader通过流式方式逐条返回结果集,它不会一次性把所有数据加载到内存,而是让代码用一个循环不断读取下一行。这意味着即使查询返回一百万行数据,内存占用也保持在很低的水平。与DataAdapter直接填充DataSet相比,DataReader更适合大结果集和只读的报表场景。
下面的代码展示了使用DataReader流式读取数据的推荐写法。
using (DbConnection connection = _factory.CreateConnection())
{
connection.ConnectionString = _connectionString;
DbCommand command = connection.CreateCommand();
command.CommandText = "SELECT Id, Name FROM BigTable";
connection.Open();
using (DbDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
// 逐行处理,避免一次性加载全部数据
int id = reader.GetInt32(0);
string name = reader.GetString(1);
ProcessRow(id, name);
}
}
}
要做到高效的连接管理,最佳实践还是始终使用using包裹连接对象,并且让连接打开时间尽量短。很多性能问题正是因为连接被不必要地长期占用,导致连接池耗尽、应用响应变慢。DataReader使用过程中连接会保持打开,因此一旦读取完毕,应立即关闭reader或释放连接。对于需要频繁往返的场景,可以使用DataAdapter的Fill方法批量加载,让连接在内部自动开关,减少人工干预。
综合来看,ADO.NET设计模式的意义不只是为了理论上的美观。Provider模式解耦了数据库供应商,Repository模式收敛了数据访问逻辑,Unit of Work模式统一了事务边界,连接池和迭代器模式解决了性能问题。当开发者理解并灵活组合这些模式后,一个数据访问层才能在真实业务中保持稳定,在长期迭代中持续为项目创造价值。选用合适的模式不是额外负担,而是在复杂的数据持久化需求中为自己降低风险、减少返工的必要策略。