【进阶篇】如何优雅封装.NET数据库访问层(彻底告别拼接SQL)
在上一篇中,我们已经分析了:
👉 为什么不建议使用SQL拼接,而应该使用参数化查询
但很多开发者在实际项目中仍然会遇到一个问题:
👉 参数化写法太繁琐,代码臃肿,开发效率低
那么有没有一种方式可以做到:
- 写法简洁(接近拼接SQL)
- 自动参数化(安全)
- 统一规范(易维护)
- 支持扩展(日志、事务、重试)
答案是:封装数据库访问层
一、为什么必须封装?
先说结论:
👉 不封装数据库访问层,项目后期一定会乱
常见问题:
1. SQL散落各处
csharp
// A类
var sql = "SELECT * FROM A WHERE id=@id";
// B类
var sql = "SELECT * FROM A WHERE id=" + id;
👉 写法不统一,维护困难
2. 参数化风格混乱
csharp
cmd.Parameters.AddWithValue("@id", id);
👉 每个人写法都不一样
3. 无法统一扩展
比如你想加:
- SQL日志
- 执行耗时
- 重试机制
- 事务控制
👉 需要全项目改代码 ❌
👉 所以核心目标:
把所有数据库操作收口到一层
二、设计目标(重点)
我们要实现一个工具层,满足:
| 目标 | 说明 |
|---|---|
| 简洁 | 类似拼接SQL写法 |
| 安全 | 自动参数化 |
| 通用 | 支持查询/增删改 |
| 可扩展 | 日志/事务/重试 |
| 易维护 | 统一入口 |
三、推荐方案:类Dapper风格封装
我们实现一个方法:
csharp
Execute(sql, new { id = 1, name = "张三" });
👉 看起来像拼接,但其实是参数化
四、核心实现(通用工具类)
1. 执行SQL(增删改)
csharp
public static int Execute(string sql, object param = null)
{
using (var conn = new SqlConnection(connStr))
{
using (var cmd = new SqlCommand(sql, conn))
{
if (param != null)
{
AddParameters(cmd, param);
}
conn.Open();
return cmd.ExecuteNonQuery();
}
}
}
2. 查询单条
csharp
public static T QueryFirst<T>(string sql, object param = null) where T : new()
{
using (var conn = new SqlConnection(connStr))
using (var cmd = new SqlCommand(sql, conn))
{
if (param != null)
{
AddParameters(cmd, param);
}
conn.Open();
using (var reader = cmd.ExecuteReader())
{
if (!reader.Read()) return default;
var obj = new T();
foreach (var prop in typeof(T).GetProperties())
{
if (!reader.HasColumn(prop.Name)) continue;
var val = reader[prop.Name];
if (val != DBNull.Value)
prop.SetValue(obj, val);
}
return obj;
}
}
}
3. 参数自动映射(核心)
csharp
private static void AddParameters(SqlCommand cmd, object param)
{
foreach (var prop in param.GetType().GetProperties())
{
var value = prop.GetValue(param) ?? DBNull.Value;
cmd.Parameters.AddWithValue("@" + prop.Name, value);
}
}
👉 这一步就是关键:
把匿名对象 → 自动转成SQL参数
五、使用方式(对比)
旧写法(拼接 ❌)
csharp
var sql = $"SELECT * FROM User WHERE Name='{name}'";
新写法(推荐 ✅)
csharp
var sql = "SELECT * FROM User WHERE Name=@Name";
var user = QueryFirst<User>(sql, new { Name = name });
👉 优点:
- 简洁
- 安全
- 可读性强
六、进阶优化(项目实战重点)
1. SQL日志(强烈建议)
csharp
Stopwatch sw = Stopwatch.StartNew();
var result = cmd.ExecuteNonQuery();
sw.Stop();
Console.WriteLine($"SQL执行耗时:{sw.ElapsedMilliseconds}ms");
Console.WriteLine(sql);
👉 在MES/WMS中非常重要(排查慢SQL)
2. 异常重试机制
适用于:
- 数据库瞬时连接失败
- 网络抖动
csharp
int retry = 3;
while (retry-- > 0)
{
try
{
return cmd.ExecuteNonQuery();
}
catch
{
if (retry == 0) throw;
Thread.Sleep(200);
}
}
3. 事务封装
csharp
public static void ExecuteInTransaction(Action<SqlConnection, SqlTransaction> action)
{
using var conn = new SqlConnection(connStr);
conn.Open();
using var tran = conn.BeginTransaction();
try
{
action(conn, tran);
tran.Commit();
}
catch
{
tran.Rollback();
throw;
}
}
👉 用法:
csharp
ExecuteInTransaction((conn, tran) =>
{
Execute("INSERT ...", param1, conn, tran);
Execute("UPDATE ...", param2, conn, tran);
});
4. 防止动态SQL注入(白名单)
csharp
var allowedColumns = new[] { "Name", "Age" };
if (!allowedColumns.Contains(column))
{
throw new Exception("非法字段");
}
七、适配你当前技术栈(重点)
你现在用:
- .NET
- PostgreSQL / SQL Server
- MES / WMS
- 高并发 + 设备数据
👉 建议:
1. PostgreSQL(Npgsql)
把:
csharp
SqlCommand
换成:
csharp
NpgsqlCommand
2. 高频接口建议
👉 必须加:
- SQL日志
- 超时控制
- 重试机制
3. 设备数据(你做过Socket)
👉 强烈建议:
- 所有入库操作必须参数化
- 禁止拼接
八、什么时候不用自己封装?
直接说实话:
👉 如果项目不复杂,直接用:
- Dapper(强烈推荐)
- SqlSugar(你已经在用)
- FreeSQL
👉 你自己封装适合:
- 公司没有规范
- 项目需要统一风格
- 想做技术沉淀
九、总结
👉 数据库访问层的本质:
不是为了封装而封装,而是为了统一和可控
最后一句(给你点"实战味"的结尾)
在小项目里:
👉 拼接SQL你可能感觉不到问题
但在生产环境中:
- MES系统
- 多设备接入
- 多系统交互
👉 一旦SQL失控:
不是报错的问题,是直接影响生产。