Использование профилирования базы данных MiniProfiler с NHibernate

Как проще всего использовать профилирование базы данных MiniProfiler с помощью NHibernate? Чтобы профилировщик работал, мне нужно обернуть DbConnection, который NHibernate использует, в ProfiledDbConnection.

Я не слишком хорошо знаком с внутренним устройством NHibernate, поэтому не знаю, где находятся все точки расширения. (Я заметил, что NHibernate ISession имеет свойство Connection, но он доступен только для чтения.)


person Brant Bobby    schedule 16.06.2011    source источник


Ответы (4)


[ОБНОВЛЕНИЕ] По следующим ссылкам можно найти версию, использующую RealProxy для прокси-сервера SqlCommand - теперь поддерживается пакетная обработка

Я оставил исходный ответ без изменений, поскольку он был принят. [/ОБНОВИТЬ]

Мне удалось частично заставить это работать, реализовав профилированный клиентский драйвер (пример для Sql Server 2008 ниже) - это работает для простых примеров, однако я еще не нашел решения для пакетной обработки NH (которое пытается привести команду назад к SqlCommand)

public class ProfiledSql2008ClientDriver : Sql2008ClientDriver
{
    public override IDbCommand CreateCommand()
    {
        return new ProfiledDbCommand(
            base.CreateCommand() as DbCommand, 
            null,
            MiniProfiler.Current);
    }

    public override IDbConnection CreateConnection()
    {
        return ProfiledDbConnection.Get(
            base.CreateConnection() as DbConnection, 
            MiniProfiler.Current);
    }
}
person Robert Milne    schedule 17.06.2011
comment
Текущая версия MiniProfiler (1.9) требует небольшой модификации. Вызов ProfiledDbConnection.Get необходимо заменить конструктором: вернуть новый ProfiledDbConnection (base.CreateConnection () как DbConnection, MiniProfiler.Current); - person Erik Schierboom; 15.03.2012

Я расширил ответ Робертса выше для работы с пакетной обработкой NHibernate. Здесь много кода, поэтому его можно сократить, некоторые из них основаны на исходном коде nHibernate для клиентского драйвера.

<property name="connection.driver_class">YoureOnTime.Data.ProfiledSqlClientDriver, YoureOnTime.Common</property>


public class ProfiledSqlClientDriver : DriverBase, IEmbeddedBatcherFactoryProvider
{
    public override IDbConnection CreateConnection()
    {
        return new ProfiledSqlDbConnection(
            new SqlConnection(), 
            MiniProfiler.Current);
    }

    public override IDbCommand CreateCommand()
    {
        return new ProfiledSqlDbCommand(
            new SqlCommand(),
            null,
            MiniProfiler.Current);
    }

    public override bool UseNamedPrefixInSql
    {
        get { return true; }
    }

    public override bool UseNamedPrefixInParameter
    {
        get { return true; }
    }

    public override string NamedPrefix
    {
        get { return "@"; }
    }

    public override bool SupportsMultipleOpenReaders
    {
        get { return false; }
    }

    public static void SetParameterSizes(IDataParameterCollection parameters, SqlType[] parameterTypes)
    {
        for (int i = 0; i < parameters.Count; i++)
        {
            SetVariableLengthParameterSize((IDbDataParameter)parameters[i], parameterTypes[i]);
        }
    }

    private const int MaxAnsiStringSize = 8000;
    private const int MaxBinarySize = MaxAnsiStringSize;
    private const int MaxStringSize = MaxAnsiStringSize / 2;
    private const int MaxBinaryBlobSize = int.MaxValue;
    private const int MaxStringClobSize = MaxBinaryBlobSize / 2;
    private const byte MaxPrecision = 28;
    private const byte MaxScale = 5;
    private const byte MaxDateTime2 = 8;
    private const byte MaxDateTimeOffset = 10;

