Меню

Асинхронный запрос ado как узнать ошибку

Автор: Alex. Опубликовано в Программирование 05 Февраль 2016. просмотров: 16930

В некоторых случаях нам приходится делать асинхронную загрузку данных из базы. Ведь так пользователь может почти сразу увидеть первые фрагменты данных по мере их загрузки, не дожидаясь, пока будет загружено всё полностью. Ещё пользователь получит возможность отменить загрузку в любой момент, пока она происходит. Ну и конечно, асинхронная загрузка позволяет сделать, например, многозакладочный пользовательский интерфейс, на каждой закладке которого можно будет параллельно загружать разные данные. В этой статье я приведу решение, которое позволит сделать асинхронную пошаговую загрузку данных в таблицу в Delphi с помощью ADO.

Создание проекта

На самом деле, в ADO уже всё готово для асинхронной загрузки данных, нужно лишь выставить определённые опции и обработать необходимые события. Но в Delphi всё оказалось не настолько просто. Для отображения данных в Delphi используется таблица TDBGrid унаследованная от TCustomDBGrid (или многочисленные сторонние разработки, тоже унаследованные от TCustomDBGrid), ссылающаяся на объект TDataSource, который, в свою очередь, ссылается на TDataSet. И в случае работы с ADO вы должны использовать обёртки TADOStoredProc, TADOQuery, TADOTable и TADODataSet, которые унаследованы от объекта TDataSet. Как раз эти обёртки и не дают в полную силу пользоваться асинхронной загрузкой данных. Чтобы понять, что работает и что не работает при использовании этих обёрток, сделаем простой пример с их использованием.

Все эксперименты в статье я буду делать, используя локальную СУБД MSSQL 2008 и Delphi 10 Seattle. Для экспериментов я создал простую таблицу и заполнил её случайным набором данных. Вот скрипт, который я использовал:

CREATE TABLE ADOTestTable (NumberField int, DateField date, StringField varchar(36))
DECLARE @Counter int
SET @Counter = 0
WHILE @Counter < 1000000
BEGIN
 INSERT INTO ADOTestTable VALUES (RAND() * 2147483647, DATEADD("day", - RAND() * 365 * 15, GETDATE()), CONVERT(varchar(36), NEWID()))
 SET @Counter += 1
END

Теперь в Delphi сделаем новый проект с VCL-формой (пункт меню File -> New -> VCL Forms Application — Delphi), на форму положим таблицу TDBGrid и две кнопки TButton. Одна кнопка будет для начала загрузки, а вторая для отмены загрузки. Имена кнопкам я дам соответствующие – ButtonLoad и ButtonCancel. Все остальные имена оставлю без изменений. Кнопку отмены по умолчанию сделаем неактивной, Enabled выставим в False. Зададим, также, заголовок формы и текст на кнопках. Ещё я добавлю компонент TProgressBar, который будет нам показывать идёт ли процесс загрузки или нет и компонент TLabel для отображения количества загруженных записей. Свойство Style прогресс-бара выставим в pbstMarquee, а Visible – в False.

После того как с визуальной частью формы закончено, положим на форму компоненты TDataSource, TADOQuery и TADOConnection. Теперь нужно связать таблицу TDBGrid (у нас она получила имя DBGrid1) с источником данных TDataSource (у нас он с именем DataSource1), источник данных – с запросом TADOQuery (у нас он с именем ADOQuery1), а запрос – с соединением TADOConnection (у нас оно с именем ADOConnection1). Т.е. получается вот такая связь: DBGrid1 -> DataSource1 -> ADOQuery1 -> ADOConnection1. Для этого устанавливаем у таблицы свойство DataSource равным DataSource1, свойство источника данных DataSet равным ADOQuery1, а свойство запроса Connection равным ADOConnection1.

Пример формы Delphi для тестирования ADO

Теперь нужно задать свойства соединения в свойстве ConnectionString объекта ADOConnection1. У меня это будет следующая строка:

Provider=SQLOLEDB.1;Data Source=(local);Initial Catalog=test;Persist Security Info=True;User ID=sa;Password=123

Здесь я сразу указал имя пользователя и пароль, поэтому можно отключить стандартное окно запроса имени пользователя и пароля выставив свойству LoginPrompt компонента ADOConnection1 значение False.

Теперь придумаем такой запрос, который будет долго выполняться, и запишем его в свойство SQL компонента ADOQuery1. Заодно будем засекать, сколько будут загружаться данные для каждого из вариантов загрузки. Я сделал вот такой запрос:

SELECT TOP 10000000 * FROM ADOTestTable t1 JOIN ADOTestTable t2 ON t1.DateField = t2.DateField AND t1.StringField <> t2.StringField

У меня этот запрос в SQL Management Stodio выполнился за 1 минуту и 9 секунд.

Простой синхронный запрос данных в Delphi с помощью ADO

Для начала сделаем простой синхронный запрос данных. Выполнять запрос будем по событию OnClick кнопки ButtonLoad. Вот такой будет код для выполнения запроса:

procedure TForm1.ButtonLoadClick(Sender: TObject);
var
    startTime: TDateTime;
begin
    //Засекам время.
    startTime := Now;
    //Выполняем запрос.
    ADOQuery1.Open;
    //Отображаем количество загруженных записей и время загрузки.
    Label1.Caption := 'Загружено: ' + IntToStr(ADOQuery1.RecordCount)
        + '; Время выполнения: ' + FormatDateTime('hh:nn:ss', Now - startTime);
end;

Запустите приложение, нажмите на кнопку «Загрузить». Записи открылись даже быстрее, чем в SQL Server Management Studio, всего за 26 секунд, но при просмотре происходят подтормаживания, причём, чем ближе к концу таблицы, тем они больше. Интересный результат.

Синхронная загрузка данных в Delphi с помощью ADO

Простой асинхронный запрос данных в Delphi с помощью ADO

Теперь попробуем выполнить этот запрос асинхронно. Для этого установите у свойства ExecuteOptions компонента ADOQuery1 флажок eoAcyncExecute в True. А чтобы соединение с СУБД тоже происходило асинхронно, установите свойству ConnecOptions значение coAsyncConnect. Количество записей теперь нужно будет показывать только после того как запрос выполнится. Об этом можно узнать по событию AfterOpen компонента ADOQuery1. Ошибки можно отловить по событию OnExecuteComplete компонента ADOConnection1. Пока запрос обрабатывается, будем показывать прогресс-бар, делать активной кнопку «Отмена», а кнопку «Загрузка» — неактивной. Отменять выполняющийся запрос будем как положено в ADO – вызовом функции Cancel. Вот как будет выглядеть код:

unit Unit1;
 
interface
 
uses
    Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
    System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.DB,
    Vcl.StdCtrls, Vcl.Grids, Vcl.DBGrids, Vcl.ComCtrls, Data.Win.ADODB;
 
type
    TForm1 = class(TForm)
        DBGrid1: TDBGrid;
        ButtonLoad: TButton;
        ButtonCancel: TButton;
        DataSource1: TDataSource;
        ADOQuery1: TADOQuery;
        ADOConnection1: TADOConnection;
        ProgressBar1: TProgressBar;
        Label1: TLabel;
        procedure ButtonLoadClick(Sender: TObject);
        procedure ADOQuery1AfterOpen(DataSet: TDataSet);
        procedure ButtonCancelClick(Sender: TObject);
        procedure ADOConnection1ExecuteComplete(Connection: TADOConnection;
            RecordsAffected: Integer; const Error: Error;
            var EventStatus: TEventStatus; const Command: _Command;
            const Recordset: _Recordset);
    private
        { Private declarations }
        startTime: TDateTime;
        procedure ShowProgress(fShow: boolean);
    public
        { Public declarations }
    end;
 
var
    Form1: TForm1;
 
implementation
 
{$R *.dfm}
 
uses Winapi.ADOInt;
 
procedure TForm1.ShowProgress(fShow: boolean);
begin
    //Пока идёт загрузка, кнопка загрузки неактивна.
    ButtonLoad.Enabled := not fShow;
    //Пока идёт загрузка, кнопка отмены активна.
    ButtonCancel.Enabled := fShow;
    //Пока идёт загрузка, показываем прогресс-бар.
    ProgressBar1.Visible := fShow;
end;
 
procedure TForm1.ADOConnection1ExecuteComplete(Connection: TADOConnection;
    RecordsAffected: Integer; const Error: Error; var EventStatus: TEventStatus;
    const Command: _Command; const Recordset: _Recordset);
begin
    //Если произошла ошибка, то нужно её показать.
    if EventStatus = TEventStatus.esErrorsOccured then
    begin
        //Показываем, что процесс загрузки завершён.
        ShowProgress(false);
        //Запрашиваем здесь состояние, но ошибку не показываем, если она происходит.
        //Если здесь не запросить состояние, то будут ошибки при закрытии формы.
        try
            ADOQuery1.RecordsetState;
        except
            //
        end;
        //Если ошибка - это не отмена, то показываем ошибку.
        if Error.Number <> adErrOperationCancelled then
            Application.MessageBox(PWideChar(Error.Description),
                PWideChar(Application.Title), MB_ICONERROR);
    end;
end;
 
procedure TForm1.ADOQuery1AfterOpen(DataSet: TDataSet);
begin
    //Отображаем количество загруженных записей и время загрузки.
    Label1.Caption := 'Загружено: ' + IntToStr(ADOQuery1.RecordCount)
        + '; Время выполнения: ' + FormatDateTime('hh:nn:ss', Now - startTime);
    //Показываем, что процесс загрузки завершён.
    ShowProgress(false);
    //Заново устанавливаем свойство DataSet, чтобы обновить таблицу.
    DataSource1.DataSet := ADOQuery1;
end;
 
procedure TForm1.ButtonCancelClick(Sender: TObject);
begin
    //Отмена запроса не работает или работает некорректно.
    ADOQuery1.Recordset.Cancel;
    //Показываем, что процесс загрузки завершён.
    ShowProgress(false);
end;
 
procedure TForm1.ButtonLoadClick(Sender: TObject);
begin
    //Закрываем запрос, если он был открыт ранее.
    ADOQuery1.Close;
    //Пока идёт загрузка, количество загруженных записей не показываем.
    Label1.Caption := 'Загружено: ...';
    //Засекам время.
    startTime := Now;
    //Выполняем запрос.
    ADOQuery1.Open;
    //Показываем процесс загрузки.
    ShowProgress(true);
end;
 
end.

Запустите форму и нажмите на кнопку «Загрузить». После этого кнопки «Загрузка» и «Отмена» поменяют активность, появится прогресс-баз и начнётся загрузка, см. картинку ниже. Данные отобразятся в таблице только после загрузки. Время загрузки здесь получилось такое же – 26 секунд, и так же при просмотре происходят подтормаживания.

Процесс асинхронной загрузки данных в Delphi с помощью ADO

Отмена простого асинхронного запроса данных в Delphi с помощью ADO

