Асинхронные вызовы SQL в ADO.NET

ОГЛАВЛЕНИЕ

Настройка асинхронной обработки

В данном примере мы создали два метода, показывающие, как это делается. Единственное различие между ними состоит в том, что используется реальный командный метод SQL.

Для начала нам нужен способ сообщения о том, что хранимая процедура закончила обработку, и можно продолжать выполнение нашего кода. Для этого мы будем использовать класс обработчика ManualResetEvent. Как только этот класс перешел в сигнализирующее состояние, мы хотим, чтобы он оставался в сигнализирующем состоянии. Мы объявили это состояние как член класса.

once it is signalled, I want it to stay signalled. I have declared it as a class member.

private static readonly ManualResetEvent _reset = new ManualResetEvent(false);

Настройка соединения и командных объектов для использования.

using (SqlConnection connection = new SqlConnection(ConnectionStr))
{
    using (SqlCommand cmd = new SqlCommand("AdoProcess_Test1", connection))
    {
        .....
    }
}

Поскольку команда SQL – это хранимая процедура без параметров, нам еще нужно инициализировать команду. Если вы думаете, что команда отнимет больше 30 секунд, то вам также нужно изменить значение свойства лимита времени команды. Свойство лимита времени соединения управляет тем, сколько времени занимает подключение к базе данных, а не тем, сколько времени требуется на выполнение команды/запроса.

cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = 30;     // По умолчанию 30 секунд,
        // только показывает, как устанавливать. Нет
//необходимости его устанавливать.

Теперь нужно сообщить соединению, какой метод вызывать, когда оно получает сообщения от базы данных. Сигнатурой метода является void <ИмяМетода>(object sender, SqlInfoMessageEventArgs e).

private static void ConnectionInfoMessage(object sender, SqlInfoMessageEventArgs e)
{
    if ( e.Errors.Count > 0 )
    {
        // Проверяем, что принимаются только информационные сообщения
        Console.WriteLine("Получено {0} сообщений", e.Errors.Count);
        foreach( SqlError info in e.Errors )
        {
            if ( info.Class > 9 ) // Серьезность
            {
                Console.WriteLine("Сообщение об ошибке : {0} :
            State : {1}", info.Message, info.State );
            }
            else
            {
                Console.WriteLine("Информационное сообщение : {0} :
            State : {1}", info.Message, info.State);
            }
        }
    }
    else
    {
        Console.WriteLine("Получено информационное сообщение соединения : {0}", e.Message);    
    }
}

Как видно, мы проверяем свойство Class у экземпляра SqlError. Это реальное значение серьезности, с которым был запущен метод RAISERROR. Свойство State имеет такое же имя, что и свойство State в вызове RAISERROR, почему разработчики ADO также не могли сохранить имя Серьезность. SqlInfoMessageEventArgs.Message является объединением совокупности ошибок, если ошибки присутствуют, с символом новой строки в качестве разделителя.

Сейчас у нас есть метод, который нужно подключить к объекту соединения.

cmd.Connection.InfoMessage += ConnectionInfoMessage;

Когда работа в асинхронном режиме завершается, нам нужен метод обратного вызова, чтобы обработать результаты и сообщить остальной части приложения, что работа завершена. Необходимо создать метод, который будет поддерживать интерфейс IAsyncResult, сигнатурой метода является void <ИмяМетода>(IAsyncResult result).

Поскольку мы демонстрируем выполнение Non Query и выполнение Reader, мы должны создать два метода обратного вызова, один для каждого вида используемой команды. Их именами будут NonQueryCallBack иReaderCallBack:

private static void NonQueryCallBack(IAsyncResult result)
{
    SqlCommand command = (SqlCommand) result.AsyncState;
    try
    {
        if (command != null)
        {
            Console.WriteLine("Ожидание завершения асинхронного вызова");
            command.EndExecuteNonQuery(result);
        }
    }
    catch (SqlException ex)
    {
        Console.WriteLine("Ошибка выполнения команды! - [{0}]", ex.Message);
    }
    finally
    {
        Console.WriteLine
        ("Завершился обратный вызов, поэтому сообщаем, что основной поток может продолжить выполняться....");
        _reset.Set();
    }
}

Когда выполняется command.EndExecuteNonQuery(result), он будет ждать поток обратного вызова до тех пор, пока выполнение команды не завершится или не произойдет исключение. Так или иначе, когда он завершается, нужно сообщить основному потоку, что работа закончена, поэтому в конце блока вызывается метод Set для экземпляра ManualResetEvent.

Давайте выполним команду, передав ее в объект SqlCommand как Асинхронное Состояние и метод обратного вызова.

AsyncCallback result = NonQueryCallBack;
cmd.Connection.Open();
cmd.BeginExecuteNonQuery(result, cmd);
Console.WriteLine("Ожидание завершения выполнения хранимой процедуры....");
_reset.WaitOne();

После начала выполнения можно продолжить другую работу (не закрывайте текущее соединение, или иначе команда будет отменена) или ждать до тех пор, пока она не закончится, что и делается в данном примере.

Теперь есть проблема с командой Execute Non Query (выполнить без запроса), состоящая в том, что она будет ждать до конца, перед тем как возвратит любые сообщения, как показывает выход из этого метода:

Waiting for completion of executing stored procedure....
Waiting for completion of the Async call
Received 4 messages
Info Message : Completed 25% At 15:23:19:697 : State : 1
Info Message : Completed 50% At 15:23:22:697 : State : 2
Info Message : Completed 75% At 15:23:25:697 : State : 3
Info Message : Completed 100% At 15:23:28:697 : State : 4
Completed call back so signal main thread to continue....
Completion of Non Execute Method....

Когда мы начали использовать этот метод для создания резервных копий баз данных перед обновлением, не был замечен этот эффект до тех пор, пока создание резервной копии не начало занимать больше чем пару секунд (когда наши тестовые базы данных были пустыми, это занимало только одну секунду!). Мы предположили, что не возвращаются наборы данных, которые мы должны  были использовать в методе ExecuteNonQuery, и это неправильно. Этот метод оптимизирован внутренне (мы надеемся), поэтому он не будет обрабатывать никаких сообщений до конца, и, подумав, вы поймете, что это правильное решение.

Чтобы справиться с этой проблемой, взамен нужно использовать методы BeginExecuteReader и EndExecuteReader. В примере мы создали другой набор методов, которые используют эти методы, так что давайте посмотрим на вывод.

Waiting for completion of executing stored procedure....
Waiting for completion of the Async call
Received 1 messages
Info Message : Completed 25% At 15:32:48:410 : State : 1
Received 1 messages
Info Message : Completed 50% At 15:32:51:410 : State : 2
Received 1 messages
Info Message : Completed 75% At 15:32:54:410 : State : 3
Received 1 messages
Info Message : Completed 100% At 15:32:57:410 : State : 4
Completed call back so signal main thread to continue....
Completion of Execute Reader Method....

Как видите, метод ConnectionInfoMessage запускается каждый раз, когда получено сообщение, а не в самом конце.

Также мы реализовали другой метод, чтобы показать, как запускать резервное копирование, и чтобы показать, что он возвращает сообщения такого же типа. Мы не выполняем  автоматически этот метод, потому что он пытается сделать резервную копию базы данных msdb, и вы можете не иметь к ней доступа.

Загрузить исходный код - 8.2 KB