How to return DataSet from DataSnap server? - delphi

Hello everyone who reads this! I hope you can help me with my problem, but if not, thank you for try. I have DataSnap server and client. DataSnap server methods can return to client a DataSet as function result. I'm getting data from MySQL DB with TFDQuery component. Somebody please help me to understand, how can I get a dataset from FDQuery component that already has data?
TDataSet.Data is OleVariant type property containing all data. But FDQuery doesn't have the same property. I need to return a dataset from FDQuery as OleVariant in function.
*Try, Except, FreeAndNil, DisposeOf etc removed from code for better understanding the problem
//Client side
procedure TForm1.GetDataSetFromServer;
var
Server: TServerMethods1Client;
DS: TClientDataSet;
begin
Server := TServerMethods1Client.Create(ClientModule1.SQLConnection1.DBXConnection);
DS := TClientDataSet.Create(nil);
DS.Data := Server.GetDataSet; //Call remote server method
end;
//DataSnap server side
function TServerMethods1.GetDataSet: OleVariant;
begin
FDQuery1.Close;
FDQuery1.SQL.Text := 'SELECT * FROM Table1';
FDQuery1.Open;
//Now i need to return all data as function result
result := ???
end;
Need any information that can be helpful. Thanks in advance! Have a nice day!

The simplest way to do this, AFAIK, is to add a TDataSetProvider, and also a TClientDataSet (if you don't already have one) to your Server module.
Then, you can modify your server code as follows:
function GetDataSet: OleVariant;
begin
if ClientDataSet1.Active then
ClientDataSet1.Close;
FDQuery1.Close;
FDQuery1.SQL.Text := 'SELECT * FROM Table1';
// FDQuery1.Open; Leave this to the DataSetProvider/ClientDataSet
//Now i need to return all data as function result
//result := ???
DataSetProvider1.DataSet := FDQuery1;
ClientDataSet1.ProviderName := 'DataSetProvider1';
ClientDataSet1.Open;
Result := ClientDataSet1.Data;
end;
The point of doing it this way is that TDataSetProvider has all the internal machinery necessary to package up its DataSet's data (i.e. FDQuery1's data) in a form that that can be sent between ClientDataSets. Incorporating the DataSetProvider in the server minimizes the code necessary in the client to consume the CDS data.
Btw, I'm assuming that your server module has the code necessary to 'export' GetDataSet as a server method.

You can also do return a TDataSet from the server function:
function TServerMethods1.GetDataSet: TDataSet;
begin
if ClientDataSet1.Active then
ClientDataSet1.Close;
FDQuery1.Close;
FDQuery1.SQL.Text := 'SELECT * FROM Table1';
DataSetProvider1.DataSet := FDQuery1;
ClientDataSet1.ProviderName := 'DataSetProvider1';
ClientDataSet1.Open;
Result := ClientDataSet1;
end;
For the client side, it now depends on what kind of server you are using.
If it is a DBX Datasnap Server, you will have to use a TsqlServerMethod (note the lower case) 'sql' here, and a TDataSetProvider with a TClientDataset, all preconfigured to retrieve the data from that Server.
If it is a REST Datasnap server, you can do this at the client:
procedure TfrmClientMain.btnRefreshClick(Sender: TObject);
var
Server: TServerMethods1Client;
lDataSet: TDataSet;
DSP: TDataSetProvider;
begin
Server := TServerMethods1Client.Create(ClientModule1.DSRestConnection1);
try
CDS.Close; // a TClientDataSet has been placed on the form
lDataSet := Server.GetDataSet();
DSP := TDataSetProvider.Create(Self);
try
DSP.DataSet := lDataSet;
CDS.SetProvider(DSP);
CDS.Open;
finally
CDS.SetProvider(nil);
DSP.Free;
end;
finally
Server.Free;
end;
end;

Related

How to return value from server to client in DataSnap projectgroup

