Автор: 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.

Теперь нужно задать свойства соединения в свойстве 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
Теперь попробуем выполнить этот запрос асинхронно. Для этого установите у свойства 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
Загрузка из примера выше работает прекрасно, пока вы не нажмёте на кнопку «Отмена». Здесь есть 2 проблемы:
-
- Когда данные уже начали загружаться на клиента, отмена не срабатывает. Вместо отмены программа зависает на строчке «ADOQuery1.Recordset.Cancel;», пока все данные полностью не загрузятся.
- После вызова функции «ADOQuery1.Recordset.Cancel;» (если конечно запрос реально отменился, см. п. 1), повторно выполнить запрос не получится, т.к. вызов метода «ADOQuery1.Open;» не произведёт никаких действий. Это происходит из-за того, что компонент ADOQuery1 остаётся закрытым (Active = False), но в то же время его статус показывает, что запрос открывается (State = dsOpening). Это наглядно показывает на то, что обёртки VCL над ADO не поддерживают отмену.
Первую проблему можно попробовать обойти одним из трёх способов:
-
- Поменять драйвер, если другой драйвер у вас есть. Но, может случиться так, что нужного вам драйвера не существует или БД не поддерживает отмену запросов.
- Использовать серверный курсор (для этого нужно выставить у компонента ADOQuery1 свойство CursorLocation в clUseServer). Но с серверным курсором вы не сможете узнать количество записей и для отображения данных нужно постоянное подключение к СУБД. Поэтому, чтобы работать с данными на клиенте, вам придётся скопировать их в оперативную память (например, в компонент TClientDataSet) или в локальную базу данных, если объём данных очень большой. А чтение всех данных из серверного курсора происходит медленно.
- Сделать фиктивную остановку загрузки. Т.е. показывать пользователю, что запрос отменён, а на самом деле давать запросу выполниться до конца и только после этого его удалять. Можно, конечно удалять компоненты 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 записей.

Поэтому, если вам нужно загрузить на компьютер пользователя большой объём данных, нужно искать другой способ. Приведённый пример загрузки и хранения данных в оперативной памяти подойдёт только для небольших объёмов.
Асинхронная пошаговая загрузка данных в 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:
| Код |
|
OnExecuteComplete — для протоколирования в Memo1 результата выполнения запросов
| Код |
|
3. Бросил на форму компонент Button1 и назначил ему обработчик события OnClick для отключения ADOConnection1 от БД:
| Код |
|
4. Бросил на форму компонент Button2 и назначил ему обработчик события OnClick для запуска асинхронного исполнения запроса:
| Код |
|
Не пугайтесь, реально, этот запрос ничего не удалит т.к. условие CustNo<>CustNo в принципе не выполнимо ![]()
Теперь, запускаем проект и жмем Button2, дожидаемся появления в Memo1 строк:
| Код |
|
После этого жмем Button1, и в итоге в Memo1 имеем:
| Код |
|
Т.е., все как и предполагалось.
А теперь, усложним ситуацию. Для этого добавим еще одну кнопку Button3 со следующим кодом обработчиком OnClick:
| Код |
|
Обратите внимание на то что в исполняемом запросе идет обращение к несуществующей в БД таблице. Т.е., этот запрос не может быть исполнен без ошибки.
А теперь, запустим проект и попытаемся выполнить этот ошибочный запрос нажатием Button3.
В Memo1 получим:
| Код |
|
Т.е., вроде все как положено. Но, интересное начинается потом.
Теперь, нажмем кнопку Button1 для закрытия соединения. И, …. вместо нормального закрытия получаем исключение. Т.е. содержимое Memo1 станет таким:
| Код |
|
Т.е., будет возбуждено исключение с результатом выполнения не заказанной операции (Close), а предыдущей асинхронной операции!
При повторном нажатии на Button1, ADOConnection1 будет нормально закрыт.
ИТОГО.
Возникновение ошибки при асинхронном исполнернии 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.ConnectionPrivate 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.RecordsetPrivate Sub CN_ConnectComplete(ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, ByVal pConnection As ADODB.Connection)
RS.ActiveConnection = CN
End SubPrivate 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

-
- Сообщения: 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 SubPrivate 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

-
- Сообщения: 4824
- Зарегистрирован: 28.04.2005 (Чт) 14:33
- Откуда: Мимо проходил
-
- ICQ
Antonariy » 05.09.2006 (Вт) 11:51
- Код: Выделить всё
Dim WithEvents CN As ADODB.Connection
Dim WithEvents RS As ADODB.RecordsetPrivate 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 SubPrivate 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 SubPrivate 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

-
- Сообщения: 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

-
- Сообщения: 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

-
- Сообщения: 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 StringPrivate Sub CN_ConnectComplete(ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, ByVal pConnection As ADODB.Connection)
RS.ActiveConnection=CN
End SubPrivate 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 SubPrivate Sub RS_FetchComplete(pError As Error, adStatus As EventStatusEnum, pRecordset As Recordset)
RS.MoveFirst
End Sub
Сначала, все работает нормально и видно как обрабатываются записи, но потом сессия с процедурой, которая запущена асинхронно вываливается в неактивную (видно в V$SESSION) (при чем на одном и том же количестве записей), естественно, обработка прекращается и цикл становится бесконечным. Такое происходит не с одной процедурой, с другими — аналогичная картина.
- Antonariy
- Повелитель 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 Метки нет (Все метки)
Ошибка при попытке написать асинхронный запрос.
Текст ошибки: Не удалось преобразовать тип «System.Data.Common.DbDateReader» в «MySql.Data.MySqlClient.MySqlDataReader» Существует явное преобразование (возможно, пропущено приведение типов). Заранее спасибо
__________________
0 |
|
Администратор
15226 / 12265 / 4902 Регистрация: 17.03.2014 Сообщений: 24,867 Записей в блоге: 1 |
|
|
17.06.2017, 17:24 |
2 |
|
РешениеАртур_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 |
|
Большое спасибо, действительно заработало!
0 |
|
Администратор
15226 / 12265 / 4902 Регистрация: 17.03.2014 Сообщений: 24,867 Записей в блоге: 1 |
|
|
17.06.2017, 21:54 |
4 |
|
Артур_1703_16, ничего удивительного в этом нет. В уроке используется MS SQL, то есть класс SqlCommand где есть метод ExecuteReaderAsync
0 |
Сообщение было отмечено Артур_1703_16 как решение