Загрузка из примера выше работает прекрасно, пока вы не нажмёте на кнопку «Отмена». Здесь есть 2 проблемы:

    1. Когда данные уже начали загружаться на клиента, отмена не срабатывает. Вместо отмены программа зависает на строчке «ADOQuery1.Recordset.Cancel;», пока все данные полностью не загрузятся.
    2. После вызова функции «ADOQuery1.Recordset.Cancel;» (если конечно запрос реально отменился, см. п. 1), повторно выполнить запрос не получится, т.к. вызов метода «ADOQuery1.Open;» не произведёт никаких действий. Это происходит из-за того, что компонент ADOQuery1 остаётся закрытым (Active = False), но в то же время его статус показывает, что запрос открывается (State = dsOpening). Это наглядно показывает на то, что обёртки VCL над ADO не поддерживают отмену.

Первую проблему можно попробовать обойти одним из трёх способов:

    1. Поменять драйвер, если другой драйвер у вас есть. Но, может случиться так, что нужного вам драйвера не существует или БД не поддерживает отмену запросов.
    2. Использовать серверный курсор (для этого нужно выставить у компонента ADOQuery1 свойство CursorLocation в clUseServer). Но с серверным курсором вы не сможете узнать количество записей и для отображения данных нужно постоянное подключение к СУБД. Поэтому, чтобы работать с данными на клиенте, вам придётся скопировать их в оперативную память (например, в компонент TClientDataSet) или в локальную базу данных, если объём данных очень большой. А чтение всех данных из серверного курсора происходит медленно.
    3. Сделать фиктивную остановку загрузки. Т.е. показывать пользователю, что запрос отменён, а на самом деле давать запросу выполниться до конца и только после этого его удалять. Можно, конечно удалять компоненты TADOQuery во время загрузки (это возможно, если выставить флаг eoAsyncFetchNonBlocking в True), но загрузка данных объектами ADO при этом не остановится, она будет продолжаться. Это видно даже в диспетчере задач. К тому же при удалении компонентов TADOQuery во время загрузки периодически происходит ошибка «access violation at 0x1cbaf811: read of address 0x00000000» где-то в недрах ADO.

Вторую проблему никак не обойти. Здесь есть только одно решение – после каждой отмены (или вообще каждый раз), для каждого нового запроса создавать новый компонент TADOQuery, а существующий компонент удалять сразу или позже при завершении работы приложения.

Учитывая возникшие с отменой проблемы, код формы сильно поменяется. Я уберу с формы компоненты ADOQuery1 и ADOConnection1 и буду создавать их при каждой загрузке данных. Ниже я приведу два примера и опишу их минусы.

Вот первый пример. Здесь я использую серверный курсор для получения данных. Для отображения я копирую данные в компонент TClientDataSet. Отмена загрузки здесь работает превосходно. Но есть другие проблемы, о которых будет написано ниже.

unit Unit1;
 
interface
 
uses
    Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
    System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.DB,
    Vcl.StdCtrls, Vcl.Grids, Vcl.DBGrids, Vcl.ComCtrls, Data.Win.ADODB,
    System.Threading, Datasnap.DBClient;
 
type
    TForm1 = class(TForm)
        DBGrid1: TDBGrid;
        ButtonLoad: TButton;
        ButtonCancel: TButton;
        DataSource1: TDataSource;
        ProgressBar1: TProgressBar;
        Label1: TLabel;
        procedure ButtonLoadClick(Sender: TObject);
        procedure ButtonCancelClick(Sender: TObject);
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
    private
        { Private declarations }
        _canceled: boolean;
        error: string;
        syncWindowHandle: HWND;
        task: ITask;
        query: TADOQuery;
        connection: TADOConnection;
        clientDataSet: TClientDataSet;
        startTime, copyingStartTime: TDateTime;
        procedure WndMethod(var Message: TMessage);
        function GetCanceled: boolean;
        procedure SetCanceled(value: boolean);
        procedure FreeInstances;
        procedure ShowProgress(fShow: boolean);
        procedure ExecuteComplete(Connection: TADOConnection;
            RecordsAffected: Integer; const Error: Error;
            var EventStatus: TEventStatus; const Command: _Command;
            const Recordset: _Recordset);
        procedure AfterOpen(DataSet: TDataSet);
        property Canceled: boolean read GetCanceled write SetCanceled;
    public
        { Public declarations }
    end;
 
var
    Form1: TForm1;
 
implementation
 
{$R *.dfm}
 
uses Winapi.ADOInt;
 
procedure TForm1.FormCreate(Sender: TObject);
begin
    query := nil;
    connection := nil;
    clientDataSet := nil;
    _canceled := false;
    //Создаём окно для синхронизации.
    syncWindowHandle := AllocateHWnd(self.WndMethod);
end;
 
procedure TForm1.FormDestroy(Sender: TObject);
begin
    //Удаляем окно синхронизации.
    DeallocateHWnd(syncWindowHandle);
    //Удаляем созданные объекты.
    FreeInstances;
end;
 
procedure TForm1.FreeInstances;
begin
    //Остановка и удаление задания на копирование данных.
    if assigned(task) then
    begin
        if task.Status = TTaskStatus.Running then
        begin
            //Останавливаем копирование данных.
            Canceled := true;
            //Ждём, пока копирование остановится.
            task.Wait;
        end;
        task := nil;
        _canceled := false;
    end;
    //Отмена и удаление запроса.
    if assigned(query) then
    begin
        if assigned(query.Recordset)
                and ((query.Recordset.State and adStateExecuting) <> 0) then
            //Отменяем выполнение запроса.
            query.Recordset.Cancel;
        FreeAndNil(query);
    end;
    //Удаление соединения.
    if assigned(connection) then
        FreeAndNil(connection);
    //Удаление сохранённых в памяти данных.
    if assigned(clientDataSet) then
    begin
        DataSource1.DataSet := nil;
        FreeAndNil(clientDataSet);
    end;
end;
 
function TForm1.GetCanceled: boolean;
begin
    //Чтение свойства Canceled.
    System.TMonitor.Enter(self);
    try
        Result := _canceled;
    finally
        System.TMonitor.Exit(self);
    end;
end;
 
procedure TForm1.SetCanceled(value: boolean);
begin
    //Установка свойства Canceled.
    System.TMonitor.Enter(self);
    try
        _canceled := value;
    finally
        System.TMonitor.Exit(self);
    end;
end;
 
procedure TForm1.ShowProgress(fShow: boolean);
begin
    //Пока идёт загрузка, кнопка загрузки неактивна.
    ButtonLoad.Enabled := not fShow;
    //Пока идёт загрузка, кнопка отмены активна.
    ButtonCancel.Enabled := fShow;
    //Пока идёт загрузка, показываем прогресс-бар.
    ProgressBar1.Visible := fShow;
end;
 
procedure TForm1.ExecuteComplete(Connection: TADOConnection;
    RecordsAffected: Integer; const Error: Error; var EventStatus: TEventStatus;
    const Command: _Command; const Recordset: _Recordset);
begin
    //Если произошла ошибка, то нужно её показать.
    if EventStatus = TEventStatus.esErrorsOccured then
    begin
        //Показываем, что процесс загрузки завершён.
        ShowProgress(false);
        //Запрашиваем здесь состояние, но ошибку не показываем, если она происходит.
        //Если здесь не запросить состояние, то будут ошибки при закрытии формы.
        try
            query.RecordsetState;
        except
            //
        end;
        //Если ошибка - это не отмена, то показываем ошибку.
        if Error.Number <> adErrOperationCancelled then
            Application.MessageBox(PWideChar(Error.Description),
                PWideChar(Application.Title), MB_ICONERROR);
    end;
end;
 
procedure TForm1.AfterOpen(DataSet: TDataSet);
begin
    error := '';
    //Создаём задание для копирования данных.
    task := TTask.Create(procedure
        var
            i: integer;
        begin
            try
                i := 0;
                //Запоминаем время начала копирования.
                copyingStartTime := Now;
                //Создаём компонент TClientDataSet для хранения данных в памяти.
                clientDataSet := TClientDataSet.Create(nil);
                //Копируем структуру.
                clientDataSet.FieldDefs.Assign(query.FieldDefs);
                clientDataSet.CreateDataSet;
                //Копируем данные.
                query.First;
                while not query.Eof do
                begin
                    Inc(i);
                    clientDataSet.Append;
                    clientDataSet.CopyFields(query);
                    clientDataSet.Post;
                    //Если копирование отменено, то выходим из цикла.
                    if Canceled then
                        break;
                    query.Next;
                end;
            except
                on e: Exception do
                begin
                    //Если произошла ошибка, то сохраняем сообщение.
                    if i > 0 then
                        error := 'При копировании записи ' + IntToStr(i) + ' произошла ошибка '
                            + '(время копирования: ' + FormatDateTime('hh:nn:ss', Now - copyingStartTime) + '). ';
                    error := error + e.Message;
                end;
            end;
            //Отправляем самим себе сообщение, что загрузка завершена.
            SendMessage(syncWindowHandle, WM_USER + 1, 0, 0);
        end
    );
    //Запускаем копирование.
    task.Start;
end;
 
procedure TForm1.WndMethod(var Message: TMessage);
var
    defProc: boolean;
begin
    defProc := false;
    try
        case Message.Msg of
        //Получено сообщение, что загрузка завершена.
        WM_USER + 1:
        begin
            //Показываем, что процесс загрузки завершён.
            ShowProgress(false);
            if not Canceled then
            begin
                //Если была ошибка, то показываем её.
                if not error.IsEmpty then
                    Application.MessageBox(PWideChar(error),
                        PWideChar(Application.Title), MB_ICONERROR)
                else
                begin
                    //Отображаем количество загруженных записей и время загрузки.
                    Label1.Caption := 'Загружено: ' + IntToStr(clientDataSet.RecordCount)
                        + '; Время выполнения: ' + FormatDateTime('hh:nn:ss', Now - startTime)
                        + '; Время копирования: ' + FormatDateTime('hh:nn:ss', Now - copyingStartTime);
                    //Заново устанавливаем свойство DataSet, чтобы обновить таблицу.
                    DataSource1.DataSet := clientDataSet;
                end;
            end;
        end
        else
            defProc := true;
        end;
    except
        Application.HandleException(Self);
    end;
    if defProc then
        Message.Result := DefWindowProc(syncWindowHandle, Message.Msg, Message.wParam, Message.lParam);
end;
 
procedure TForm1.ButtonCancelClick(Sender: TObject);
begin
    if (query.Recordset.State and adStateExecuting) <> 0 then
        //Отмена запроса.
        query.Recordset.Cancel
    else if assigned(task) and (task.Status = TTaskStatus.Running) then
        //Отмена копирования данных в память.
        Canceled := true;
end;
 
