在业务开发中,软删除是比物理删除更常用的数据删除方式,通常我们会在表中增加一个is_deleted字段,删除时将该字段设为1,查询时只返回is_deleted为0的记录。Dapper作为轻量级ORM,本身没有内置的全局查询过滤器功能,需要开发者自行实现统一的软删除查询逻辑。

软删除的基础实现
首先我们来看最基础的软删除查询写法,假设我们有一张user表,包含id、name、is_deleted三个字段,查询未删除用户的SQL如下:
-- 查询未删除的用户 SELECT id, name FROM user WHERE is_deleted = 0;
如果使用Dapper执行这个查询,代码可以写成:
using Dapper;
using System.Data.SqlClient;
using System.Linq;
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public class UserRepository
{
private readonly string _connectionString;
public UserRepository(string connectionString)
{
_connectionString = connectionString;
}
public List<User> GetActiveUsers()
{
using (var conn = new SqlConnection(_connectionString))
{
string sql = "SELECT id, name FROM user WHERE is_deleted = 0";
return conn.Query<User>(sql).ToList();
}
}
}
这种方式的问题是,每次查询user表都需要手动加上is_deleted = 0的条件,如果表数量多或者查询场景复杂,会产生大量重复代码,也容易遗漏条件导致查询到已删除的数据。
Dapper全局查询过滤器实现思路
思路一:封装通用查询方法
我们可以封装一个通用的查询方法,自动为所有标记了软删除特性的实体拼接过滤条件。首先定义一个标识软删除的接口:
public interface ISoftDelete
{
int Is_Deleted { get; set; }
}
让我们的实体类实现这个接口:
public class User : ISoftDelete
{
public int Id { get; set; }
public string Name { get; set; }
public int Is_Deleted { get; set; }
}
然后封装一个通用的查询方法,通过反射判断实体是否实现了ISoftDelete接口,如果是则自动拼接过滤条件:
using Dapper;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;
public class DapperRepository
{
private readonly string _connectionString;
public DapperRepository(string connectionString)
{
_connectionString = connectionString;
}
public List<T> Query<T>(string sql, object param = null)
{
using (var conn = new SqlConnection(_connectionString))
{
// 判断T是否实现了ISoftDelete接口
var isSoftDelete = typeof(ISoftDelete).IsAssignableFrom(typeof(T));
if (isSoftDelete)
{
// 检查sql中是否已经包含is_deleted条件,避免重复拼接
if (!sql.Contains("is_deleted", StringComparison.OrdinalIgnoreCase))
{
// 简单处理,实际场景需要根据sql结构拼接,比如where后面加条件
if (sql.Contains("WHERE", StringComparison.OrdinalIgnoreCase))
{
sql += " AND is_deleted = 0";
}
else
{
sql += " WHERE is_deleted = 0";
}
}
}
return conn.Query<T>(sql, param).ToList();
}
}
}
使用时只需要调用封装好的Query方法,不需要手动写过滤条件:
var repo = new DapperRepository("your_connection_string");
// 这里不需要手动加is_deleted条件,方法会自动拼接
var users = repo.Query<User>("SELECT id, name FROM user");
思路二:使用SQL拦截器动态修改SQL
如果需要更灵活的处理,比如支持多表查询、复杂SQL的自动过滤,可以使用Dapper的拦截器功能,在SQL执行前动态修改SQL文本。首先实现一个DbCommandInterceptor:
using Dapper;
using System;
using System.Data;
using System.Data.Common;
using System.Text.RegularExpressions;
public class SoftDeleteInterceptor : DbCommandInterceptor
{
public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
// 判断当前查询的表是否包含需要软删除过滤的表,这里可以维护一个需要过滤的表名列表
var needFilterTables = new List<string> { "user", "order" };
foreach (var table in needFilterTables)
{
// 简单正则匹配,实际场景需要更严谨的SQL解析
if (Regex.IsMatch(command.CommandText, $@"b{table}b", RegexOptions.IgnoreCase))
{
if (!command.CommandText.Contains("is_deleted", StringComparison.OrdinalIgnoreCase))
{
if (command.CommandText.Contains("WHERE", StringComparison.OrdinalIgnoreCase))
{
command.CommandText += $" AND {table}.is_deleted = 0";
}
else
{
command.CommandText += $" WHERE {table}.is_deleted = 0";
}
}
}
}
base.ReaderExecuting(command, interceptionContext);
}
}
然后在使用Dapper的时候注册这个拦截器:
using Dapper;
using System.Data.SqlClient;
public class DbContext
{
private static bool _interceptorRegistered = false;
public static SqlConnection GetConnection(string connectionString)
{
var conn = new SqlConnection(connectionString);
if (!_interceptorRegistered)
{
// 注册拦截器
SqlConnection.ClearAllPools();
var interceptor = new SoftDeleteInterceptor();
DbInterception.Add(interceptor);
_interceptorRegistered = true;
}
return conn;
}
}
注意事项
- 动态拼接SQL的时候要注意SQL注入问题,上面的示例只是简单演示,实际场景需要更严谨的SQL解析逻辑,避免拼接恶意内容。
- 如果表名或者字段名是数据库关键字,拼接条件的时候需要加上对应的转义符,比如MySQL用反引号,SQL Server用方括号。
- 对于更新、删除操作,如果需要同时处理软删除逻辑,也可以在通用方法中做对应的处理,比如删除操作自动转为更新is_deleted字段。
- 如果项目中有部分查询需要包含已删除的数据,可以加一个开关参数,在调用查询方法的时候传入,跳过自动拼接过滤条件。
总结
Dapper虽然没有内置全局查询过滤器,但是通过封装通用查询方法或者SQL拦截器的方式,都可以实现软删除的统一查询过滤。开发者可以根据项目的复杂程度选择合适的实现方式,简单的项目用封装通用方法的方式就足够,复杂的多表查询场景可以考虑用SQL拦截器实现更灵活的逻辑,最终减少重复代码,保证查询逻辑的一致性。