    private static void SetDefaultParameterSize(IDbDataParameter dbParam, SqlType sqlType)
    {
        switch (dbParam.DbType)
        {
            case DbType.AnsiString:
            case DbType.AnsiStringFixedLength:
                dbParam.Size = MaxAnsiStringSize;
                break;

            case DbType.Binary:
                if (sqlType is BinaryBlobSqlType)
                {
                    dbParam.Size = MaxBinaryBlobSize;
                }
                else
                {
                    dbParam.Size = MaxBinarySize;
                }
                break;
            case DbType.Decimal:
                dbParam.Precision = MaxPrecision;
                dbParam.Scale = MaxScale;
                break;
            case DbType.String:
            case DbType.StringFixedLength:
                dbParam.Size = IsText(dbParam, sqlType) ? MaxStringClobSize : MaxStringSize;
                break;
            case DbType.DateTime2:
                dbParam.Size = MaxDateTime2;
                break;
            case DbType.DateTimeOffset:
                dbParam.Size = MaxDateTimeOffset;
                break;
        }
    }

    private static bool IsText(IDbDataParameter dbParam, SqlType sqlType)
    {
        return (sqlType is StringClobSqlType) || (sqlType.LengthDefined && sqlType.Length > MsSql2000Dialect.MaxSizeForLengthLimitedStrings &&
            (DbType.String == dbParam.DbType || DbType.StringFixedLength == dbParam.DbType));
    }

    private static void SetVariableLengthParameterSize(IDbDataParameter dbParam, SqlType sqlType)
    {
        SetDefaultParameterSize(dbParam, sqlType);

        // Override the defaults using data from SqlType.
        if (sqlType.LengthDefined && !IsText(dbParam, sqlType))
        {
            dbParam.Size = sqlType.Length;
        }

        if (sqlType.PrecisionDefined)
        {
            dbParam.Precision = sqlType.Precision;
            dbParam.Scale = sqlType.Scale;
        }
    }

    public override IDbCommand GenerateCommand(CommandType type, SqlString sqlString, SqlType[] parameterTypes)
    {
        IDbCommand command = base.GenerateCommand(type, sqlString, parameterTypes);
        //if (IsPrepareSqlEnabled)
        {
            SetParameterSizes(command.Parameters, parameterTypes);
        }
        return command;
    }

    public override bool SupportsMultipleQueries
    {
        get { return true; }
    }

    #region IEmbeddedBatcherFactoryProvider Members

    System.Type IEmbeddedBatcherFactoryProvider.BatcherFactoryClass
    {
        get { return typeof(ProfiledSqlClientBatchingBatcherFactory); }
    }

    #endregion
}


public class ProfiledSqlClientBatchingBatcher : AbstractBatcher
{
    private int batchSize;
    private int totalExpectedRowsAffected;
    private SqlClientSqlCommandSet currentBatch;
    private StringBuilder currentBatchCommandsLog;
    private readonly int defaultTimeout;

    public ProfiledSqlClientBatchingBatcher(ConnectionManager connectionManager, IInterceptor interceptor)
        : base(connectionManager, interceptor)
    {
        batchSize = Factory.Settings.AdoBatchSize;
        defaultTimeout = PropertiesHelper.GetInt32(NHibernate.Cfg.Environment.CommandTimeout, NHibernate.Cfg.Environment.Properties, -1);

        currentBatch = CreateConfiguredBatch();
        //we always create this, because we need to deal with a scenario in which
        //the user change the logging configuration at runtime. Trying to put this
        //behind an if(log.IsDebugEnabled) will cause a null reference exception 
        //at that point.
        currentBatchCommandsLog = new StringBuilder().AppendLine("Batch commands:");
    }

    public override int BatchSize
    {
        get { return batchSize; }
        set { batchSize = value; }
    }

    protected override int CountOfStatementsInCurrentBatch
    {
        get { return currentBatch.CountOfCommands; }
    }