procedure TForm1.ButtonLoadClick(Sender: TObject);
begin
    //Удаляем созданные объекты.
    FreeInstances;
    //Создаём компоненты.
    connection := TADOConnection.Create(nil);
    query := TADOQuery.Create(nil);
    //Настраиваем соединение.
    connection.ConnectionString := 'Provider=SQLOLEDB.1;Data Source=(local);Initial Catalog=test;Persist Security Info=True;User ID=sa;Password=123';
    connection.ConnectOptions := coAsyncConnect;
    connection.LoginPrompt := false;
    connection.OnExecuteComplete := ExecuteComplete;
    //Настраиваем запрос.
    query.Connection := connection;
    query.CursorLocation := clUseServer;
    query.ExecuteOptions := [eoAsyncExecute];
    query.CursorType := TCursorType.ctOpenForwardOnly;
    query.AfterOpen := AfterOpen;
    //10000000 записей - это слишком много для сохранения в памяти, поэтому лучше это число уменьшить.
    query.SQL.Text := 'SELECT TOP 10000000 * FROM ADOTestTable t1 JOIN ADOTestTable t2 ON t1.DateField = t2.DateField AND t1.StringField <> t2.StringField';
    //Пока идёт загрузка, количество загруженных записей не показываем.
    Label1.Caption := 'Загружено: ...';
    //Засекам время.
    startTime := Now;
    //Выполняем запрос.
    query.Open;
    //Показываем процесс загрузки.
    ShowProgress(true);
end;
 
end.

В этом примере копирование работает крайне медленно. Судите сами: примерно половина запрошенных данных копировалась аж 23 минуты и 17 секунд. Замеры с помощью профайлера показали, что очень медленно работает перемещение по данным с помощью серверного курсора (строка кода «query.Next;») — 20 минут и 19 секунд. На втором месте оказалось копирование полей (строка кода «clientDataSet.CopyFields(query);») – 2 минуты и 9 секунд. В довершение ко всему, примерно посередине загрузки произошла ошибка при копировании 5218000-ой записи: «Exception class EDBClient with message ‘Insufficient memory for this operation.’.». Так что, в компонент TClientDataSet у вас никак не удастся записать 10000000 записей.

Ошибка при добавлении большого количества данных в TClientDataSet

Поэтому, если вам нужно загрузить на компьютер пользователя большой объём данных, нужно искать другой способ. Приведённый пример загрузки и хранения данных в оперативной памяти подойдёт только для небольших объёмов.

Асинхронная пошаговая загрузка данных в Delphi с помощью ADO

Смысл пошаговой загрузки заключается в том, что пользователю не нужно ждать до конца выполнения запроса, чтобы увидеть данные. Как только первая порция данных загружена, её уже можно отобразить в таблице. После этого по мере подгрузки всё новых и новых данных их тоже нужно отображать в таблице. Таким образом, пользователь может начать просматривать таблицу почти сразу после начала загрузки и будет видеть динамику загрузки.

Есть два варианта, как это можно сделать. Первый вариант – это небольшое изменение предыдущего примера, где загрузка происходит из серверного курсора. Смысл изменения состоит в том, что мы должны сразу привязать таблицу к компоненту TClientDataSet и по мере наполнения компонента TClientDataSet, таблица сразу будет отображать новые записи. Здесь поменяются только методы AfterOpen и WndMethod:

procedure TForm1.AfterOpen(DataSet: TDataSet);
begin
    error := '';
    //Запоминаем время начала копирования.
    copyingStartTime := Now;
    //Создаём компонент TClientDataSet для хранения данных в памяти.
    clientDataSet := TClientDataSet.Create(nil);
    //Копируем структуру.
    clientDataSet.FieldDefs.Assign(query.FieldDefs);
    clientDataSet.CreateDataSet;
    //Заранее устанавливаем свойство DataSet.
    DataSource1.DataSet := clientDataSet;
    //Создаём задание для копирования данных.
    task := TTask.Create(procedure
        var
            i: integer;
        begin
            try
                i := 0;
                //Копируем данные.
                query.First;
                while not query.Eof do
                begin
                    Inc(i);
                    //Если копирование отменено, то выходим из цикла.
                    if Canceled then
                        break;
                    SendMessage(syncWindowHandle, WM_USER + 2, 0, 0);
                    query.Next;
                end;
            except
                on e: Exception do
                begin
                    //Если произошла ошибка, то сохраняем сообщение.
                    if i > 0 then
                        error := 'При копировании записи ' + IntToStr(i) + ' произошла ошибка '
                            + '(время копирования: ' + FormatDateTime('hh:nn:ss', Now - copyingStartTime) + '). ';
                    error := error + e.Message;
                end;
            end;
            //Отправляем самим себе сообщение, что загрузка завершена.
            SendMessage(syncWindowHandle, WM_USER + 1, 0, 0);
        end
    );
    //Запускаем копирование.
    task.Start;
end;
 
procedure TForm1.WndMethod(var Message: TMessage);
var
    defProc: boolean;
    bookmark: TBookmark;
begin
    defProc := false;
    try
        case Message.Msg of
        //Получено сообщение, что загрузка завершена.
        WM_USER + 1:
        begin
            //Показываем, что процесс загрузки завершён.
            ShowProgress(false);
            //Если была ошибка, то показываем её.
            if not error.IsEmpty then
                Application.MessageBox(PWideChar(error),
                    PWideChar(Application.Title), MB_ICONERROR)
            else
                //Отображаем количество загруженных записей и время загрузки.
                Label1.Caption := 'Загружено: ' + IntToStr(clientDataSet.RecordCount)
                    + '; Время выполнения: ' + FormatDateTime('hh:nn:ss', Now - startTime)
                    + '; Время копирования: ' + FormatDateTime('hh:nn:ss', Now - copyingStartTime);
            if Canceled then
                Label1.Caption := 'Загрузка отменена! ' + Label1.Caption;
        end;
        WM_USER + 2:
        begin
            //Копируем запись.
            clientDataSet.DisableControls;
            try
                bookmark := clientDataSet.GetBookmark;
                try
                    clientDataSet.Append;
                    clientDataSet.CopyFields(query);
                    clientDataSet.Post;
                finally
                    clientDataSet.GotoBookmark(bookmark);
                end;
            finally
                clientDataSet.EnableControls;
            end;
            //Правая полоса прокрутки не появляется при повторной загрузке таблицы,
            //поэтому показываем её принудительно.
            if clientDataSet.RecordCount < 2 then
                ShowScrollBar(DBGrid1.Handle, SB_VERT, true);
            //Отображаем количество загруженных записей.
            Label1.Caption := 'Загружено: ' + IntToStr(clientDataSet.RecordCount)
        end;
        else
            defProc := true;
        end;
    except
        Application.HandleException(Self);
    end;
    if defProc then
        Message.Result := DefWindowProc(syncWindowHandle, Message.Msg, Message.wParam, Message.lParam);
end;

Второй вариант – это установка флага eoAsyncFetchNonBlocking в True у компонента TADOQuery и постепенная загрузка данных в событии OnFetchProgress. Этот способ работает в 10 раз быстрее, чем предыдущий. Правда, здесь я столкнулся с одной проблемой: при выборке данных по событию OnFetchProgress невозможно установить курсор на первую запись (вызовы методов First или Prior не помогают) и, в результате, у нас происходит загрузка всех записей, кроме первой. Чтобы это избежать, будем загружать данные напрямую с помощью ADO. Пример поменяется сильно, поэтому я приведу здесь полный текст:

unit Unit1;
 
interface
 
uses
    Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
    System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.DB,
    Vcl.StdCtrls, Vcl.Grids, Vcl.DBGrids, Vcl.ComCtrls, Data.Win.ADODB,
    System.Threading, Datasnap.DBClient;
 
type
    TForm1 = class(TForm)
        DBGrid1: TDBGrid;
        ButtonLoad: TButton;
        ButtonCancel: TButton;
        DataSource1: TDataSource;
        ProgressBar1: TProgressBar;
        Label1: TLabel;
        procedure ButtonLoadClick(Sender: TObject);
        procedure ButtonCancelClick(Sender: TObject);
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
    private
        { Private declarations }
        error: string;
        syncWindowHandle: HWND;
        query: TADOQuery;
        connection: TADOConnection;
        clientDataSet: TClientDataSet;
        startTime: TDateTime;
        recordsetBookmark: Variant;
        procedure FetchProgress(DataSet: TCustomADODataSet;
            Progress, MaxProgress: Integer; var EventStatus: TEventStatus);
        procedure FetchComplete(DataSet: TCustomADODataSet;
            const Error: Error; var EventStatus: TEventStatus);
        procedure WndMethod(var Message: TMessage);
        procedure FreeInstances;
        procedure ShowProgress(fShow: boolean);
        procedure ExecuteComplete(Connection: TADOConnection;
            RecordsAffected: Integer; const Error: Error;
            var EventStatus: TEventStatus; const Command: _Command;
            const Recordset: _Recordset);
    public
        { Public declarations }
    end;
 
var
    Form1: TForm1;
 
implementation
 
{$R *.dfm}
 
uses Winapi.ADOInt;
 
procedure TForm1.FormCreate(Sender: TObject);
begin
    query := nil;
    connection := nil;
    clientDataSet := nil;
    //Создаём окно для синхронизации.
    syncWindowHandle := AllocateHWnd(self.WndMethod);
end;
 
procedure TForm1.FormDestroy(Sender: TObject);
begin
    //Удаляем окно синхронизации.
    DeallocateHWnd(syncWindowHandle);
    //Удаляем созданные объекты.
    FreeInstances;
end;
 
procedure TForm1.FreeInstances;
begin
    //Отмена и удаление запроса.
    if assigned(query) then
    begin
        if assigned(query.Recordset)
                and ((query.Recordset.State and adStateExecuting) <> 0) then
            //Отменяем выполнение запроса.
            query.Recordset.Cancel;
        FreeAndNil(query);
    end;
    //Удаление соединения.
    if assigned(connection) then
        FreeAndNil(connection);
    //Удаление сохранённых в памяти данных.
    if assigned(clientDataSet) then
    begin
        DataSource1.DataSet := nil;
        FreeAndNil(clientDataSet);
    end;
    error := '';
end;
 
procedure TForm1.ShowProgress(fShow: boolean);
begin
    //Пока идёт загрузка, кнопка загрузки неактивна.
    ButtonLoad.Enabled := not fShow;
    //Пока идёт загрузка, кнопка отмены активна.
    ButtonCancel.Enabled := fShow;
    //Пока идёт загрузка, показываем прогресс-бар.
    ProgressBar1.Visible := fShow;
end;
 
procedure TForm1.ExecuteComplete(Connection: TADOConnection;
    RecordsAffected: Integer; const Error: Error; var EventStatus: TEventStatus;
    const Command: _Command; const Recordset: _Recordset);
begin
    //Если произошла ошибка, то нужно её показать.
    if EventStatus = TEventStatus.esErrorsOccured then
    begin
        //Показываем, что процесс загрузки завершён.
        ShowProgress(false);
        //Запрашиваем здесь состояние, но ошибку не показываем, если она происходит.
        //Если здесь не запросить состояние, то будут ошибки при закрытии формы.
        try
            query.RecordsetState;
        except
            //
        end;
        //Если ошибка - это не отмена, то показываем ошибку.
        if Error.Number <> adErrOperationCancelled then
            Application.MessageBox(PWideChar(Error.Description),
                PWideChar(Application.Title), MB_ICONERROR);
    end;
end;
 
procedure TForm1.WndMethod(var Message: TMessage);
var
    defProc: boolean;
    bookmark: TBookmark;
    i, j: integer;
    goToFirst: boolean;
    fields, rows: Variant;
