如何在NHibernate 5中捕获查询执行过程?
解决NHibernate 5中捕获ADO异常并记录完整查询信息的方案
我完全懂你的痛点——NHibernate默认抛出的异常经常缺少足够的查询细节,而直接封装SqlCommand又因为它是密封类、NH5改用DbCommand接口而卡壳。下面给你几个可行的方案,都不需要子类化或代理SqlCommand:
方案一:利用NHibernate的CommandInterceptor拦截命令执行
NHibernate提供的ICommandInterceptor接口能在命令执行前后介入,刚好可以在这里包裹try/catch捕获异常,同时获取完整的命令信息。
实现步骤:
- 创建自定义拦截器与命令装饰器:
public class CommandExceptionLoggingInterceptor : EmptyCommandInterceptor { public override DbCommand PrepareCommand(DbCommand command, DbCommandInterceptionContext interceptionContext) { var preparedCommand = base.PrepareCommand(command, interceptionContext); // 用装饰器包装原命令,注入异常捕获逻辑 return new LoggingDbCommandWrapper(preparedCommand); } } // 装饰器类,继承DbCommand包装原命令 public class LoggingDbCommandWrapper : DbCommand { private readonly DbCommand _innerCommand; public LoggingDbCommandWrapper(DbCommand innerCommand) { _innerCommand = innerCommand ?? throw new ArgumentNullException(nameof(innerCommand)); } // 转发所有DbCommand的属性与基础方法给原命令 public override string CommandText { get => _innerCommand.CommandText; set => _innerCommand.CommandText = value; } public override int CommandTimeout { get => _innerCommand.CommandTimeout; set => _innerCommand.CommandTimeout = value; } public override CommandType CommandType { get => _innerCommand.CommandType; set => _innerCommand.CommandType = value; } public override UpdateRowSource UpdatedRowSource { get => _innerCommand.UpdatedRowSource; set => _innerCommand.UpdatedRowSource = value; } protected override DbConnection DbConnection { get => _innerCommand.Connection; set => _innerCommand.Connection = value; } protected override DbParameterCollection DbParameterCollection => _innerCommand.Parameters; protected override DbTransaction DbTransaction { get => _innerCommand.Transaction; set => _innerCommand.Transaction = value; } public override void Cancel() => _innerCommand.Cancel(); public override void Prepare() => _innerCommand.Prepare(); public override DbParameter CreateParameter() => _innerCommand.CreateParameter(); // 重点:重写ExecuteReader,添加异常捕获与日志 public override DbDataReader ExecuteReader(CommandBehavior behavior) { try { return _innerCommand.ExecuteReader(behavior); } catch (DbException ex) { LogFullQueryException(ex, _innerCommand); throw; // 重新抛出,不破坏原有异常流程 } } // 覆盖其他执行方法,确保全场景捕获 public override int ExecuteNonQuery() { try { return _innerCommand.ExecuteNonQuery(); } catch (DbException ex) { LogFullQueryException(ex, _innerCommand); throw; } } public override object ExecuteScalar() { try { return _innerCommand.ExecuteScalar(); } catch (DbException ex) { LogFullQueryException(ex, _innerCommand); throw; } } // 日志记录逻辑,可根据自己的日志框架调整 private void LogFullQueryException(DbException ex, DbCommand command) { var paramDetails = string.Join(", ", command.Parameters.Cast<DbParameter>() .Select(p => $"{p.ParameterName} = {p.Value ?? "NULL"}")); var logContent = $"ADO异常触发:{ex.Message}\n" + $"SQL语句:{command.CommandText}\n" + "参数列表:" + paramDetails + "\n" + $"命令类型:{command.CommandType}\n" + $"超时设置:{command.CommandTimeout}秒"; // 替换为你的日志组件,比如Serilog.Log.Error(ex, logContent); Console.WriteLine(logContent); } protected override void Dispose(bool disposing) { if (disposing) { _innerCommand.Dispose(); } base.Dispose(disposing); } }
- 在NHibernate配置中注册拦截器:
var config = new Configuration(); config.SetInterceptor(new CommandExceptionLoggingInterceptor()); // 其他配置项... var sessionFactory = config.BuildSessionFactory();
这个方案完全依托NHibernate原生扩展点,不需要修改驱动,能覆盖所有通过NH执行的命令,同时拿到完整的SQL、参数等细节。
方案二:通过SessionFactory事件监听替换命令
另一种思路是监听CommandPrepared事件,在命令准备完成后替换为我们的装饰器:
var config = new Configuration(); var sessionFactory = config.BuildSessionFactory(); sessionFactory.Events.CommandPrepared += (sender, e) => { e.Command = new LoggingDbCommandWrapper(e.Command); };
效果和方案一一致,只是注册方式不同,适合喜欢用事件驱动方式的场景。
方案三:自定义SqlClientDriver包装命令
虽然你之前尝试重写驱动遇到问题,但其实可以重写CreateCommand方法,返回包装后的命令:
public class LoggingSqlClientDriver : SqlClientDriver { public override DbCommand CreateCommand() { var rawCommand = base.CreateCommand(); return new LoggingDbCommandWrapper(rawCommand); } }
然后在配置中指定使用这个驱动:
config.Properties[Environment.ConnectionDriver] = typeof(LoggingSqlClientDriver).AssemblyQualifiedName;
关键注意事项
- 敏感信息脱敏:如果需要记录连接相关信息,一定要去掉密码、密钥等敏感内容。
- 不要吞异常:捕获后必须重新抛出,否则会打乱NHibernate的异常处理流程。
- 覆盖全执行方法:除了
ExecuteReader,务必覆盖ExecuteNonQuery、ExecuteScalar等方法,确保所有命令执行的异常都能被捕获。
这样就能在捕获ADO异常时,拿到比NH默认异常更完整的查询信息了。
内容的提问来源于stack exchange,提问作者Michael
相关产品推荐
相关产品推荐