    public override void AddToBatch(IExpectation expectation)
    {
        totalExpectedRowsAffected += expectation.ExpectedRowCount;
        IDbCommand batchUpdate = CurrentCommand;

        string lineWithParameters = null;
        var sqlStatementLogger = Factory.Settings.SqlStatementLogger;
        if (sqlStatementLogger.IsDebugEnabled || log.IsDebugEnabled)
        {
            lineWithParameters = sqlStatementLogger.GetCommandLineWithParameters(batchUpdate);
            var formatStyle = sqlStatementLogger.DetermineActualStyle(FormatStyle.Basic);
            lineWithParameters = formatStyle.Formatter.Format(lineWithParameters);
            currentBatchCommandsLog.Append("command ")
                .Append(currentBatch.CountOfCommands)
                .Append(":")
                .AppendLine(lineWithParameters);
        }
        if (log.IsDebugEnabled)
        {
            log.Debug("Adding to batch:" + lineWithParameters);
        }
        currentBatch.Append(((ProfiledSqlDbCommand)batchUpdate).Command);

        if (currentBatch.CountOfCommands >= batchSize)
        {
            ExecuteBatchWithTiming(batchUpdate);
        }
    }

    protected void ProfiledPrepare(IDbCommand cmd)
    {
        try
        {
            IDbConnection sessionConnection = ConnectionManager.GetConnection();

            if (cmd.Connection != null)
            {
                // make sure the commands connection is the same as the Sessions connection
                // these can be different when the session is disconnected and then reconnected
                if (cmd.Connection != sessionConnection)
                {
                    cmd.Connection = sessionConnection;
                }
            }
            else
            {
                cmd.Connection = (sessionConnection as ProfiledSqlDbConnection).Connection;
            }

            ProfiledSqlDbTransaction trans = (ProfiledSqlDbTransaction)typeof(NHibernate.Transaction.AdoTransaction).InvokeMember("trans", System.Reflection.BindingFlags.GetField | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, ConnectionManager.Transaction, null);
            if (trans != null)
                cmd.Transaction = trans.Transaction;
            Factory.ConnectionProvider.Driver.PrepareCommand(cmd);
        }
        catch (InvalidOperationException ioe)
        {
            throw new ADOException("While preparing " + cmd.CommandText + " an error occurred", ioe);
        }
    }

    protected override void DoExecuteBatch(IDbCommand ps)
    {
        log.DebugFormat("Executing batch");
        CheckReaders();
        ProfiledPrepare(currentBatch.BatchCommand);
        if (Factory.Settings.SqlStatementLogger.IsDebugEnabled)
        {
            Factory.Settings.SqlStatementLogger.LogBatchCommand(currentBatchCommandsLog.ToString());
            currentBatchCommandsLog = new StringBuilder().AppendLine("Batch commands:");
        }

        int rowsAffected;
        try
        {
            rowsAffected = currentBatch.ExecuteNonQuery();
        }
        catch (DbException e)
        {
            throw ADOExceptionHelper.Convert(Factory.SQLExceptionConverter, e, "could not execute batch command.");
        }

        Expectations.VerifyOutcomeBatched(totalExpectedRowsAffected, rowsAffected);

        currentBatch.Dispose();
        totalExpectedRowsAffected = 0;
        currentBatch = CreateConfiguredBatch();
    }

    private SqlClientSqlCommandSet CreateConfiguredBatch()
    {
        var result = new SqlClientSqlCommandSet();
        if (defaultTimeout > 0)
        {
            try
            {
                result.CommandTimeout = defaultTimeout;
            }
            catch (Exception e)
            {
                if (log.IsWarnEnabled)
                {
                    log.Warn(e.ToString());
                }
            }
        }

        return result;
    }
}


public class ProfiledSqlClientBatchingBatcherFactory : IBatcherFactory
{
    public virtual IBatcher CreateBatcher(ConnectionManager connectionManager, IInterceptor interceptor)
    {
        return new ProfiledSqlClientBatchingBatcher(connectionManager, interceptor);
    }
}


public class ProfiledSqlDbCommand : ProfiledDbCommand
{
    public ProfiledSqlDbCommand(SqlCommand cmd, SqlConnection conn, MiniProfiler profiler)
        : base(cmd, conn, profiler)
    {
        Command = cmd;
    }