begin
    defProc := false;
    try
        case Message.Msg of
        //Получено сообщение, что подгружена часть данных.
        WM_USER + 1:
        begin
            try
                if (error = '') and assigned(query) then
                begin
                    goToFirst := false;
                    if not assigned(clientDataSet) then
                    begin
                        //Создаём компонент TClientDataSet для хранения данных в памяти.
                        clientDataSet := TClientDataSet.Create(nil);
                        //Копируем структуру.
                        clientDataSet.FieldDefs.Assign(query.FieldDefs);
                        clientDataSet.CreateDataSet;
                        //Заранее устанавливаем свойство DataSet.
                        DataSource1.DataSet := clientDataSet;
                        //После начала загрузки будем двигаться на первую запись.
                        goToFirst := true;
                    end
                    else if query.Recordset.RecordCount > clientDataSet.RecordCount then
                    begin
                        //Сдвигаемся на одну запись, после текущей.
                        query.Recordset.Move(1, recordsetBookmark);
                        //Сохраняем закладку для текущей записи.
                        recordsetBookmark := query.Recordset.Bookmark;
                    end;
                    if query.Recordset.RecordCount > clientDataSet.RecordCount then
                    begin
                        //Создаём массив со списком индексов полей.
                        fields := VarArrayCreate([0, clientDataSet.FieldCount - 1], varVariant);
                        for i := 0 to clientDataSet.FieldCount - 1 do
                            fields[i] := i;
                        //Выгружаем значения полей для всех появившихся записей в массив.
                        rows := query.Recordset.GetRows(query.Recordset.RecordCount - clientDataSet.RecordCount,
                            recordsetBookmark, fields);
                        //Идём на последнюю запись и сохраняем для неё закладку.
                        query.Recordset.MoveLast;
                        recordsetBookmark := query.Recordset.Bookmark;
                        //Копируем пришедшие данные.
                        clientDataSet.DisableControls;
                        try
                            bookmark := clientDataSet.GetBookmark;
                            for i := VarArrayLowBound(rows, 2) to VarArrayHighBound(rows, 2) do
                            begin
                                clientDataSet.Append;
                                for j := 0 to clientDataSet.FieldCount - 1 do
                                    clientDataSet.Fields[j].Value := rows[j, i];
                                clientDataSet.Post;
                                //Правая полоса прокрутки не появляется при повторной загрузке таблицы,
                                //поэтому показываем её принудительно.
                                if clientDataSet.RecordCount < 2 then
                                    ShowScrollBar(DBGrid1.Handle, SB_VERT, true);
                            end;
                            if goToFirst then
                                clientDataSet.First
                            else
                                clientDataSet.GotoBookmark(bookmark);
                        finally
                            clientDataSet.EnableControls;
                        end;
                    end;
                    //Отображаем количество загруженных записей.
                    Label1.Caption := 'Загружено: ' + IntToStr(clientDataSet.RecordCount);
                end;
            except
                on e: Exception do
                begin
                    //Если во время копирования происходит ошибка, то сохраняем текст ошибки...
                    error := e.Message;
                    //... и отправляем самим себе сообщение, что загрузка завершена.
                    PostMessage(syncWindowHandle, WM_USER + 2, 0, 0);
                end;
            end;
        end;
        WM_USER + 2:
        begin
            //Показываем, что процесс загрузки завершён.
            ShowProgress(false);
            //Если была ошибка, то показываем её.
            if not error.IsEmpty then
            begin
                //Если выборка данных продолжает происходить, то удаляем объекты TADOQuery и TADOConnection.
                if assigned(query) and ((query.Recordset.State and adStateFetching) <> 0) then
                    try
                        FreeAndNil(query);
                    finally
                        try
                            FreeAndNil(connection);
                        finally
                            Application.MessageBox(PWideChar(error),
                                PWideChar(Application.Title), MB_ICONERROR)
                        end;
                    end;
            end
            else
                //Отображаем количество загруженных записей и время загрузки.
                Label1.Caption := 'Загружено: ' + IntToStr(clientDataSet.RecordCount)
                    + '; Время выполнения: ' + FormatDateTime('hh:nn:ss', Now - startTime);
        end;
        else
            defProc := true;
        end;
    except
        Application.HandleException(Self);
    end;
    if defProc then
        Message.Result := DefWindowProc(syncWindowHandle, Message.Msg, Message.wParam, Message.lParam);
end;
 
procedure TForm1.FetchComplete(
    DataSet: TCustomADODataSet; const Error: Error;
    var EventStatus: TEventStatus);
begin
    //Отправляем самим себе сообщение, что загружена часть данных.
    SendMessage(syncWindowHandle, WM_USER + 1, 0, 0);
    //Отправляем самим себе сообщение, что загрузка завершена.
    SendMessage(syncWindowHandle, WM_USER + 2, 0, 0);
end;
 
procedure TForm1.FetchProgress(
    DataSet: TCustomADODataSet; Progress, MaxProgress: Integer;
    var EventStatus: TEventStatus);
begin
    //Отправляем самим себе сообщение, что загружена часть данных.
    SendMessage(syncWindowHandle, WM_USER + 1, 0, 0);
end;
 
procedure TForm1.ButtonCancelClick(Sender: TObject);
begin
    if (query.Recordset.State and adStateExecuting) <> 0 then
        //Отмена запроса.
        query.Recordset.Cancel
    else
    begin
        //Для отмены загрузки просто удаляем объекты TADOQuery и TADOConnection.
        try
            FreeAndNil(query);
        finally
            try
                FreeAndNil(connection);
            finally
                //Показываем, что процесс загрузки завершён.
                ShowProgress(false);
                Label1.Caption := 'Загрузка отменена! ' + Label1.Caption;
            end;
        end;
    end;
end;
 
procedure TForm1.ButtonLoadClick(Sender: TObject);
begin
    //Удаляем созданные объекты.
    FreeInstances;
    //Создаём компоненты.
    connection := TADOConnection.Create(nil);
    query := TADOQuery.Create(nil);
    //Настраиваем соединение.
    connection.ConnectionString := 'Provider=SQLOLEDB.1;Data Source=(local);Initial Catalog=test;Persist Security Info=True;User ID=sa;Password=123';
    connection.ConnectOptions := coAsyncConnect;
    connection.LoginPrompt := false;
    connection.OnExecuteComplete := ExecuteComplete;
    //Настраиваем запрос.
    query.Connection := connection;
    query.ExecuteOptions := [eoAsyncExecute, eoAsyncFetchNonBlocking];
    query.OnFetchProgress := FetchProgress;
    query.OnFetchComplete := FetchComplete;
    query.CursorType := TCursorType.ctOpenForwardOnly;
    //10000000 записей - это слишком много для сохранения в памяти, поэтому лучше это число уменьшить.
    query.SQL.Text := 'SELECT TOP 10000000 * FROM ADOTestTable t1 JOIN ADOTestTable t2 ON t1.DateField = t2.DateField AND t1.StringField <> t2.StringField';
    //Пока идёт загрузка, количество загруженных записей не показываем.
    Label1.Caption := 'Загружено: ...';
    //Засекам время.
    startTime := Now;
    //Устанавливаем закладку в начало.
    recordsetBookmark := 1;
    //Выполняем запрос.
    query.Open;
    //Показываем процесс загрузки.
    ShowProgress(true);
end;
 
end.

Асинхронная пошаговая загрузка нескольких наборов данных в Delphi с помощью ADO

Если вам требуется асинхронно загрузить несколько наборов данных, так же как это сделано в предыдущем примере, то вы можете даже не пытаться это делать. Дело в том, что когда вы будете переходить к следующему набору данных с помощью «query.Recordset := query.NextRecordset(recordsAffected);», в этот момент компонент TADOQuery закрывает курсор и больше вы ничего не получите. Да, это прекрасно работает при синхронных запросах, но только не по событию OnFetchProgress. Пример с чтением из серверного курсора тоже работать не будет.

Но если вы всё-таки захотите асинхронно пошагово загружать данные с несколькими наборами, то используйте ADO напрямую. Кода, конечно, будет намного больше, но в этом случае всё будет работать. В статье я не буду приводить таких примеров, т.к. это будет довольно объёмный код. Но если вы заинтересованы, пишите об этом в комментариях.

Tags:
ADO
Учебники по программированию
Delphi

Добавить комментарий

Решил тут воспользоваться возможностью асинхронного исполнения запросов в ADO. Но, столкнулся с проблемой корректной обработки ошибок. 

Для демонстрации, сделал простейший пример.
1. Бросил на форму компонент Memo1 для протоколирования происходящих событий

2. Бросил на форму компонент ADOConnection1 назначил ему два обработчика событий:

OnBeforeConnect — для настройки соединения с демонстрационной БД поставляемой с Delphi:

Код

procedure TForm1.ADOConnection1BeforeConnect(Sender: TObject);
begin
  ADOConnection1.ConnectionString := 'FILE NAME=' + DataLinkDir + 'DBDEMOS.UDL';
  ADOConnection1.LoginPrompt := False;
end;

OnExecuteComplete — для протоколирования в Memo1 результата выполнения запросов

Код

procedure TForm1.ADOConnection1ExecuteComplete(Connection: TADOConnection;
  RecordsAffected: Integer; const Error: Error; var EventStatus: TEventStatus;
  const Command: _Command; const Recordset: _Recordset);
begin
  case EventStatus  of
    esOK            : Memo1.Lines.Add('ExecuteComplete esOK ('+IntToStr(RecordsAffected)+')');
    esErrorsOccured : Memo1.Lines.Add('ExecuteComplete esErrorsOccured: '+Error.Description);
    esCantDeny      : Memo1.Lines.Add('ExecuteComplete esCantDeny');
    esCancel        : Memo1.Lines.Add('ExecuteComplete esCancel');
    esUnwantedEvent : Memo1.Lines.Add('ExecuteComplete esUnwantedEvent');
    else              Memo1.Lines.Add('ExecuteComplete unknown '+IntToStr(Ord(EventStatus)));
  end;
  EventStatus := esUnwantedEvent;
end;

3. Бросил на форму компонент Button1 и назначил ему обработчик события OnClick для отключения ADOConnection1 от БД:

Код

procedure TForm1.Button1Click(Sender: TObject);
begin
  try
    if  ADOConnection1.Connected  then begin
      Memo1.Lines.Add('Before do Close');
      ADOConnection1.Close;
      Memo1.Lines.Add('After do Close');
    end;
  except on e :Exception do
    Memo1.Lines.Add('Exeption on Close: '+e.Message);
  end;
end;

4. Бросил на форму компонент Button2 и назначил ему обработчик события OnClick для запуска асинхронного исполнения запроса:

Код

procedure TForm1.Button2Click(Sender: TObject);
begin
  Memo1.Clear;
  try  ADOConnection1.Close;  except  end;
  try
    Memo1.Lines.Add('Before do valid Execute');
    ADOConnection1.Execute('DELETE FROM [Customer] WHERE CustNo<>CustNo',cmdText,[eoAsyncExecute]);
    Memo1.Lines.Add('After do valid Execute');
  except on e :Exception do
    Memo1.Lines.Add('Exeption on valid Execute: '+e.Message);
  end;
end;

Не пугайтесь, реально, этот запрос ничего не удалит т.к. условие CustNo<>CustNo в принципе не выполнимо smile