I write datasnap application. I should know id of last uploaded record on server and after that upload new records from local db using cliendataset component.
I don't realise meaning of this code, what are there Parameters[0].Value, Parameters[1].Value and how it change correctly in order to return result from server:
FSelectCountryCommand.Parameters[0].Value.SetInt32();
FSelectCountryCommand.ExecuteUpdate;
Result := FSelectCountryCommand.Parameters[1].Value.GetInt32;
if I write manually random integer value, for example
result:=10; //FSelectCountryCommand.Parameters[1].Value.GetInt32;
then it works correctly and returns this value in ClientFormUnit.
My final goal is return result value(maxcountry_id) from Server to buttonclick procedure of ClientForm unit
Query on server:
select max(country_id) as maxcountry_id from country
ClientForm unit:
procedure Tclient.buttonCountryClick(Sender: TObject);
var c:integer;
begin
c:=Clientmodule1.ServerMethods1Client.SelectCountry;
Clientmodule1.lqCountry.close;
Clientmodule1.lqCountry.Params[0].AsInteger:= c;
Clientmodule1.lqCountry.close;
end;
ClientClassesUnit:
function TServerMethods1Client.SelectCountry:Integer;
begin
if FSelectCountryCommand = nil then
begin
FSelectCountryCommand := FDBXConnection.CreateCommand;
FSelectCountryCommand.CommandType :=
TDBXCommandTypes.DSServerMethod;
FSelectCountryCommand.Text := 'TServerMethods1.SelectCountry';
FSelectCountryCommand.Prepare;
end;
FSelectCountryCommand.Parameters[0].Value.SetInt32();
FSelectCountryCommand.ExecuteUpdate;
Result := FSelectCountryCommand.Parameters[1].Value.GetInt32;
end;
ServerMethodUnit:
function TServerMethods1.SelectCountry:Integer;
begin
qCountry.close;
qCountry.Open;
Result:= qCountryMaxCountry_id.AsInteger;
end;
I tried use global variable but without success: it is seen only in project area, not in project group area.
SOLUTION
this code works fine:
FSelectCountryCommand.ExecuteUpdate;
Result := FSelectCountryCommand.Parameters[0].Value.GetInt32;
As I understand, as select query doesn't contain parameters therefore this operator:
FSelectCountryCommand.Parameters[0].Value.SetInt32();
is redundant.

Lost records in datasnap REST client

I have a DataSnap/REST server and client application.
The server have a method to return one or more datasets with some number of records but, when this number of records is greater than 50, the first 50 records aren't received by the client application.
I have debugged the server application and the SQL statement is correct (I have executed it from IBExpert and returns the correct number of records).
I have used XE7, FireDAC and Firebird.
Into the server application I have this method to return a dataset
procedure TSvrMethodsMdl.AddTable(SQL, TabName: string; JSON: TFDJSONDataSets);
var
Q: TFDQuery;
begin
Q := TFDQuery.Create(nil);
try
Q.Connection := conTPV;
Q.Transaction := conTPV.Transaction;
Q.SQL.Text := SQL;
TFDJSONDataSetsWriter.ListAdd(JSON, TabName, Q);
finally
// FreeAndNil(Q);
end;
end;
Into the client application, for receiving records, I have this code
var
LDataSetList: TFDJSONDataSets;
LDataSet: TFDDataSet;
tTemp: TFDMemTable;
begin
....
// get remote data
LDataSetList := CliConnectMdl.SvrMethodsMdlClient.GetDataChanged(Shop, ResError);
// process data
LDataSet := TFDJSONDataSetsReader.GetListValueByName(LDataSetList, MyTabName);
tTemp.AppendData(LDataSet);
I think that is some of configuration, but I cant find what.
Any idea? Thanks

How to return Oracle Cursor from stored proc as Client Dataset using Delphi and DBExpress

