You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在NHibernate 5中捕获查询执行过程?

解决NHibernate 5中捕获ADO异常并记录完整查询信息的方案

我完全懂你的痛点——NHibernate默认抛出的异常经常缺少足够的查询细节,而直接封装SqlCommand又因为它是密封类、NH5改用DbCommand接口而卡壳。下面给你几个可行的方案,都不需要子类化或代理SqlCommand:

方案一:利用NHibernate的CommandInterceptor拦截命令执行

NHibernate提供的ICommandInterceptor接口能在命令执行前后介入,刚好可以在这里包裹try/catch捕获异常,同时获取完整的命令信息。

实现步骤:

  1. 创建自定义拦截器与命令装饰器:
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);
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:40:39