Теперь, запускаем проект и жмем Button2, дожидаемся появления в Memo1 строк:

Код

Before do valid Execute
After do valid Execute
ExecuteComplete esOK (0)

После этого жмем Button1, и в итоге в Memo1 имеем:

Код

Before do valid Execute
After do valid Execute
ExecuteComplete esOK (0)
Before do Close
After do Close

Т.е., все как и предполагалось. 
А теперь, усложним ситуацию. Для этого добавим еще одну кнопку Button3 со следующим кодом обработчиком OnClick:

Код

procedure TForm1.Button3Click(Sender: TObject);
begin
  Memo1.Clear;
  try  ADOConnection1.Close;  except  end;
  try
    Memo1.Lines.Add('Before do invalid Execute');
    ADOConnection1.Execute('DELETE FROM [НесуществующаяТаблица] WHERE CustNo<>CustNo',cmdText,[eoAsyncExecute]);
    Memo1.Lines.Add('After do invalid Execute');
  except on e :Exception do
    Memo1.Lines.Add('Exeption on invalid Execute: '+e.Message);
  end;
end;

Обратите внимание на то что в исполняемом запросе идет обращение к несуществующей в БД таблице. Т.е., этот запрос не может быть исполнен без ошибки.
А теперь, запустим проект и попытаемся выполнить этот ошибочный запрос нажатием Button3.
В Memo1 получим:

Код

Before do invalid Execute
After do invalid Execute
ExecuteComplete esErrorsOccured: Ядро базы данных Microsoft Jet не может найти входную таблицу или запрос 'НесуществующаяТаблица'.  Проверьте существование таблицы или запроса и правильность имени.

Т.е., вроде все как положено. Но, интересное начинается потом.
Теперь, нажмем кнопку Button1 для закрытия соединения. И, …. вместо нормального закрытия получаем исключение. Т.е. содержимое Memo1 станет таким:

Код

Before do invalid Execute
After do invalid Execute
ExecuteComplete esErrorsOccured: Ядро базы данных Microsoft Jet не может найти входную таблицу или запрос 'НесуществующаяТаблица'.  Проверьте существование таблицы или запроса и правильность имени.
Exeption on Close: Ядро базы данных Microsoft Jet не может найти входную таблицу или запрос 'НесуществующаяТаблица'.  Проверьте существование таблицы или запроса и правильность имени

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

ИТОГО.
Возникновение ошибки при асинхронном исполнернии SQL-оператора, ADO, запоминает это, и при первом следующем обращении возбуждает соответствующее исключение.
Так вот вопрос в том, как сделать так что бы после обработки ошибки в OnExecuteComplete я не получал этого исключения при последующем обращении к ADO?

P.S.
Особенно забавно получается если в приведенном выше тесте после нажатия на Button3 закрыть программу не нажимая на Button1. В этом случае в программе возникают утечки памяти. Это показывает что разработчики Delphi сами не расчитывали на подобный ход событий.

Written on 22 Мая 2011.

Как использовать асинхронные вызовы SQL, чтобы получать сообщения о ходе работы

Введение

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

Нашей первоначальной идеей было показать, как вы можете ретранслировать клиенту сообщения оператора  SQL BACKUP, но если у вас нет достаточно большой базы данных, вы не заметите проблему, о которой мы расскажем. Нет существенного различия между выполнением хранимой процедуры и команды BACKUP, обе отправляют свои информационные сообщения как ошибки (которыми они не являются). Обе используют RAISERROR с NOWAIT, из которой мы можем принимать сообщения. Мы предполагаем, что BACKUP имеет другой метод для возврата информационных сообщений. Для хранимых процедур есть только команда RAISERROR.

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

Почему вы хотите это сделать?

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

Используемая хранимая процедура

Следующая хранимая процедура будет использоваться в следующих примерах:

CREATE PROCEDURE AdoProcess_Test1
AS
BEGIN
    SET NOCOUNT ON     -- Используется для повышения производительности и с целью
            -- убедиться, что обратно не отправляется ненужная информация
    DECLARE @time VARCHAR(16)
    DECLARE @time VARCHAR(16)
    SET @time = CONVERT(VARCHAR(16),GETDATE(),114)
    RAISERROR( 'Completed 25%% At %s', 2, 1, @time) WITH NOWAIT
    WAITFOR DELAY '00:00:03'
    SET @time = CONVERT(VARCHAR(16),GETDATE(),114)
    RAISERROR( 'Completed 50%% At %s', 2, 2, @time) WITH NOWAIT
    WAITFOR DELAY '00:00:03'
    SET @time = CONVERT(VARCHAR(16),GETDATE(),114)
    RAISERROR( 'Completed 75%% At %s', 1, 3, @time) WITH NOWAIT
    WAITFOR DELAY '00:00:03'
    SET @time = CONVERT(VARCHAR(16),GETDATE(),114)
    RAISERROR( 'Completed 100%% At %s', 1, 4, @time) WITH NOWAIT
END;

Самыми важными строками являются те, которые начинаются с RAISERROR. Для этого примера мы жестко запрограммировали строки сообщений внутри хранимой процедуры. Обычной рекомендацией является добавление пользовательских сообщений в базу данных и ссылка на номер сообщения в RAISERROR, смотрите документацию MSDN. В строке сообщения используется такой же синтаксис, что и для функции C printf. Вот почему двойной символ %% присутствует в строке.

Формат строки RAISERROR такой:

RAISERROR( <Message/MessageId>, <Severity>, <State>[, <Arguments>]) [WITH <Options>]
  • Message/MessageId (сообщение/идентификатор сообщения) —  Реальное сообщение, которое будет возвращаться клиенту, можно использовать символы-заполнители printf для указания расположения переменных.
  • Severity (серьезность) сообщения – Меньше 10 для информационных сообщений.
  • State (Состояние) – Это уникальный номер, обозначающий, где в хранимой процедуре была возбуждена RAISERROR.
  • Arguments (Аргументы) – Необязательные аргументы для Сообщения, если используются символы-заполнители printf.
  • Options (Параметры) – Дополнительные параметры, в нашем случае мы хотим возвращать сообщение, как только оно было возбуждено, поэтому используется параметр NOWAIT. Вы можете отправлять сообщения в журнал регистрации событий, если нужно.

Пример имеет пару вспомогательных функций, которые подключаются к базе данных и создают/удаляют хранимую процедуру автоматически в tempdb. Поэтому вам необходимо разрешение (полномочия) CREATE/DROP PROCEDURE в tempdb.

Связывающая строка

Связывающая строка является одной из самых важных вещей во всей установке. Для данного примера мы жестко закодировали ее внутри класса C#, но обычно она помещается в файл конфигурации приложения или веб-приложения.

private const string ConnectionStr = "Data Source=(local);
    Initial Catalog=tempdb;Trusted_Connection=SSPI;Asynchronous Processing=true";

Полный список всех параметров, которые вы можете использовать при подключении к SQL, смотрите в документации MSDN. Описание используемых  нами параметров приводится ниже:

  • Источник данных – Это сервер, к которому вы собираетесь подключаться. Мы используем собственный локальный сервер. Если вы установили версию SQL Express с Visual Studio, то вам нужно изменить имя сервера с (локальный) на (локальный)SQLEXPRESS.
  • Исходный каталог – Это имя реальной базы данных, размещающейся на сервере SQL, которую вы будете использовать. Если вы не имеете полномочий CREATE/DROP в tempdb, то измените значение параметра на имя той базы данных, в которой у вас есть эти полномочия.
  • Защищенное соединение – Этот параметр сообщает клиентской программе SQL, что вы используете ваши параметры доступа в Windows для возможностей технологии единого входа (Single Sign On). Рекомендуется использовать SSPI вместо значения истина.
  • Асинхронная обработка – Чтобы заставить ADO.NET выполнять асинхронную обработку (вызов методов Begin (начать) на командном объекте), этот параметр должен иметь значение trueили в противном случае вы получите InvalidOperationException в примере. (истина),

Для реальных приложений мы обычно добавляем параметры ‘Имя приложения’ и ‘Идентификатор Workshare’ в соединение. DBA знает, которое приложение/компьютер вызывает проблему в базе данных (поддерживает дружественность с DBA!).

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

В данном примере мы создали два метода, показывающие, как это делается. Единственное различие между ними состоит в том, что используется реальный командный метод 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

 
Zikurat ©
 
(2014-12-21 14:50)
[0]

Долго разбирался и наконец докопался до крайне странного глюка, когда ADO после запроса к MS SQL оказывается в очень непонятном состоянии.

Проблема точно воспроизводится на MS SQL 2005 + Delphi XE, другое не тестировал, но было бы любопытно, если камрады сообщат.

Итак, исполнение запроса и фетчинг ставим в асинхронный режим:

ADOQuery1.ExecuteOptions := ADOQuery1.ExecuteOptions + [eoAsyncExecute] + [eoAsyncFetch];

Запрос в ADOQuery1.SQL.Text должен быть таким:

SET NOCOUNT ON;

declare @max_timestamp timestamp;

select @max_timestamp=max(timestamp) from
 (
     select max(timestamp) timestamp from Table1
     union
     select max(timestamp) timestamp from Table2
 ) t;

select @max_timestamp;

Суть запроса — выбираем максимальный Timestamp из двух таблиц. Вроде все просто.

Для повторения глюка в Table1 нужно иметь записи, а вот Table2 должна быть пустой! Это обязательное условие, также обязателен union, обязательно вводить переменную @max_timestamp, иначе опять же глюка не получится (если например просто select сделать, а не в переменную).

Итогом выполнения является то, что рекордсет сходит с ума, в нем как будто ничего нету, работать с ним нельзя, события фетчинга не происходит.


 
Zikurat ©
 
(2014-12-21 14:55)
[1]

Запрос можно переделать на, допустим, такой:

SET NOCOUNT ON;

select 5 MyColumn;  --  <-- ДОБАВИЛИ

declare @max_timestamp timestamp;

select @max_timestamp=max(timestamp) from
 (
     select max(timestamp) timestamp from Table1
     union
     select max(timestamp) timestamp from Table2
 ) t;

select @max_timestamp;

Тогда запрос проходит, событие фетчинга — всё ок, понятно что мы получили набор данным в виде колонки MyColumn и значения 5.
При попытке перейти на следующий рекордсет — пишется ошибка, что рекордсет не открыт!

var
 i: integer;
...
 ADOQuery1.Recordset := ADOQuery1.NextRecordset(i); // исключение

Если же сделать наоборот:

SET NOCOUNT ON;

declare @max_timestamp timestamp;

select @max_timestamp=max(timestamp) from
 (
     select max(timestamp) timestamp from Table1
     union
     select max(timestamp) timestamp from Table2
 ) t;

select @max_timestamp;

select 5 MyColumn;  --  <-- ДОБАВИЛИ

То есть, первым подставить наш глючный рекордсет, то тогда при выполнении:

var
 i: integer;
...
 ADOQuery1.Recordset := ADOQuery1.NextRecordset(i);

то ADO намертво навсегда зависнет.


 
Zikurat ©
 
(2014-12-21 14:57)
[2]

Стоит добавить в Table2 хоть одну запись — все начинает работать супер.