    public SqlCommand Command { get; set; }

    private DbTransaction _trans;

    protected override DbTransaction DbTransaction
    {
        get { return _trans; }
        set
        {
            this._trans = value;
            ProfiledSqlDbTransaction awesomeTran = value as ProfiledSqlDbTransaction;
            Command.Transaction = awesomeTran == null ? (SqlTransaction)value : awesomeTran.Transaction;
        }
    }
}



public class ProfiledSqlDbConnection : ProfiledDbConnection
{
    public ProfiledSqlDbConnection(SqlConnection connection, MiniProfiler profiler)
        : base(connection, profiler)
    {
        Connection = connection;
    }

    public SqlConnection Connection { get; set; }

    protected override DbTransaction BeginDbTransaction(System.Data.IsolationLevel isolationLevel)
    {
        return new ProfiledSqlDbTransaction(Connection.BeginTransaction(isolationLevel), this);
    }       

}


public class ProfiledSqlDbTransaction : ProfiledDbTransaction
{
    public ProfiledSqlDbTransaction(SqlTransaction transaction, ProfiledDbConnection connection)
        : base(transaction, connection)
    {
        Transaction = transaction;
    }

    public SqlTransaction Transaction { get; set; }
}
person Craig    schedule 04.07.2011
comment
Спасибо! Прекрасно работает с моей настройкой FluentNhibernate! - person Johan Wikström; 24.07.2011
comment
Спасибо за это. Я получал недопустимое исключение приведения в установщике ProfiledSqlDbCommand.DbTransaction, но как только я прокомментировал это, все, похоже, работало. Это было случайно или я просто прокомментировал что-то важное? Все мои модульные тесты, попадающие в базу данных с этим соединением, пройдены, но я мог что-то пропустить. - person longda; 31.08.2011

Попробуйте реализовать NHibernate.Connection.IConnectionProvider (вы можете просто унаследовать DriverConnectionProvider), в GetConnection() оберните IDbConnection, как вам нужно.

Подключите своего провайдера подключения с помощью клавиши Environment.ConnectionProvider в свойствах конфигурации.

person Mauricio Scheffer    schedule 16.06.2011
comment
Выглядит неплохо, но ProfiledDbConnection mvc-mini-profiler требует конкретного DbConnection, а не IDbConnection, который возвращает метод GetConnection(). :( - person Brant Bobby; 16.06.2011
comment
@Brant: это взлом, но я уверен, что вы можете использовать DbConnection. В противном случае выясните, почему mvc-mini-profiler действительно нуждается в DbConnection, возможно, он может работать с IDbConnection вместо этого. - person Mauricio Scheffer; 16.06.2011
comment
Хм, это тоже не совсем работает, кажется, NHibernate ожидает, что IDbConnection будет SqlConnection, и пытается его преобразовать. Если нет способа обойти это, я полагаю, что невозможно использовать пакет mvc-mini-profiler с NHibernate. - person Brant Bobby; 16.06.2011
comment
@Brant: это может произойти, если вы используете SQL Server с пакетной обработкой. Так что попробуйте отключить пакетирование NH. - person Mauricio Scheffer; 16.06.2011
comment
Мне действительно удалось заставить это работать с пакетной обработкой NHibernate. Взломали изрядное количество раз, но отправлю код завтра. - person Craig; 03.07.2011

Если кому-то интересно, я выполнил интеграцию с помощью специального приложения Log4net. Таким образом, я чувствую себя в безопасности и не связываюсь с объектом Connection.

Примерный план выглядит примерно так: NHibernate испускает sqlstrings как операторы отладки, а приложение, настроенное в log4net.xml, вызывает Start и Dispose в MiniProfiler.

person Konstantin    schedule 02.10.2012
comment
Привет, @ Konstantin, это звучит как новый подход. Не могли бы вы разместить ссылку на то, что вы построили? - person Steve Scheffler; 03.10.2012