Возврат двух наборов данных с сервера DataSnap в одном запросе

Комментарий 27 июня 2012 г.

В исходном сообщении есть полезный код, но на самом деле он не иллюстрирует, как вернуть несколько наборов данных с сервера DataSnap в одном запросе из клиентского приложения. Чтобы увидеть пример того, как это сделать, посмотрите на Ответ с пометкой Правильно в самом низу страницы.


2011-08-31 Комментарий

Благодаря Ганни, я снова все посмотрел. Досадной проблемой была моя собственная ошибка, которая теперь исправлена. Я могу выполнять несколько операторов SQL на сервере DataSnap в рамках одного запроса клиента к серверу, создавая/удаляя компонент TSQLQuery между каждым запросом к базе данных.

Моя проблема возникла, когда я оставил отладочную строку кода в своем хранимом процессе, пытаясь обойти хорошо известную проблему, которая не позволяет вам получить доступ к параметру output после вызова TSQLStoredProc.Open ( http://qc.embarcadero.com/wc/qcmain.aspx?d=90211 ).

Таким образом, несмотря на то, что моя проблема решена, первоначальные проблемы остаются - вы не можете вызвать метод Open для получения данных, а затем получить доступ к параметру output, и вы не можете получить доступ к нескольким наборам данных, возвращенным из одного хранимого процесса.

Еще раз спасибо, Ганни, за ваше предложение.


Исходное сообщение

Я пытаюсь вернуть два разных набора данных с сервера DataSnap одним запросом. Оба исходят из одной и той же базы данных. Один представляет собой значение одного поля/одной записи, другой – набор данных с несколькими полями/многозаписями.

Сервер DataSnap имеет следующий метод:

function TDSSvrMethods.GetData(const SQL: string; var Params: OleVariant; var Key: string): OleVariant;
var qry: TSQLQuery; cds: TClientDataSet;
begin
  // create TSQLQuery & TClientDataSet
  // Link the two components via cds.SetProvider(qry);
  // run first query, set 'Key' to the result <-- this works
  qry.Close;
  // run second query <-- I see this hit the database
  // return dataset via 'Result := cds.Data;'
  // destory TSQLQuery & TClientDataSet
end;

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

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

Я пробовал несколько подходов:

ONE: динамически создайте компонент TSQLQuery для первого запроса, извлеките значение базы данных, уничтожьте TSQLQuery, создайте новый TSQLQuery и извлеките второй набор данных. Это не помогло. Я могу использовать SQL Server Profiler и наблюдать, как обе команды обращаются к базе данных, но первый результирующий набор отображается как набор данных для обоих запросов.

ДВА: сделайте то же, что и в #1, но используйте TSQLStoredProcedure вместо TSQLQuery. Результат тот же.

ТРИ: используйте TSQLStoredProcedure и возвращайте оба набора данных из одной и той же хранимой процедуры, например:

create procedure sp_test_two_datasets
as
  select 'dataset1' as [firstdataset]
  select * from sometable -- 2nd dataset
go

Поскольку у TSQLStoredProcedure есть NextRecordSet, я надеялся получить доступ к обоим наборам данных, но безуспешно. Когда я вызываю NextRecordSet, он возвращает nil.

ЧЕТЫРЕ : возвращает два значения в одном вызове TSQLStoredProcedure, используя набор данных и параметр output:

create procedure sp_another_test
  @singlevalue varchar(255) output
as
  select * from sometable
go

Код Delphi выглядит примерно так:

var sp: TSQLStoredProc; cds: TClientDataSet;
...
cds.SetProvider(sp);
...
sp.CommandText := 'sp_another_test :value output';
sp.Params.ParamByName('value').Value := Key; // in/out method parameter from above
sp.Open;
Key := sp.Params.ParamByName('value').Value; // single string value
Result := cds.Data; // dataset
...

Я проверяю sp.Params и вижу один параметр ввода/вывода с именем value. Я не могу получить доступ к параметру output, когда также возвращается набор данных. Это ИЗВЕСТНАЯ ошибка (уже много лет): http://qc.embarcadero.com/wc/qcmain.aspx?d=90211

ЗАКЛЮЧЕНИЕ:

Поскольку сервер DataSnap делится своим основным TSQLConnection со всеми подключающимися клиентами, а компоненты TSQLQuery (или TSQLStoredProc) и TClientDataSet создаются/освобождаются при каждом запросе, единственное, что остается, это удерживать предыдущий набор данных и возвращать его. для компонентов TSQLQuery и TSQLStoredProc это компонент TSQLConnection. Я пытался вызвать TSQLConnection.CloseDataSets перед закрытием и освобождением компонентов TSQLQuery (или TStoredProc), но это тоже не помогло.

Возможно, более пристальный взгляд на TSQLConnection поможет. Вот как это выглядит в файле .dfm:

object sqlcon: TSQLConnection
  DriverName = 'MSSQL'
  GetDriverFunc = 'getSQLDriverMSSQL'
  LibraryName = 'dbxmss.dll'
  LoginPrompt = False
  Params.Strings = (
    'SchemaOverride=%.dbo'
    'DriverUnit=DBXMSSQL'

      'DriverPackageLoader=TDBXDynalinkDriverLoader,DBXCommonDriver150.' +
      'bpl'

      'DriverAssemblyLoader=Borland.Data.TDBXDynalinkDriverLoader,Borla' +
      'nd.Data.DbxCommonDriver,Version=15.0.0.0,Culture=neutral,PublicK' +
      'eyToken=91d62ebb5b0d1b1b'

      'MetaDataPackageLoader=TDBXMsSqlMetaDataCommandFactory,DbxMSSQLDr' +
      'iver150.bpl'

      'MetaDataAssemblyLoader=Borland.Data.TDBXMsSqlMetaDataCommandFact' +
      'ory,Borland.Data.DbxMSSQLDriver,Version=15.0.0.0,Culture=neutral' +
      ',PublicKeyToken=91d62ebb5b0d1b1b'
    'GetDriverFunc=getSQLDriverMSSQL'
    'LibraryName=dbxmss.dll'
    'VendorLib=sqlncli10.dll'
    'HostName=localhost'
    'Database=Database Name'
    'MaxBlobSize=-1'
    'LocaleCode=0000'
    'IsolationLevel=ReadCommitted'
    'OSAuthentication=False'
    'PrepareSQL=True'
    'User_Name=user'
    'Password=password'
    'BlobSize=-1'
    'ErrorResourceFile='
    'OS Authentication=False'
    'Prepare SQL=False')
  VendorLib = 'sqlncli10.dll'
  Left = 352
  Top = 120
end

И во время выполнения я делаю несколько вещей, чтобы мне не приходилось развертывать файл .INI для драйверов DBX. Во-первых, модуль, который позволяет мне зарегистрировать собственный драйвер без INI:

unit DBXRegDB;

interface

implementation

uses
  DBXCommon, DBXDynalinkNative;

type
  TDBXInternalDriver = class(TDBXDynalinkDriverNative)
  public
    constructor Create(DriverDef: TDBXDriverDef); override;
  end;

  TDBXInternalProperties = class(TDBXProperties)
  private
  public
    constructor Create(DBXContext: TDBXContext); override;
  end;

{ TDBXInternalDriver }

constructor TDBXInternalDriver.Create(DriverDef: TDBXDriverDef);
begin
  inherited Create(DriverDef, TDBXDynalinkDriverLoader);
  InitDriverProperties(TDBXInternalProperties.Create(DriverDef.FDBXContext));
end;

{ TDBXInternalProperties }

constructor TDBXInternalProperties.Create(DBXContext: TDBXContext);
begin
  inherited Create(DBXContext);

  Values[TDBXPropertyNames.SchemaOverride]         :=       '%.dbo';
  Values[TDBXPropertyNames.DriverUnit]             :=       'DBXMSSQL';
  Values[TDBXPropertyNames.DriverPackageLoader]    :=       'TDBXDynalinkDriverLoader,DBXCommonDriver150.bpl';
  Values[TDBXPropertyNames.DriverAssemblyLoader]   :=       'Borland.Data.TDBXDynalinkDriverLoader,Borland.Data.DbxCommonDriver,Version=15.0.0.0,Culture=neutral,PublicKeyToken=91d62ebb5b0d1b1b';
  Values[TDBXPropertyNames.MetaDataPackageLoader]  :=       'TDBXMsSqlMetaDataCommandFactory,DbxMSSQLDriver150.bpl';
  Values[TDBXPropertyNames.MetaDataAssemblyLoader] :=       'Borland.Data.TDBXMsSqlMetaDataCommandFactory,Borland.Data.DbxMSSQLDriver,Version=15.0.0.0,Culture=neutral,PublicKeyToken=91d62ebb5b0d1b1b';
  Values[TDBXPropertyNames.GetDriverFunc]          :=       'getSQLDriverMSSQL';
  Values[TDBXPropertyNames.LibraryName]            :=       'dbxmss.dll';
  Values[TDBXPropertyNames.VendorLib]              :=       'sqlncli10.dll';
  Values[TDBXPropertyNames.HostName]               :=       'ServerName';
  Values[TDBXPropertyNames.Database]               :=       'Database Name';
  Values[TDBXPropertyNames.MaxBlobSize]            :=       '-1';
  Values['LocaleCode']                             :=       '0000';
  Values[TDBXPropertyNames.IsolationLevel]         :=       'ReadCommitted';
  Values['OSAuthentication']                       :=       'False';
  Values['PrepareSQL']                             :=       'True';
  Values[TDBXPropertyNames.UserName]               :=       'user';
  Values[TDBXPropertyNames.Password]               :=       'password';
  Values['BlobSize']                               :=       '-1';
  Values[TDBXPropertyNames.ErrorResourceFile]      :=       '';
  Values['OS Authentication']                      :=       'False';
  Values['Prepare SQL']                            :=       'True';
  Values[TDBXPropertyNames.ConnectTimeout]         :=       '30';

  // Not adding connection pooling to the default driver parameters
end;

var
  InternalConnectionFactory: TDBXMemoryConnectionFactory;

initialization
  TDBXDriverRegistry.RegisterDriverClass('MSSQL_NoINI', TDBXInternalDriver);
  InternalConnectionFactory := TDBXMemoryConnectionFactory.Create;
  InternalConnectionFactory.Open;
  TDBXConnectionFactory.SetConnectionFactory(InternalConnectionFactory);

end.

Вышеупомянутый метод включен в проект (файл .dpr) и автоматически регистрирует драйвер. Следующий метод использует его для настройки TSQLConnection (sqlcon) во время выполнения (при запуске сервера DataSnap):

procedure SetupConnection(const hostname, port, dbname, username, password, maxcon: string);
begin
  if sqlcon.Connected then
    Exit;

  // Our custom driver -- does not use DBXDrivers.ini
  sqlcon.Params.Clear;
  sqlcon.DriverName := 'MSSQL_NoINI';
  sqlcon.VendorLib := sqlcon.Params.Values[TDBXPropertyNames.VendorLib];
  sqlcon.LibraryName := sqlcon.Params.Values[TDBXPropertyNames.LibraryName];
  sqlcon.GetDriverFunc := sqlcon.Params.Values[TDBXPropertyNames.GetDriverFunc];

  sqlcon.Params.Values[TDBXPropertyNames.HostName]           := hostname;
  sqlcon.Params.Values[TDBXPropertyNames.Port]               := port;
  sqlcon.Params.Values[TDBXPropertyNames.Database]           := dbname;
  sqlcon.Params.Values[TDBXPropertyNames.UserName]           := username;
  sqlcon.Params.Values[TDBXPropertyNames.Password]           := password;
  sqlcon.Params.Values[TDBXPropertyNames.DelegateConnection] := DBXPool.sDriverName;
  sqlcon.Params.Values[DBXPool.sDriverName + '.' + TDBXPoolPropertyNames.MaxConnections]    := maxcon;
  sqlcon.Params.Values[DBXPool.sDriverName + '.' + TDBXPoolPropertyNames.MinConnections]    := '1';
  sqlcon.Params.Values[DBXPool.sDriverName + '.' + TDBXPoolPropertyNames.ConnectTimeout]    := '1000';
  sqlcon.Params.Values[DBXPool.sDriverName + '.' + 'DriverUnit']                            := DBXPool.sDriverName;
  sqlcon.Params.Values[DBXPool.sDriverName + '.' + 'DelegateDriver']                        := 'True';
  sqlcon.Params.Values[DBXPool.sDriverName + '.' + 'DriverName']                            := DBXPool.sDriverName;
end;

Возможно, какие-либо из этих настроек портят компонент TSQLConnection и заставляют его кэшировать наборы данных и возвращать их вместо того, который был выполнен самым последним компонентом TSQLQuery?

Любая помощь будет принята с благодарностью. Как вы понимаете, это сводит меня с ума!

Спасибо, Джеймс


person James L.    schedule 27.08.2011    source источник
comment
Я не использовал Delphi уже много лет, но подозреваю, что проблема связана с компакт-дисками. Если вы смешаете порядок двух операторов, вы всегда получите результаты первого оператора? Есть ли явный метод cds.clear, который вы можете вызывать между запросами?   -  person Preston    schedule 28.08.2011
comment
Хороший вопрос... Оказывается, пытаясь обойти другие существующие ошибки Embarcadero, я представил свою собственную ошибку, из-за которой она выглядела так, как будто она кэшировала первый набор результатов. Спасибо, что нашли время ответить.   -  person James L.    schedule 28.08.2011
comment
Я рад, что вы нашли решение. Я только недавно взял Delphi после 7-8-летнего перерыва. Я перешел с D5E на D2010 Pro. У меня есть все книги Марко Канту, так что у меня есть контрольный журнал того, что изменилось в каждой версии, но это тяжело. SO - отличное место, чтобы пообщаться.   -  person Michael Riley - AKA Gunny    schedule 30.08.2011
comment
XE2 должен выйти со дня на день (самая новая версия Delphi). Он может похвастаться 64-битным компилятором, кросс-компиляцией для Windows и Mac и инструментами для карманных компьютеров. Самой сложной частью перехода от D5 к D2009 был Unicode. Мне пришлось скрыть весь мой старый код, что заняло довольно много времени.   -  person James L.    schedule 31.08.2011
comment
Обратите внимание, что QualityCentral закрыт, поэтому вы можете больше не могу получить доступ к qc.embarcadero.com ссылкам. Если вам нужен доступ к старым данным контроля качества, посмотрите QCScraper.   -  person Remy Lebeau    schedule 09.06.2017


Ответы (2)


Что произойдет, если вы закроете и CDS?

function TDSSvrMethods.GetData(const SQL: string; var Params: OleVariant; var Key: string): OleVariant; 
var qry: TSQLQuery; cds: TClientDataSet; 
begin 
  // create TSQLQuery & TClientDataSet
  // Link the two components via cds.SetProvider(qry);
  // run first query, set 'Key' to the result <-- this works
  qry.Close;
  cds.Close;
  // run second query <-- I see this hit the database
  cds.Open
  // return dataset via 'Result := cds.Data;'
  // destory TSQLQuery & TClientDataSet end;

person Michael Riley - AKA Gunny    schedule 27.08.2011
comment
Я попробовал это, и это не помогло. ОДНАКО, это заставило меня тщательно все изучить, и я нашел проблему. Я опубликую это как ответ. Спасибо и +1 за вашу помощь. - person James L.; 28.08.2011

Как уже упоминалось, пытаясь обойти две ошибки DBX Framework, я представил ошибку, из-за которой казалось, что TSQLConnection возвращает предыдущий набор данных для последующего запроса данных. После того, как я исправил свою ошибку, мне просто пришлось обойти две ошибки DBX Framework (поскольку мы не можем исправить/перекомпилировать фреймворк самостоятельно):

ONE: вы не можете вызвать метод Open и получить доступ к параметру output.

ДВА: вы не можете получить доступ к нескольким наборам данных, возвращенным из одного хранимого процесса.

Временное решение: я просто выполняю два запроса с сервера DataSnap к базе данных, а затем обрабатываю/упаковываю отдельные наборы данных для отправки обратно клиенту (в одном ответе).


Комментарий 27 июня 2012 г.

Поскольку этот поток получает несколько просмотров, я решил объяснить, как я упаковываю несколько наборов данных в один ответ от сервера DataSnap.

DataSnap может вернуть OleVariant клиентскому приложению. Легко создать OleVariant, представляющий собой массив OleVariant. Поскольку свойство TClientDataSet.Data является OleVariant, мы можем создать массив наборов данных для передачи обратно клиенту. Этот пример возвращает 5 наборов данных. Предположим, что эти методы существуют на сервере DataSnap:

function TServerMethods1.GetData(SQL: string): OleVariant;
var cds: TClientDataSet;
begin
  cds := TClientDataSet.Create(nil);
  try

    { setup 'cds' to connect to database }
    { pull data }

    Result := cds.Data;
  finally
    FreeAndNil(cds);
  end;
end;

function TServerMethods1.GetMultipleDataSets: OleVariant;
begin
  Result := VarArrayCreate([0, 4], varVariant);
  Result[0] := GetData('select * from Table1');
  Result[1] := GetData('select * from Table2');
  Result[2] := GetData('select * from Table3');
  Result[3] := GetData('select * from Table4');
  Result[4] := GetData('select * from Table5');
end;

Вы можете назначить данные на стороне клиента, поместив 5 компонентов TClientDataSet на форму и назначив их свойство Data элементам из файла OleVariant.

procedure X;
var DataArray: OleVariant;
begin
  try
    with ProxyMethods.TServerMethods1.Create(SQLConnection1.DBXConnection, True) do
    try
      DataArray := GetMultipleDataSets;
    finally
      Free;
    end;

    ClientDataSet1.Data := DataArray[0];
    ClientDataSet2.Data := DataArray[1];
    ClientDataSet3.Data := DataArray[2];
    ClientDataSet4.Data := DataArray[3];
    ClientDataSet5.Data := DataArray[4];
  finally
    VarClear(DataArray);
  end;
end;

(Я набрал этот пример, не проверяя его. Мой фактический код включает проверку границ вариантного массива и другие динамические элементы.)

person James L.    schedule 29.08.2011