Стоит написать select iNull(max(timestamp), 0) timestamp from Table2 — все опять же начинает работать.

В таком варианте (без ввода переменной):

SET NOCOUNT ON;

declare @max_timestamp timestamp;

select max(timestamp) timestamp from
 (
     select max(timestamp) timestamp from Table1
     union
     select max(timestamp) timestamp from Table2
 ) t;

Тоже отлично работает.

Интересно — что за бага?!


 
Ega23 ©
 
(2014-12-21 16:52)
[3]

select @max_timestamp=max(timestamp) from
 (
     select max(timestamp) timestamp from Table1
     union
     select max(timestamp) timestamp from Table2
 ) t;

А если так


select @max_timestamp=isnull(max(t.ts), 0) from
 (
     select isnull(max(timestamp), 0) ts from Table1
     union
     select isnull(max(timestamp), 0) ts from Table2
 ) t;


 
Zikurat ©
 
(2014-12-21 17:08)
[4]


> А если так

а если так — то смотри пост [2]


 
Ega23 ©
 
(2014-12-21 18:40)
[5]

Тогда в чём проблема? null union something даёт null


 
Zikurat ©
 
(2014-12-21 19:44)
[6]


> null union something даёт null

Олег, ты жжошь ;))

Во-первых, null union something дает не null, а массив записей, где есть null и something

Не надо путать логические условия и объединение результатов запроса.

Во-вторых, max от (null union something) результатом дает something, если он не null.

В-третьих, как это всё может объяснить зависание и глюки ADO? Если БД + провайдер на некоторый запрос возвращает null — это абсолютно нормально, если же возвращается нечто, к чему нельзя обратиться, а в некоторых ситуациях происходит глобальное зависание + потери памяти при освобождении объектов ADO — причем здесь null?!


 
Zikurat ©
 
(2014-12-21 19:51)
[7]

К тому же, ежу понятно, что в случае наличия записей в Table1 и отсутствия записей в Table2 — результат выборки будет зависеть исключительно от записей Table1. Добавление записей в Table2 с timestamp ниже, чем у записей в Table1 по идее ничего не должен изменить с точки зрения результата. Все равно будет выбрана максимальная запись из Table1.

Но по факту добавление любой записи в Table2 приводит к пропаданию глюка, при том, что результат запроса не изменяется.


 
sniknik ©
 
(2014-12-22 00:56)
[8]

> Все равно будет выбрана максимальная запись из Table1.
она будет выбрана в переменную, рекордсет же первый будет пустой

Выполнено применительно к -1 записям. (no recordset)
            применительно к 1 записям

для
declare @max_timestamp timestamp;

select @max_timestamp=max(ts) from
 (
     select max(ts) ts from Table1
     union
     select max(ts) ts from Table2
 ) t;

select @max_timestamp;

нокаунт не нужен, тут нет инсертов (хотя и с ним пробовал), а timestamp для имен полей … зарезервированное слово (может из-за этого глюк? хотя и так тоже пробовал).
нет глюка.

попробуй заменить ADOQuery на ADODataSet.


 
sniknik ©
 
(2014-12-22 01:21)
[9]

> попробуй заменить ADOQuery на ADODataSet.
не получится… понял в чем проблема.

это описанный случай, первый пустой рекордсет от переменной
> Выполнено применительно к -1 записям. (no recordset)
>            применительно к 1 записям

а вот что получается если обе таблицы имеют данные
> Выполнено применительно к 1 записям
первого как бы не существует, причем не зависимо включен SET NOCOUNT ON; или выключен SET NOCOUNT OFF;

я в тесте на Command-ах все делаю поэтому и не увидел сначала.


 
Zikurat ©
 
(2014-12-23 18:18)
[10]

sniknik, не совсем полностью я тебя понял.
У меня по сути вопроса два:

1) что это за глюк, MS SQL / ADO?
Интересно было бы даже потеоретизировать…

2) на примере TADOQuery интересно как то можно понять, что мы получили «глючный» датасет? И если он глючный — как то можно переключиться на следующий? Потому что при попытке переключения на следующий — идет зависание, если использовать NextRecordset (см. [1] в конце).


 
sniknik ©
 
(2014-12-26 02:05)
[11]

> 1) что это за глюк, MS SQL / ADO?
ИМХО MS SQL, возвращает на «не возвращающей» команде. все равно, что дельфя при присвоении переменной (i:= 1;) вдруг начала бы форму рисовать. в ADO уже последствия.

> И если он глючный — как то можно переключиться на следующий?
на датасете не получится, он для данных, их обработки, что и приводит к глюку при отсутствующих/кривых данных. а вот на ADOCommand-ах, рекордсетах, легко (вон я даже не заметил поначалу). + там и ошибку возвращенную прочитать можно.


 
sniknik ©
 
(2014-12-26 08:01)
[12]

> + там и ошибку возвращенную прочитать можно.
вот, что получается при чтении ошибки каждого рекордсета —

8153 : Внимание! Значение NULL исключено в статистических или других операциях SET.
RecAff — 1


 
sniknik ©
 
(2014-12-26 08:18)
[13]

http://msdn.microsoft.com/ru-ru/library/ms190368%28SQL.90%29.aspx

мелкософт не считает это ошибкой, предупреждением, а то что из-за этого (возврата этого предупреждения) ломается логика обработки данных… ну это проблема обработчика.

т.е. глюк c их точки зрения не их а delphi. хотя причиной стало внутреннее предупреждение.


 
Zikurat ©
 
(2014-12-26 18:29)
[14]


> 8153 : Внимание! Значение NULL исключено в статистических
> или других операциях SET.
> RecAff — 1

где ты всё это читаешь?!

Я вот переделал на ADOCommand, свой запрос также ввожу в CommandText, исполняю Execute, точно также выполнение запроса определяется по событию у TAdoConnect.OnExecuteComplete, там статус, что запрос прекрасно исполнился (EventStatus = esOk). Если попытаться присвоить Recordset, то всплывает, что рекордсет не открыт.

procedure TForm1.ADOConnection1ExecuteComplete(Connection: TADOConnection;
 RecordsAffected: Integer; const Error: Error;
 var EventStatus: TEventStatus; const Command: _Command;
 const Recordset: _Recordset);
begin
 if EventStatus = esOk then
   MyAdoDataset.Recordset := Recordset;
...

А откуда ты свои ошибки берешь?!


 
sniknik ©
 
(2014-12-26 22:03)
[15]

> А откуда ты свои ошибки берешь?!
у меня нет ошибок… 🙂

сообщение об ошибке MSSQL из ADO, как говорил —
> а вот на ADOCommand-ах, рекордсетах, легко
у тебя же  MyAdoDataset.Recordset := Recordset; а он —
> для данных, их обработки, что и приводит к глюку при отсутствующих/кривых данных.

вот так для твоего глючного случая получится
var
 V: OleVariant;  
begin
 if EventStatus = esOk then
   MyAdoDataset.Recordset := Recordset.NextRecordset(V);


 
sniknik ©
 
(2014-12-26 22:16)
[16]

> где ты всё это читаешь?!
вариантно, смотря как/для чего писать
коллекция
ADOConnection1.Errors
событие
ADOConnection1InfoMessage
обьект
Conn: _Connection;
Conn:= CreateComObject(CLASS_Connection) as _Connection;
Conn.Errors.Item


 
Zikurat ©
 
(2014-12-29 13:40)
[17]

В моем запросе у меня  Errors.Count = 0

