记录带参数值的NHibernate错误查询
靠谱的NHibernate失败查询记录方案
嘿,这个问题我之前做项目的时候也踩过坑,IInterceptor确实在记录失败查询的参数和线程安全上有局限。给你推荐两个实用的方案,尤其是第二个,几乎完美解决你的需求:
方案一:改进IInterceptor,用ThreadLocal实现线程安全的参数记录
如果你的NHibernate版本比较旧,没法用新的拦截器,可以这样优化:
- 首先定义一个线程本地存储的容器,用来临时保存当前线程的SQL和参数:
public static class QueryLoggingContext { private static readonly ThreadLocal<(string Sql, ICollection<object> Parameters)> _currentQuery = new ThreadLocal<(string, ICollection<object>)>(); public static void SetCurrentQuery(string sql, ICollection<object> parameters) { _currentQuery.Value = (sql, parameters); } public static (string Sql, ICollection<object> Parameters)? GetCurrentQuery() { return _currentQuery.IsValueCreated ? _currentQuery.Value : null; } public static void Clear() { _currentQuery.Value = default; } } - 然后在全局异常处理中,捕获NHibernate的
GenericADOException,取出上下文里的SQL和参数记录,然后清空上下文:
这个方案解决了线程安全问题,但try { // 执行NHibernate操作 } catch (GenericADOException ex) { var queryContext = QueryLoggingContext.GetCurrentQuery(); if (queryContext.HasValue) { // 记录日志,比如用log4net log.Error($"Failed query: {queryContext.Value.Sql}, Parameters: {string.Join(", ", queryContext.Value.Parameters)}", ex); QueryLoggingContext.Clear(); } throw; }IInterceptor本身没法直接拿到参数值,所以更推荐下面的方案。
方案二:使用IDbCommandInterceptor(NHibernate 5.2+)
NHibernate 5.2之后引入了IDbCommandInterceptor,专门用来拦截数据库命令的执行,能直接获取DbCommand对象,自然就能拿到参数值,而且每个命令都是绑定到当前会话的,完全线程安全:
- 首先自定义一个实现
IDbCommandInterceptor的类:public class FailedQueryLoggingInterceptor : IDbCommandInterceptor { private readonly ILog _log = LogManager.GetLogger(typeof(FailedQueryLoggingInterceptor)); public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { LogIfFailed(command, interceptionContext); } public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { LogIfFailed(command, interceptionContext); } public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { LogIfFailed(command, interceptionContext); } private void LogIfFailed(DbCommand command, DbCommandInterceptionContext interceptionContext) { if (interceptionContext.Exception != null) { // 提取SQL和参数 var sql = command.CommandText; var parameters = command.Parameters.Cast<DbParameter>() .Select(p => $"{p.ParameterName} = {p.Value}") .ToList(); // 用log4net记录失败的查询、参数和异常 _log.Error($"Failed to execute query:\nSQL: {sql}\nParameters: {string.Join("\n", parameters)}", interceptionContext.Exception); } } // 其他接口方法可以留空或者调用默认实现 public DbCommand NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) => command; public DbCommand ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) => command; public DbCommand ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) => command; } - 然后在配置NHibernate的时候,把这个拦截器注册进去:
这个方案的核心优势:// 常规配置方式 var configuration = new Configuration(); configuration.AddInterceptor(new FailedQueryLoggingInterceptor()); // Fluent NHibernate配置方式 Fluently.Configure() .ExposeConfiguration(cfg => cfg.AddInterceptor(new FailedQueryLoggingInterceptor())) // 其他配置项...- 直接拿到
DbCommand,参数名称和值都能轻松获取,完美解决参数记录难题 - 每个命令都绑定到当前会话,不存在线程安全问题(NHibernate会话本身就是线程隔离的)
- 只在命令执行失败时记录日志,性能开销极低
- 能捕获所有数据库层面的异常,包括你提到的ORA错误
- 直接拿到
额外提示
如果项目用了依赖注入,可以把FailedQueryLoggingInterceptor注册为单例——它本身无状态,完全线程安全。另外,记录参数时要注意敏感信息(比如密码),可以在日志逻辑里过滤掉这类参数值,避免数据泄露。
内容的提问来源于stack exchange,提问作者Dries Schuddinck
相关产品推荐
相关产品推荐