1st off I am Still a little green to Delphi so this might be a "mundane detail" that's being over looked. [sorry in advance]
I have a need to create a TSQLDataset or TClientDataSet from an Oracle 11g cursor contained in a package. I am using Delphi XE2 and DBExpress to connect to the DB and DataSnap to send the data back to the client.
I'm having problems executing the stored procedure from the Delphi code.
Package Head:
create or replace
PACKAGE KP_DATASNAPTEST AS
procedure GetFaxData(abbr varchar2, Res out SYS_REFCURSOR);
END KP_DATASNAPTEST;
Package Body:
create or replace
PACKAGE body KP_DATASNAPTEST AS
procedure GetFaxData(abbr varchar2, Res out SYS_REFCURSOR)is
Begin
open Res for
SELECT Name,
Address1,
City,
fax_nbr
FROM name
JOIN phone on name.Abrv = phone.abrv
WHERE phone.fax_nbr is not null and name.abrv = abbr;
end;
END KP_DATASNAPTEST;
I have no problem executing this procedure in SQL Developer the problem resides in this code on the DataSnap server:
function TKPSnapMethods.getCDS_Data2(): OleVariant;
var
cds: TClientDataSet;
dsp: TDataSetProvider;
strProc: TSQLStoredProc;
begin
strProc := TSQLStoredProc.Create(self);
try
strProc.MaxBlobSize := -1;
strProc.SQLConnection:= SQLCon;//TSQLConnection
dsp := TDataSetProvider.Create(self);
try
dsp.ResolveToDataSet := True;
dsp.Exported := False;
dsp.DataSet := strProc;
cds := TClientDataSet.Create(self);
try
cds.DisableStringTrim := True;
cds.ReadOnly := True;
cds.SetProvider(dsp);
strProc.Close;
strProc.StoredProcName:= 'KP_DATASNAPTEST.GetFaxData';
strProc.ParamCheck:= true;
strProc.ParamByName('abbr').AsString:= 'ZZZTOP';
strProc.Open; //<--Error: Parameter 'Abbr' not found.
cds.Open;
Result := cds.Data;
finally
FreeAndNil(cds);
end;
finally
FreeAndNil(dsp);
end;
finally
FreeAndNil(strProc);
self.SQLCon.Close;
end;
end;
I have also tried assigning the param value through the ClientDataSet without any luck.
I would not be apposed to returning a TDataSet from the function if its easier or produces results. The data is used to populate custom object attributes.
As paulsm4 mentioned in this answer, Delphi doesn't care about getting stored procedure parameter descriptors, and so that you have to it by yourself. To get params of the Oracle stored procedure from a package, you can try to use the GetProcedureParams method to fill the list with parameter descriptors and with the LoadParamListItems procedure fill with that list Params collection. In code it might look like follows.
Please note, that following code was written just in browser according to documentation, so it's untested. And yes, about freeing ProcParams variable, this is done by the FreeProcParams procedure:
var
ProcParams: TList;
StoredProc: TSQLStoredProc;
...
begin
...
StoredProc.PackageName := 'KP_DATASNAPTEST';
StoredProc.StoredProcName := 'GetFaxData';
ProcParams := TList.Create;
try
GetProcedureParams('GetFaxData', 'KP_DATASNAPTEST', ProcParams);
LoadParamListItems(StoredProc.Params, ProcParams);
StoredProc.ParamByName('abbr').AsString := 'ZZZTOP';
StoredProc.Open;
finally
FreeProcParams(ProcParams);
end;
...
end;
I don't think Delphi will automagically recognize Oracle parameter names and fill them in for you. I think you need to add the parameters. For example:
with strProc.Params.Add do
begin
Name := 'abbr';
ParamType := ptInput;
Value := ZZZTOP';
...
end;

Delphi, TBlobField and UTF8

Here's my architecture :
Datasnap Client <=> Datasnap Server <=> Oracle 11 XE
I'm using a remote provider with TDSProviderConnection from the client side to access my dataset.
Basically, I'm using a TIdHTTP component to query a website and store the result in an Oracle CLOB column.
When saving result to a file, text, accented and other exotic characters are displayed correctly.
same text inserted into a clob using sqldeveloper is displayed correctly as well.
But when I'm doing this thru datasnap architecture, wrong characters shows up (like black diamonds or "upperscore" (underscore on top)
My DB Charset is AL32UTF8, which is the default charset on Oracle 11 XE.
To better understand where the problem is, I rewrote part of my client to access my database directly. And I can say the problem is not in the communication between datasnap client and server.
Now my architecture is :
Client <=> Database
And I access Oracle XE the following way :
TClientDataSet <=> TDataSetProvider <=> TSQLDataSet <=> TSQLConnection
Response from TIdHTTP, which is a TMemoryStream, is stored in the TClientDataset with :
With ClientDataSet do
begin
Edit;
(Fieldbyname('MYCLOBFIELD') as TBlobField).LoadFromStream(MS);
ApplyUpdates(-1);
end;
EDIT : 21st of may
I did test around TBlobField and this component seems to be part of my problem. Let me explain :
I took a random string containing characters from an extended charset like this : 'ÐÒÙÜßąĀûÆ'
And with my ClientDataSet, changed the assignment to this :
FieldByname('MYCLOB').value := 'ÐÒÙÜßąĀûÆ'; // <-- Inserted correctly into Oracle.
Putting this string in a file 'test.txt' and trying to display a popup with the content does not work :
var
MyBlobField: TBlobField;
begin
MyBlobField.LoadFromFile('test.txt');
ShowMessage(MyBlobField.AsString); // <-- does not display correctly
But Using a TMemo to display the content works like a charm :
var
MyMemo: TMemo;
begin
MyMemo.Lines.LoadFromFile('test.txt'); // <-- Works perfectly !!
I tried to set the TBlobField.BlobType property to ftOraClob or ftBlob with no luck.
Finally, using a TStringList (what TMemo.Lines is actually) to load my string into Oracle does the trick.
I guess either something is wrong with TBlobField.LoadFromFile/LoadFromStream or I'm not using it correctly.
TStringList inherits its LoadFromFile/LoadFromStream method from TStrings, which works.
Any help will be greatly appreciated.
Regards.
If you want to put some data to TBlobField you can try:
procedure SetParamBlob(Param : TParam; sData : String);
var
Str : TStringStream;
begin
Str := TStringStream.Create(sData);
try
Param.LoadFromStream(Str, ftBlob);
finally
Str.Free;
end;
end;
or this:
procedure SetParamBlob(Param : TParam; sData : String);
var List : TStringList;
MemStream : TMemoryStream;
begin
Param.Clear;
Param.DataType := ftBlob;
List := TStringList.Create;
MemStream := TMemoryStream.Create;
try
List.Text := sData;
List.SaveToStream(MemStream);
MemStream.Seek(0, soFromBeginning);
Param.LoadFromStream(MemStream, ftBlob);
finally
FreeAndNil(List);
FreeAndNil(MemStream);
end;
end;
...
...
SetParamBlob(q.ParamByName('FIELD'), MyMemo.Text);
...
You can load data from file by this:
function LoadData(sFileSrc : String) : String;
var F : TFileStream;
begin
F := TFileStream.Create(sFileSrc, fmOpenRead + fmShareDenyNone);
try
SetLength(Result, f.Size);
f.Read(Result[1],f.Size);
finally
F.Free;
end;
end;