Событие InfoMessage не возникает ((

Я так и не понял как ты получаешь эти предупреждения…


 
Zikurat ©
 
(2014-12-29 13:40)
[18]

В моем запросе у меня  Errors.Count = 0

Событие InfoMessage не возникает ((

Я так и не понял как ты получаешь эти предупреждения…


 
sniknik ©
 
(2014-12-30 07:54)
[19]

> В моем запросе у меня  Errors.Count = 0
режим асинхронный, прошу заметить… получить сразу после выполнения запроса не получится. нужно подождать, проверить статус исполнения (чего там конкретно не помню уже).

> Событие InfoMessage не возникает (
у тебя ошибка в коде, выше уже несколько раз про датасеты говорил, не могут они глючного рекордсета разобрать, а глючный первым идет… валится до всех событий. у меня на «командах» нет разбора, нет ошибки, есть событие.


 
Zikurat ©
 
(2014-12-30 16:03)
[20]


> режим асинхронный, прошу заметить.

конечно, я это учитываю


> нужно подождать, проверить статус исполнения

естественно, возникает ADOConnectionExecute, в котором всегда все ок, то есть esOk


> е могут они глючного рекордсета разобрать, а глючный первым
> идет.

да, откуда и вопрос — где ты взял эти сообщения аля:

8153 : Внимание! Значение NULL исключено в статистических или других операциях SET.

Я их ни разу не видел. Рекордсет глючный получается, да. Если в том же ADOConnectionExecute попробовать присвоить рекордсет в существующий датасет:

procedure TForm1.ADOConnection1ExecuteComplete(Connection: TADOConnection;
 RecordsAffected: Integer; const Error: Error;
 var EventStatus: TEventStatus; const Command: _Command;
 const Recordset: _Recordset);
begin
 if EventStatus = esOk then
   MyAdoDataset.Recordset := Recordset;
...

то получим исключение, что рекордсет «not open»


 
sniknik ©
 
(2014-12-30 23:03)
[21]

> Я их ни разу не видел.
ты не читаешь ответы. игнорируешь их. не правишь код. правильно… откуда их тебе увидеть.

> Если в том же ADOConnectionExecute попробовать присвоить рекордсет в существующий датасет:
НЕ ПРИСВАИВАЙ ДАТАСЕТУ! датасет ГЛЮЧИТ с «кривым» рекордсетом.


 
Zikurat ©
 
(2014-12-31 09:49)
[22]

Мы ходим по кругу… ты можешь простым языком объяснить из какого свойства события ты берешь ошибку предупреждение про set


 
sniknik ©
 
(2014-12-31 15:07)
[23]

sniknik ©   (26.12.14 22:16) [16]


 
Zikurat ©
 
(2014-12-31 16:19)
[24]

Ну что же такое … я же тоже написал что errors.count у меня равен нулю!!

Естественно я дождался окончания выполнения запроса возник ado connection execute complete !! И через 10 секунд в errors пусто после этого.


 
sniknik ©
 
(2014-12-31 17:48)
[25]

> я же тоже написал что errors.count у меня равен нулю!!
ошибка в коде.

> Естественно я дождался окончания выполнения запроса возник ado connection execute complete !! И через 10 секунд в errors пусто после этого.
скорее всего курсор локальный, при нем ошибки не передаются… вроде, не  помню. вот сообщения (print) так точно, разбирался этим как то одно время.


 
Zikurat ©
 
(2014-12-31 18:17)
[26]


> ошибка в коде.

не понял я этой фразы.


> вроде, не  помню. вот сообщения (print) так точно, разбирался
> этим как то одно время.
>

Николай… у меня ощущение, что ты надо мной издеваешься :((… Можешь ты привести хоть какой-то намек на кусок кода, который может в моем запросе:

SET NOCOUNT ON;

declare @max_timestamp timestamp;

select @max_timestamp=max(timestamp) from
 (
     select max(timestamp) timestamp from Table1
     union
     select max(timestamp) timestamp from Table2
 ) t;

select @max_timestamp;

Отловить твою ошибку:

8153 : Внимание! Значение NULL исключено в статистических или других операциях SET.

У меня НЕ получается! Все события говорят о том, что запрос исполнился, все хорошо. Единственная «фигня» — это что рекордсет присвоить нельзя. Но вот твоего текста об ошибки я нигде не вижу.


 
sniknik ©
 
(2014-12-31 19:54)
[27]

> Николай… у меня ощущение, что ты надо мной издеваешься :((…
а мне что ты.

> Можешь ты привести хоть какой-то намек на кусок кода, который может в моем запросе:
запрос <> код.

> Единственная «фигня» — это что рекордсет присвоить нельзя.
РЕКОРДСЕТ можно, но не ДАТАсету, сколько раз повторять. вот так sniknik ©   (26.12.14 22:03) [15] в твоем случае присвоить получится, хотя даст ошибку в нормальном, т.к. без проверки а второго там не будет.


 
Zikurat ©
 
(2015-01-01 17:50)
[28]

слушай, можешь показать компонент, участок кода, свойство или что-то такое, откуда можно извлечь ошибку

«8153 : Внимание! Значение NULL исключено в статистических или других операциях SET.»

при моем запросе?


 
sniknik ©
 
(2015-01-02 12:57)
[29]

> слушай, можешь показать компонент, участок кода, свойство или что-то такое, откуда можно извлечь ошибку
могу, но не покажу… слишком долго говорим об этом, слишком много моих ответов ты проигнорировал, ничего не делая, не меняя подход, просто повторяя свой первый вопрос, не пытаясь хотя бы попробовать изменить что-то по моим словам…
все ответы уже даны, если бы хотел уже бы получил результат, если бы не получалось изменить по моим словам давно бы переспросил по конкретному…

на тупое повторение первого поста иногда разбавленное словами «не получается», я давно уже не даю кода. не адекватно ведешь беседу с моей точки зрения.
хочешь результата, вернись к началу и ПОПЫТАЙСЯ.


 
Zikurat ©
 
(2015-01-02 13:28)
[30]

Ясно,  спасибо хотя бы за что-то


Правила форума
Темы, в которых будет сначала написано «что нужно сделать», а затем просьба «помогите», будут закрыты.
Читайте требования к создаваемым темам.

MOV
Постоялец
Постоялец
 
Сообщения: 414
Зарегистрирован: 13.03.2004 (Сб) 15:13
Откуда: Санкт-Петербург

Асинхронная обработка в ADO

Как можно сделать, чтобы

при вызове

AODDB.Connection.execute «my_procedure»

чтобы прога не висла.

представляю, что можно перед ее запуском пустить таймер и в нем ее перерисовывать и делать всякие другие нужные штуки пока выполняется проца, но может.

можно через АПИ многопоточную обработку,

но может само АДО позволяет запустить процу и выполнять прогу дальше?


alibek
Большой Человек
Большой Человек
 
Сообщения: 14204
Зарегистрирован: 19.04.2002 (Пт) 11:40
Откуда: Russia

Сообщение alibek » 14.08.2006 (Пн) 14:58

Почитай про аргумент Option, в частности, про флаг adAsyncExecute.

Lasciate ogni speranza, voi ch’entrate.


MOV
Постоялец
Постоялец
 
Сообщения: 414
Зарегистрирован: 13.03.2004 (Сб) 15:13
Откуда: Санкт-Петербург

Сообщение MOV » 14.08.2006 (Пн) 15:37

Спасибо! Оно!!! Ура! :)


MOV
Постоялец
Постоялец
 
Сообщения: 414
Зарегистрирован: 13.03.2004 (Сб) 15:13
Откуда: Санкт-Петербург

Сообщение MOV » 14.08.2006 (Пн) 17:13

в догонку как динамически созадвать новое соединение

то есть допустим ADODB.Connection.execute «myprocedure»

запускаю с флагом adAsyncExecute,

все отлично проца запустилась,

но мне надо (если необходимо, запустить еще несколько сессий с этой процедурой (с другими параметрами)) сколько сессий заранее еизвестно.

То есть надо как-то динамически создавать новое ADODB connection и экзекутить его.

Может как-то создать динамический массив объектов этого типа?

ааа все. получилось:

да типа такого :

Код: Выделить всё
Dim Cn() As ADODB.Connection

Private Sub Command1_Click()
                Static cnt As Long
                cnt = cnt + 1
                ReDim Preserve Cn(cnt)
                Set Cn(cnt) = New ADODB.Connection
                Cn(cnt).ConnectionString = "mystring"
                Cn(cnt).Open
                Cn(cnt).Execute "truncate table trace", , 16
                cnt = cnt + 1
                ReDim Preserve Cn(cnt)
                Set Cn(cnt) = New ADODB.Connection
                Cn(cnt).ConnectionString = "mystring"
                Cn(cnt).Open
                Cn(cnt).Execute "truncate table trace", , 16
End Sub


MOV
Постоялец
Постоялец
 
Сообщения: 414
Зарегистрирован: 13.03.2004 (Сб) 15:13
Откуда: Санкт-Петербург

Сообщение MOV » 05.09.2006 (Вт) 11:37

Вот такой код:

Код: Выделить всё
Dim WithEvents CN As ADODB.Connection
Dim WithEvents RS As ADODB.Recordset

Private Sub CN_ConnectComplete(ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, ByVal pConnection As ADODB.Connection)
RS.ActiveConnection = CN
End Sub

Private Sub Command1_Click()
Set CN = New ADODB.Connection
Set RS = New ADODB.Recordset
CN.ConnectionString = "Provider=OraOLEDB.Oracle.1;Persist Security " & _
                                "Info=False;User ID=us; PASSwORD=pass; Data Source=source"
CN.Open
RS.Open "select field1, sum(field2) from mytable group by field1", , , , 16
RS.MoveFirst
End Sub

Хочу открыть рекордсет асинхронно, чтобы прога не висла при выполнении запроса, но какое событие надо отлоить, чтобы увидеть, что запрос уже выполнен и можно продолжать работу (RS.MoveFirst)?

Ни одно из событий не срабатывает.

Близко к истине событие RS_FetchComplete но как его вызвать (метода fetch у рекордсета не вижу :( есть filter но как его юзать и вызовет ли он фетч_комплит?).

Или это надо делать как-то по другому?


Konst_One
Член-корреспондент академии VBStreets
Член-корреспондент академии VBStreets
Аватара пользователя

 
Сообщения: 3041
Зарегистрирован: 09.04.2004 (Пт) 13:47
Откуда: Химки
  • Сайт
  • ICQ

Сообщение Konst_One » 05.09.2006 (Вт) 11:46

Код: Выделить всё
Private Sub rs_FetchComplete(ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, ByVal pRecordset As ADODB.Recordset)

    bComplete = True

    'ЗДЕСЬ!!!

End Sub

Private Sub rs_FetchProgress(ByVal Progress As Long, ByVal MaxProgress As Long, adStatus As ADODB.EventStatusEnum, ByVal pRecordset As ADODB.Recordset)

    Me.lblRecordCount.Caption = "Найдено записей: " & Me.rs.RecordCount

End Sub


Antonariy
Повелитель Internet Explorer
Повелитель Internet Explorer
Аватара пользователя

 
Сообщения: 4824
Зарегистрирован: 28.04.2005 (Чт) 14:33
Откуда: Мимо проходил
  • ICQ

Сообщение Antonariy » 05.09.2006 (Вт) 11:51

Код: Выделить всё
Dim WithEvents CN As ADODB.Connection

Dim WithEvents RS As ADODB.Recordset

Private Sub CN_ConnectComplete(ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, ByVal pConnection As ADODB.Connection)

    RS.Open "select field1, sum(field2) from mytable group by field1", cn, , , 16

End Sub

Private Sub Command1_Click()

    Set CN = New ADODB.Connection

    Set RS = New ADODB.Recordset

    CN.ConnectionString = "Provider=OraOLEDB.Oracle.1;Persist Security " & _

    "Info=False;User ID=us; PASSwORD=pass; Data Source=source"

    CN.Open

End Sub

Private Sub RS_FetchComplete(pError As Error, adStatus As EventStatusEnum, pRecordset As Recordset)

    RS.MoveFirst

End Sub

Лучший способ понять что-то самому — объяснить это другому.


MOV
Постоялец
Постоялец
 
Сообщения: 414
Зарегистрирован: 13.03.2004 (Сб) 15:13
Откуда: Санкт-Петербург

Сообщение MOV » 05.09.2006 (Вт) 12:28

Оба варианта не проходят.

Я же говорю, что событие ФетчКомплит Не возникает

ранво как и события rs_FetchProgress


Antonariy
Повелитель Internet Explorer
Повелитель Internet Explorer
Аватара пользователя

 
Сообщения: 4824
Зарегистрирован: 28.04.2005 (Чт) 14:33
Откуда: Мимо проходил
  • ICQ

Сообщение Antonariy » 05.09.2006 (Вт) 13:33

RS.Open «select field1, sum(field2) from mytable group by field1», CN, , , adAsyncFetch (32)

С какого потолка значения констант берешь?

Лучший способ понять что-то самому — объяснить это другому.


MOV
Постоялец
Постоялец
 
Сообщения: 414
Зарегистрирован: 13.03.2004 (Сб) 15:13
Откуда: Санкт-Петербург

Сообщение MOV » 05.09.2006 (Вт) 13:59

да все равно не работает хоть 16, хоть 32 хоть просто adAsyncFetch :)

может черканете реально работающий пример где бы отработало событие FetchComplete?


Konst_One
Член-корреспондент академии VBStreets
Член-корреспондент академии VBStreets
Аватара пользователя

 
Сообщения: 3041
Зарегистрирован: 09.04.2004 (Пт) 13:47
Откуда: Химки
  • Сайт
  • ICQ

Сообщение Konst_One » 05.09.2006 (Вт) 14:07

Код: Выделить всё
  Set cn = New ADODB.Connection

  cn.CommandTimeout = 0

  cn.ConnectionTimeout = 30

  cn.CursorLocation = adUseClient

  cn.Open modADO.GetConnectionString

 
  Set rsEvents = New ADODB.Recordset

  rsEvents.CursorLocation = adUseClient

  rsEvents.PageSize = 100

  rsEvents.CacheSize = 1000

  Set rsEvents.ActiveConnection = cn

  rsEvents.Open SQL, , adOpenStatic, adLockReadOnly, adAsyncFetchNonBlocking

 
 
  Me.lblRecordCount.Caption = "Найдено записей: " & rsEvents.RecordCount

 
  Set dtgEvents.DataSource = rsEvents

остальное ты уже видел


MOV
Постоялец
Постоялец
 
Сообщения: 414
Зарегистрирован: 13.03.2004 (Сб) 15:13
Откуда: Санкт-Петербург

Сообщение MOV » 05.09.2006 (Вт) 14:43

во. спасибо. работает классно.


Antonariy
Повелитель Internet Explorer
Повелитель Internet Explorer
Аватара пользователя

 
Сообщения: 4824
Зарегистрирован: 28.04.2005 (Чт) 14:33
Откуда: Мимо проходил
  • ICQ

Сообщение Antonariy » 05.09.2006 (Вт) 16:06

Забыл сказать, FetchComplete произойдет только с клиентским рекордсетом, когда все записи отданы.

Вложения
fetchTest.rar
(1.54 Кб) Скачиваний: 128

Лучший способ понять что-то самому — объяснить это другому.


MOV
Постоялец
Постоялец
 
Сообщения: 414
Зарегистрирован: 13.03.2004 (Сб) 15:13
Откуда: Санкт-Петербург

Сообщение MOV » 14.09.2006 (Чт) 9:54

Сессия вываливается в некативную.

Делаю так:

Код: Выделить всё
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Dim CNR() As ADODB.Connection
Dim WithEvents CN As ADODB.Connection
Dim WithEvents RS As ADODB.Recordset
Dim Cstring as String

Private Sub CN_ConnectComplete(ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, ByVal pConnection As ADODB.Connection)
    RS.ActiveConnection=CN
End Sub

Private Sub Command1_Click()
'АСИНХРОННО ЗАПУСКАЮ ПРОЦУ:   
    Set CNR(0) = New ADODB.Connection
    Cstring="Provider=OraOLEDB.Oracle.1;Persist Security " & _
    "Info=False;User ID=us; PASSwORD=pass; Data Source=source"
     CNR(0).Open Cstring
     CNR.Execute "myprocedure",, adAsyncExecute
'ДАЛЕЕ СМОТРЮ КАК ОНА ВЫПОЛНЯЕТСЯ:
     Set CN= New ADODB.Connection
     Set RS= New ADODB.Recordset
     CN.Open Cstring
     cnt=1
     While cnt<>0
           RS.Open "select count(*) cnt from mytable where dt_obr is null"
           if not RS.EOF then
               RS.MoveFirst 
               cnt=RS.Fields("cnt").value
           else
               cnt=0 'необработанных записей не осталось, выходим из цикла
           end if
            RS.Close
           DoEvents
           Sleep 5000   
     Wend
End Sub

Private Sub RS_FetchComplete(pError As Error, adStatus As EventStatusEnum, pRecordset As Recordset)
    RS.MoveFirst
End Sub

Сначала, все работает нормально и видно как обрабатываются записи, но потом сессия с процедурой, которая запущена асинхронно вываливается в неактивную (видно в V$SESSION) (при чем на одном и том же количестве записей), естественно, обработка прекращается и цикл становится бесконечным. Такое происходит не с одной процедурой, с другими — аналогичная картина.


Antonariy
Повелитель Internet Explorer
Повелитель Internet Explorer
Аватара пользователя

 
Сообщения: 4824
Зарегистрирован: 28.04.2005 (Чт) 14:33
Откуда: Мимо проходил
  • ICQ

Сообщение Antonariy » 14.09.2006 (Чт) 11:19

Потому что тут такая дикая мешанина… Вообще непонятно где тут что асинхронно выполняется… Завершения CN_ConnectComplete не ждешь, вернее оно вообще не нужно — соединение открывается без асинхронности… RS_FetchComplete никогда не выполнится… While cnt<>0 … Sleep 5000 Wend — вообще дикость. Этот цикл блокирует прогу, а если ты им всего лишь ждешь результата выполнения myprocedure, то нафига тогда асинхронность?

Ты бы хоть скачал мой пример, чтобы увидеть, как на самом деле работает асинхронность.

Лучший способ понять что-то самому — объяснить это другому.


MOV
Постоялец
Постоялец
 
Сообщения: 414
Зарегистрирован: 13.03.2004 (Сб) 15:13
Откуда: Санкт-Петербург

Сообщение MOV » 14.09.2006 (Чт) 12:08

Этот цикл блокирует прогу, а если ты им всего лишь ждешь результата выполнения myprocedure, то нафига тогда асинхронность?

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

А на счет слип ты прав, может из-за того, что слишком большой слип прога и отваливается — попробую поставить на 1 секунду. Да и слип сам по себе вешает прогу конечно тоже. Делать все это в отдельной тереде было бы круче наверно, но не умею. Да. Пример сейчас скачаю.


alibek
Большой Человек
Большой Человек
 
Сообщения: 14204
Зарегистрирован: 19.04.2002 (Пт) 11:40
Откуда: Russia

Сообщение alibek » 14.09.2006 (Чт) 12:13

А знаешь, чем тебе грозит DoEvents в цикле?

Lasciate ogni speranza, voi ch’entrate.


MOV
Постоялец
Постоялец
 
Сообщения: 414
Зарегистрирован: 13.03.2004 (Сб) 15:13
Откуда: Санкт-Петербург

Сообщение MOV » 14.09.2006 (Чт) 12:41

А знаешь, чем тебе грозит DoEvents в цикле?

Чем? :(

Неужели нельзя у меня без него ведь форма не будет перерисовываться.

1. Блин может тогда кто-то сбросит как в отдельном треде запустить процедуру на серваке?

2. в принципе можно сбросить скрипт запускающий процу на винт, а потом шелом запустить sqlplus на выполнение этого скрипта в скрытом режиме, но это коряво как-то и труднее отловить оишбку, если проца в склплюсе ругнется вдруг.

В общем, в отдельном потоке было бы лучше. Пойду искать инфу по многопоточности :(


alibek
Большой Человек
Большой Человек
 
Сообщения: 14204
Зарегистрирован: 19.04.2002 (Пт) 11:40
Откуда: Russia

Сообщение alibek » 14.09.2006 (Чт) 13:27

Тебе уже дали нормальное решение.

Запускаешь процедуру асинхронно.

Устанавливаешь специальный флаг.

Блокируешь все «лишние» элементы управления.

В процедуре _Complete сбрасываешь флаг, восстанавливаешь элементы управления и продолжаешь необходимые действия.

Lasciate ogni speranza, voi ch’entrate.


Wasup!
Продвинутый пользователь
Продвинутый пользователь
 
Сообщения: 120
Зарегистрирован: 21.06.2005 (Вт) 11:09

Сообщение Wasup! » 03.11.2006 (Пт) 13:52

Продолжу в этом топике.

Пытаюсь сделать асинхронное выполнение запросов.

У меня получилось сделать это только с проверкой rs.state в таймере.

Думаю, что правильней пользоваться событиями, как описано выше, но у меня событие fetchComplete не возникает. Пробовал

разные варианты. Пример прикладываю.

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

Есть подозрение, что что-то там не так — при отмене большого запроса программа подвисает на 5-20 сек.

Вложения
Ado events.zip
(4.93 Кб) Скачиваний: 103


GSerg
Шаман
Шаман
 
Сообщения: 14286
Зарегистрирован: 14.12.2002 (Сб) 5:25
Откуда: Магадан

Сообщение GSerg » 03.11.2006 (Пт) 14:08

У тебя VB не версии 5, случаем?

И Timer1 свой злостный отключи.

Как только вы переберёте все варианты решения и не найдёте нужного, тут же обнаружится решение, простое и очевидное для всех, кроме вас


Wasup!
Продвинутый пользователь
Продвинутый пользователь
 
Сообщения: 120
Зарегистрирован: 21.06.2005 (Вт) 11:09

Сообщение Wasup! » 03.11.2006 (Пт) 14:23

Нет 6-ой. Ado подключен 2.7

Событие не возникает и при включенном и при выключенном таймере.


alibek
Большой Человек
Большой Человек
 
Сообщения: 14204
Зарегистрирован: 19.04.2002 (Пт) 11:40
Откуда: Russia

Сообщение alibek » 03.11.2006 (Пт) 14:31

Попробуй задать rs.Properties(«Initial Fetch Size») = 0

К тому же, не Fetch, а FetchNonBlocking.

Lasciate ogni speranza, voi ch’entrate.


GSerg
Шаман
Шаман
 
Сообщения: 14286
Зарегистрирован: 14.12.2002 (Сб) 5:25
Откуда: Магадан

Сообщение GSerg » 03.11.2006 (Пт) 14:34

2.8 поставь, и asyncexecute убери.

Как только вы переберёте все варианты решения и не найдёте нужного, тут же обнаружится решение, простое и очевидное для всех, кроме вас



Вернуться в Visual Basic 1–6

Кто сейчас на конференции

Сейчас этот форум просматривают: AhrefsBot и гости: 4

Артур_1703_16

0 / 0 / 0

Регистрация: 24.05.2017

Сообщений: 4

1

Ошибка при написании асинхронных запросов

17.06.2017, 16:52. Показов 1758. Ответов 3

Метки нет (Все метки)


Ошибка при попытке написать асинхронный запрос.
Код:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
        private async void button1_Click(object sender, EventArgs e)
        {
            MySqlConnection conn = new MySqlConnection("Database = ; Data Source = ; User Id = ; Password = ;");
 
            try
            {
 
                MySqlCommand command = new MySqlCommand("SELECT * FROM resultats WHERE УИН = @УИН", conn);
                await conn.OpenAsync();
                command.Parameters.AddWithValue("@УИН", uin);
 
                MySqlDataReader sqlReader = null;
 
                sqlReader = await command.ExecuteReaderAsync(); // ошибка!                
 
                sqlReader.Close();
                
                conn.Close();
 
            }
            
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString());
            }
 
        }
C#
1
//sqlReader = await command.ExecuteReaderAsync(); // ошибка!

Текст ошибки: Не удалось преобразовать тип «System.Data.Common.DbDateReader» в «MySql.Data.MySqlClient.MySqlDataReader» Существует явное преобразование (возможно, пропущено приведение типов).

Заранее спасибо

__________________
Помощь в написании контрольных, курсовых и дипломных работ, диссертаций здесь



0



Администратор

Эксперт .NET

15226 / 12265 / 4902

Регистрация: 17.03.2014

Сообщений: 24,867

Записей в блоге: 1

17.06.2017, 17:24

2

Лучший ответ Сообщение было отмечено Артур_1703_16 как решение

Решение

Артур_1703_16, у класса MySqlCommand нет метода ExecuteReaderAsync. Он есть в базовом классе и метод возвращает Task<DbDataReader> — отсюда и ошибка. Измени тип переменной sqlReader на System.Data.Common.DbDateReader.



1



0 / 0 / 0

Регистрация: 24.05.2017

Сообщений: 4

17.06.2017, 19:04

 [ТС]

3

Большое спасибо, действительно заработало!
Что удивительно, здесь строка: sqlReader = await command.ExecuteReaderAsync(); работает..
https://www.youtube.com/watch?v=Vh6rn1CAgis (31:20)



0



Администратор

Эксперт .NET

15226 / 12265 / 4902

Регистрация: 17.03.2014

Сообщений: 24,867

Записей в блоге: 1

17.06.2017, 21:54

4

Артур_1703_16, ничего удивительного в этом нет. В уроке используется MS SQL, то есть класс SqlCommand где есть метод ExecuteReaderAsync



0



0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии

А вот еще интересные материалы:

  • Яшка сломя голову остановился исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного где ошибка
  • Асер ошибка no bootable device
  • Ас23 ошибка додж стратус