声明
using Dapper;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
基础配置与缓存
#region 基础配置与缓存
/// <summary>
/// 基础排除名单。
/// <para>这些属性通常属于分页基类,用于程序控制而非数据库物理列,构建 SQL 时将自动跳过。</para>
/// <summary>
public static readonly string[] BaseExcludes = { "pageNumber", "pageSize", "totalCount", "sortField", "sortOrder" };
/// <summary>
/// 线程安全的反射属性缓存。
/// <para>Key: DTO 类型; Value: 属性信息数组。用于降低高频反射带来的性能开销。</para>
/// <summary>
private static readonly ConcurrentDictionary _propCache = new();
#endregion
查询
#region 查询构建 (SELECT / WHERE / QUERY)
/// <summary>
/// 自动根据 DTO 属性生成 SELECT 字段部分。
/// <para>逻辑:优先识别 [SqlFunction] 生成函数列;其次识别 [Column] 生成物理列;否则使用属性名。</para>
/// </summary>
/// <typeparam name="T">DTO 实体类型。</typeparam>
/// <param name="sbSql">StringBuilder 实例。</param>
/// <param name="tableAlias">表别名(可选,如 "t")。</param>
/// <param name="addNewLine">字段间是否换行(默认 true)。</param>
/// <param name="excludeFields">手动指定额外排除的属性名(可选)。</param>
public static void BuildSelect<T>(StringBuilder sbSql, string tableAlias = "", bool addNewLine = true, string[]? excludeFields = null)
{
string aliasPrefix = string.IsNullOrWhiteSpace(tableAlias) ? "" : $"{tableAlias}.";
var props = GetFilteredProperties<T>(excludeFields);
var fieldList = props.Select(prop =>
{
// 1. 获取SqlFunctionAttribute特性
string? functionSql = GetDynamicAttributeValue(prop, "SqlFunctionAttribute", "FunctionSql");
if (!string.IsNullOrEmpty(functionSql))
{
return $"{functionSql} AS {prop.Name}";
}
// 2. 处理物理映射列
var colAttr = prop.GetCustomAttribute<ColumnAttribute>();
string columnName = colAttr?.Name ?? prop.Name;
return $"{aliasPrefix}{columnName} AS {prop.Name}";
}).ToList();
string separator = addNewLine ? ",\n " : ", ";
sbSql.Append(string.Join(separator, fieldList));
}
/// <summary>
/// 构建参数化 WHERE 子句片段。
/// <para>功能:自动识别空值跳过、模糊查询 (LIKE)、集合查询 (IN) 及基础相等匹配。</para>
/// </summary>
/// <typeparam name="T">参数 DTO 类型。</typeparam>
/// <param name="paramDto">包含条件的实例。</param>
/// <param name="tableAlias">表别名(如 "t")。</param>
/// <param name="likeFields">需执行模糊查询的属性名数组。</param>
/// <param name="inFields">需执行 IN 查询的属性名数组(新增)。</param>
/// <param name="excludeFields">需排除构建条件的属性名数组。</param>
/// <returns>返回包含参数化 SQL、Dapper 参数对象及还原后的调试 SQL。</returns>
public static (string Sql, DynamicParameters Params, string DebugSql) BuildWhere<T>(
T paramDto,
string tableAlias = "",
string[]? likeFields = null,
string[]? inFields = null, // 新增参数
string[]? excludeFields = null)
{
if (paramDto == null) return (string.Empty, new DynamicParameters(), string.Empty);
var dynamicParams = new DynamicParameters();
var sbWhere = new StringBuilder();
string aliasPrefix = string.IsNullOrWhiteSpace(tableAlias) ? "" : $"{tableAlias}.";
var props = GetFilteredProperties<T>(excludeFields);
foreach (var prop in props)
{
var val = prop.GetValue(paramDto);
// 过滤空值及日期类型
if (val == null || string.IsNullOrWhiteSpace(val.ToString())) continue;
if (prop.PropertyType == typeof(DateTime) || prop.PropertyType == typeof(DateTime?)) continue;
var attr = prop.GetCustomAttribute<ColumnAttribute>();
string columnName = attr?.Name ?? prop.Name;
// 1. 优先处理显式指定的 IN 查询
if (inFields != null && inFields.Contains(prop.Name))
{
// 如果是集合类型
if (val is System.Collections.IEnumerable list && !(val is string))
{
sbWhere.Append($" AND {aliasPrefix}{columnName} IN :{prop.Name}");
dynamicParams.Add(prop.Name, val);
}
// 如果是单个值,也转为 IN 查询
else
{
var items = val.ToString()?
.Split(',')
.Select(x => x.Trim())
.Where(x => !string.IsNullOrEmpty(x)) // 过滤空字符串
.ToList() ?? new List<string>();
if (!items.Any())
{
throw new ArgumentException($"字段 {prop.Name} IN 查询的值不能为空");
}
sbWhere.Append($" AND {aliasPrefix}{columnName} IN :{prop.Name}");
dynamicParams.Add(prop.Name, items);
}
}
// 2. 处理模糊查询
else if (likeFields != null && likeFields.Contains(prop.Name))
{
sbWhere.Append($" AND {aliasPrefix}{columnName} LIKE '%' || :{prop.Name} || '%'");
dynamicParams.Add(prop.Name, val.ToString().Trim());
}
// 3. 自动识别集合类型为 IN 查询
else if (val is System.Collections.IEnumerable enumerable && !(val is string))
{
sbWhere.Append($" AND {aliasPrefix}{columnName} IN :{prop.Name}");
dynamicParams.Add(prop.Name, val);
}
// 4. 默认相等匹配
else
{
sbWhere.Append($" AND {aliasPrefix}{columnName} = :{prop.Name}");
dynamicParams.Add(prop.Name, val);
}
}
string whereSql = sbWhere.ToString();
return (whereSql, dynamicParams, ToDebugSql(whereSql, dynamicParams));
}
/// <summary>
/// 构建完整的单表查询 SQL 语句(包含 SELECT、FROM、WHERE)。
/// </summary>
/// <typeparam name="T">DTO 类型。</typeparam>
/// <param name="paramDto">条件实例。</param>
/// <param name="tableName">物理表名。</param>
/// <param name="tableAlias">表别名。</param>
/// <param name="likeFields">模糊查询字段。</param>
/// <param name="inFields">IN 查询字段。</param>
/// <param name="excludeFields">排除字段。</param>
/// <param name="customWhere">手动补充的 SQL 片段(如 "AND STATUS != 9")。</param>
/// <returns>返回完整 SQL 结果元组。</returns>
public static (string Sql, DynamicParameters Params, string DebugSql) BuildQuery<T>(
T paramDto,
string tableName,
string tableAlias = "",
string[]? likeFields = null,
string[]? inFields = null, // 新增参数
string[]? excludeFields = null,
string? customWhere = null)
{
if (string.IsNullOrWhiteSpace(tableName))
{
throw new ArgumentException("表名不能为空", nameof(tableName));
}
var sbSql = new StringBuilder("SELECT ");
BuildSelect<T>(sbSql, tableAlias);
sbSql.Append($"\nFROM {tableName} {tableAlias}\nWHERE 1=1 ");
var whereRes = BuildWhere(paramDto, tableAlias, likeFields, inFields, excludeFields);
string fullSql = sbSql.ToString() + whereRes.Sql;
string fullDebugSql = sbSql.ToString() + whereRes.DebugSql;
if (!string.IsNullOrWhiteSpace(customWhere))
{
// 只允许 AND 和 OR 开头
if (!Regex.IsMatch(customWhere.Trim(), @"^(AND|OR)\s", RegexOptions.IgnoreCase))
{
throw new ArgumentException("customWhere 必须以 AND 或 OR 开头");
}
// 检查是否包含危险关键字
var dangerousKeywords = new[] { "DROP", "DELETE", "TRUNCATE", "CREATE", "ALTER" };
if (dangerousKeywords.Any(kw => customWhere.Contains(kw, StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException("customWhere 包含不允许的操作关键字");
}
fullSql += "\n" + customWhere.Trim();
}
return (fullSql, whereRes.Params, fullDebugSql);
}
#endregion
保存
#region 保存构建 (INSERT / UPDATE)
/// <summary>
/// 构建保存(新增或更新)SQL 语句及参数。
/// <para>注意:会自动过滤标记了 [SqlFunction] 的属性,使其不参与入库操作。</para>
/// <para>支持 Oracle 序列:若主键值为 "xxx.NEXTVAL",则直接拼入 SQL 而不作为参数绑定。</para>
/// </summary>
/// <typeparam name="T">模型类型。</typeparam>
/// <param name="model">待保存的数据对象。</param>
/// <param name="saveType">保存类型:0-新增 (INSERT), 1-更新 (UPDATE)。</param>
/// <param name="tableName">目标物理表名。</param>
/// <param name="pkFields">主键属性名数组(用于 WHERE 条件及序列判定)。</param>
/// <returns>返回包含 SQL 和 Dapper 参数的元组</returns>
public static (string Sql, DynamicParameters Params, string DebugSql) BuildSave<T>(
T model,
int saveType,
string tableName,
string[] pkFields)
{
var props = GetFilteredProperties<T>();
var pkList = pkFields.Select(x => x.ToLower()).ToList();
var dynamicParams = new DynamicParameters();
string sql;
if (saveType == 0) // INSERT 模式
{
var sbCols = new StringBuilder();
var sbVals = new StringBuilder();
foreach (var prop in props)
{
if (HasAttribute(prop, "SqlFunctionAttribute"))
continue;
var val = prop.GetValue(model);
bool isPk = pkList.Contains(prop.Name.ToLower());
if (!isPk && (val == null || (val is string str && string.IsNullOrWhiteSpace(str))))
continue;
var attr = prop.GetCustomAttribute<ColumnAttribute>();
string colName = attr?.Name ?? prop.Name;
// 特殊处理:Oracle 序列字符串直接拼接
if (isPk && val is string seqStr && seqStr.ToUpper().EndsWith(".NEXTVAL"))
{
sbCols.Append($"{colName}, ");
sbVals.Append($"{seqStr}, ");
continue;
}
sbCols.Append($"{colName}, ");
sbVals.Append($":{prop.Name}, ");
dynamicParams.Add(prop.Name, val); // 直接添加原始值
}
sql = $"INSERT INTO {tableName} ({sbCols.ToString().TrimEnd(' ', ',')}) " +
$"\nVALUES ({sbVals.ToString().TrimEnd(' ', ',')})";
}
else // UPDATE 模式
{
var sbSets = new StringBuilder();
var sbWheres = new StringBuilder();
foreach (var prop in props)
{
if (HasAttribute(prop, "SqlFunctionAttribute"))
continue;
var val = prop.GetValue(model);
var attr = prop.GetCustomAttribute<ColumnAttribute>();
string colName = attr?.Name ?? prop.Name;
bool isPk = pkList.Contains(prop.Name.ToLower());
if (isPk)
{
if (val == null)
throw new InvalidOperationException(
$"更新失败:主键字段 '{prop.Name}' 的值不能为空");
if (sbWheres.Length > 0) sbWheres.Append(" AND ");
sbWheres.Append($"{colName} = :{prop.Name}");
dynamicParams.Add(prop.Name, val);
}
else
{
if (val == null || (val is string str && string.IsNullOrWhiteSpace(str)))
continue;
sbSets.Append($"{colName} = :{prop.Name}, ");
dynamicParams.Add(prop.Name, val); // 直接添加原始值
}
}
if (sbSets.Length == 0)
throw new InvalidOperationException("更新失败:没有可更新的有效字段值。");
if (sbWheres.Length == 0)
throw new InvalidOperationException("更新失败:未指定主键字段条件。");
sql = $"UPDATE {tableName} SET {sbSets.ToString().TrimEnd(' ', ',')} " +
$"\nWHERE {sbWheres.ToString()}";
}
return (sql, dynamicParams, ToDebugSql(sql, dynamicParams));
}
/// <summary>
/// 构建计数查询 SQL(用于获取总记录数)
/// </summary>
/// <typeparam name="T">查询参数 DTO 类型</typeparam>
/// <param name="paramDto">包含查询条件的实例</param>
/// <param name="tableName">目标表名</param>
/// <param name="tableAlias">表别名(可选,如 "t")</param>
/// <param name="likeFields">需执行模糊查询的字段名数组(可选)</param>
/// <param name="excludeFields">需排除的字段名数组(可选)</param>
/// <param name="customWhere">自定义 WHERE 条件(可选,如 "AND CREATE_TIME > SYSDATE-30")</param>
/// <param name="countExpression">自定义计数表达式(可选,默认 "COUNT(1)",可传入 "COUNT(DISTINCT XXXX)" 等)</param>
/// <returns>返回包含 SQL 和 Dapper 参数的元组</returns>
public static (string Sql, DynamicParameters Params, string DebugSql) BuildCountQuery<T>(
T paramDto,
string tableName,
string tableAlias = "",
string[]? likeFields = null,
string[]? excludeFields = null,
string? customWhere = null,
string countExpression = "COUNT(1)")
{
if (string.IsNullOrWhiteSpace(tableName)) throw new ArgumentException("表名不能为空", nameof(tableName));
if (string.IsNullOrWhiteSpace(countExpression)) countExpression = "COUNT(1)";
// 构建基础 SELECT COUNT 语句
var sbSql = new StringBuilder();
sbSql.Append($"SELECT {countExpression}");
sbSql.Append($"\nFROM {tableName}");
// 添加表别名
if (!string.IsNullOrWhiteSpace(tableAlias))
{
sbSql.Append($" {tableAlias}");
}
sbSql.Append("\nWHERE 1=1");
// 构建 WHERE 条件
var whereRes = BuildWhere(paramDto, tableAlias, likeFields, excludeFields);
string fullSql = sbSql.ToString() + whereRes.Sql;
string fullDebugSql = sbSql.ToString() + whereRes.DebugSql;
// 添加自定义 WHERE 条件
if (!string.IsNullOrWhiteSpace(customWhere))
{
// 只允许 AND 和 OR 开头
if (!Regex.IsMatch(customWhere.Trim(), @"^(AND|OR)\s", RegexOptions.IgnoreCase))
{
throw new ArgumentException("customWhere 必须以 AND 或 OR 开头");
}
// 检查是否包含危险关键字
var dangerousKeywords = new[] { "DROP", "DELETE", "TRUNCATE", "CREATE", "ALTER" };
if (dangerousKeywords.Any(kw => customWhere.Contains(kw, StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException("customWhere 包含不允许的操作关键字");
}
fullSql += "\n" + customWhere.Trim();
}
return (fullSql, whereRes.Params, fullDebugSql);
}
/// <summary>
/// 构建删除 SQL 语句及参数。
/// <para>支持单条记录删除或基于主键的批量删除。</para>
/// </summary>
/// <typeparam name="T">模型类型。</typeparam>
/// <param name="tableName">目标物理表名。</param>
/// <param name="pkField">主键属性名(如 "poId")。</param>
/// <param name="pkValues">主键值列表。若传入多个值,则生成 IN 语句;若为一个值,则生成 = 语句。</param>
/// <returns>返回包含 SQL 和 Dapper 参数的元组</returns>
public static (string Sql, DynamicParameters Params, string DebugSql) BuildDelete(
string tableName,
string pkField,
List<string> pkValues)
{
#region 1. 参数校验
if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
if (string.IsNullOrEmpty(pkField)) throw new ArgumentNullException(nameof(pkField));
if (pkValues == null || !pkValues.Any()) throw new InvalidOperationException("删除失败:未指定主键值");
var dynamicParams = new DynamicParameters();
string sql;
string whereCondition;
#endregion
#region 2. 构造 WHERE 条件
if (pkValues.Count == 1)
{
// 单条删除:WHERE PK = :PK
whereCondition = $"{pkField} = :pkValue";
dynamicParams.Add("pkValue", pkValues[0]);
}
else
{
// 批量删除:WHERE PK IN (:pkVal0, :pkVal1, ...)
// Dapper 支持直接传递集合给 IN 子句,但为了生成的 DebugSql 更精确,这里手动处理占位符
var paramNames = new List<string>();
for (int i = 0; i < pkValues.Count; i++)
{
string pName = $"pkVal{i}";
paramNames.Add($":{pName}");
dynamicParams.Add(pName, pkValues[i]);
}
whereCondition = $"{pkField} IN ({string.Join(", ", paramNames)})";
}
#endregion
#region 3. 组装 SQL
sql = $"DELETE FROM {tableName} WHERE {whereCondition}";
#endregion
return (sql, dynamicParams, ToDebugSql(sql, dynamicParams));
}
/// <summary>
/// 构建获取 Oracle 序列值的查询 SQL
/// </summary>
/// <param name="sequenceName">序列名称(由外部传入)</param>
/// <returns>返回包含 SQL 和 Dapper 参数的元组</returns>
public static (string Sql, DynamicParameters Params, string DebugSql) BuildSequenceQuery(string sequenceName)
{
if (string.IsNullOrWhiteSpace(sequenceName))
{
throw new ArgumentException("序列名不能为空", nameof(sequenceName));
}
var sql = $"SELECT {sequenceName}.NEXTVAL FROM DUAL";
var parameters = new DynamicParameters();
var debugSql = $"-- 序列号获取\n{sql};";
return (sql, parameters, debugSql);
}
#endregion
内部辅助方法
#region 内部辅助方法
///
/// Dapper 参数还原工具。
/// 逻辑:将 SQL 中的参数占位符(如 :name)替换为实际值的字符串表示,便于开发人员拷贝到数据库工具中直接运行调试。
///
/// 带占位符的参数化 SQL。
/// Dapper 的 DynamicParameters 容器。
/// 替换后的原生预览 SQL。
public static string ToDebugSql(string sql, DynamicParameters? parameters)
{
if (parameters == null || string.IsNullOrEmpty(sql)) return sql;
string debugSql = sql;
var paramNames = parameters.ParameterNames.OrderByDescending(x => x.Length);
foreach (var name in paramNames)
{
var pVal = parameters.Get
特性
///
/// 函数控制
///
[AttributeUsage(AttributeTargets.Property)]
public class SqlFunctionAttribute : Attribute
{
///
/// 函数
///
public string FunctionSql { get; }
///
/// 函数
///
///
public SqlFunctionAttribute(string functionSql)
{
FunctionSql = functionSql;
}
}
文章评论