How to achieve independent cloned TADODataSet?

The scenarios is like this:
We have some SQL table. We are performing an SQL query on this table and we have results in TADOQuery object.
var
qryOryginal, qryClone: TADOQuery;
begin
//setup all the things here
qryOryginal.Active := True;
qryClone.Clone(qryOryginal, ltBatchOptimistic);
qryOryginal.Delete; //delete in qryOryginal casues that qryClone deletes its record too!
end;
So, after cloning the DataSet my qryClone should hold and independent data(at least I thought so). However, performing Delete on qryOryginal causes the same operation on the qryClone. I don't want that.
Any ideas?
I know I could store the data elsewhere, in TClientDataSet perhaps but I would like to try the above solution first.
Thanks in advance for your time.
You can use the recordset of a TADODataSet to clone a TADODataSet.
ds1.Recordset := CloneRecordset(ds2.Recordset);
This version works from Delphi XE. ADOInt is updated with the type library definitions for MDAC 2.8
uses ADOInt, Variants;
function CloneRecordset(const Data: _Recordset): _Recordset;
implementation
function CloneRecordset(const Data: _Recordset): _Recordset;
var
newRec: _Recordset;
stm: Stream;
begin
newRec := CoRecordset.Create as _Recordset;
stm := CoStream.Create;
Data.Save(stm, adPersistADTG);
newRec.Open(stm, EmptyParam, CursorTypeEnum(adOpenUnspecified),
LockTypeEnum(adLockUnspecified), 0);
Result := newRec;
end;
This version must be used for versions of Delphi prior to Delphi XE. ADOR_TLB is generated from msado28.tlb.
uses ADOInt, ADOR_TLB, Variants;
function CloneRecordset(const Data: ADOInt._Recordset): ADOInt._Recordset;
implementation
function CloneRecordset(const Data: ADOInt._Recordset): ADOInt._Recordset;
var
newRec: ADOR_TLB._Recordset;
stm: Stream;
begin
newRec := ADOR_TLB.CoRecordset.Create as ADOR_TLB._Recordset;
stm := CoStream.Create;
(Data as ADOR_TLB._Recordset).Save(stm, adPersistADTG);
newRec.Open(stm, EmptyParam, CursorTypeEnum(adOpenUnspecified),
LockTypeEnum(adLockUnspecified), 0);
Result := newRec as ADOInt._Recordset;
end;
I more like realization with TClientDataSet, because we can freely edit it as we need after copying.
var
MyADOStoredProc: TADOStoredProc;
DataSetProvider: TDataSetProvider;
ClientDataSet: TClientDataSet;
DataSource: TDataSource;
...
// here now we have an opened ADOStoredProc object MyADOStoredProc
// let's copy data from it
DataSetProvider := TDataSetProvider.Create(Self);
DataSetProvider.Name := 'DataSetProvider' + FormatDateTime('_yyyy_mm_dd_hh_nn_ss', Now);
DataSetProvider.DataSet := MyADOStoredProc;
ClientDataSet := TClientDataSet.Create(Self);
ClientDataSet.ProviderName := DataSetProvider.Name;
DataSource := TDataSource.Create(Self);
DataSource.DataSet := ClientDataSet;
ClientDataSet.Open;
MyADOStoredProc.Close;
ClientDataSet.First;
// here we can modify our ClientDataSet as we need, besides MyADOStoredProc is closed
if not ClientDataSet.Eof then
ClientDataSet.Delete;
...
Cloning just clones the cursor on dataset, not duplicating the data kept in the dataset.
If you need to have two independent data, then you have to copy the data from the original dataset to the second one.
If you want to read or modify a single dataset without changing the current cursor on the dataset, then you can use Clone method.
Or second way, use Devexpress dxMemData. Very useful and easy-to-use component.
var
MD: TdxMemData;
SP: TADOStoredProc;
...
...
// and after opening stored procedure:
MD.Close;
MD.Open;
MD.LoadFromDataset(SP);

Resources