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);
Related
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;
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;
D2010, Win7 64bit.
Hello,
I have a buttonClick event with needs to process a TDataSet opened in another routine...
GetDBGenericData.
The function GetDBGenericData returns a TDataSet. This routine basically takes a tQuery component, sets it's SQL property, and opens it. It then returns the TDataSet to my buttonclick.
procedure TForm1.Button2Click(Sender: TObject);
var
DS : TDataSet;
begin
DS := TDataSet.Create(nil);
DS := GetDBGenericData(dbSOURCE, 'LIST_ALL_SCHEMAS', [] );
while Not DS.EOF do
begin
ShowMessage(DS.FieldByName('USERNAME').AsString);
DS.Next;
end;
DS.Close;
DS.Free;
My problem is -- Understanding DS.
I am creating it here in this routine. I am "assigning" it to a TDataSet that points to a component. If I DON'T Free it, I have a memory leak (as reported by EurekaLog).
If I do free it, I get an AV the next time I run this routine. (specifically inside the GetDBGenericData routine).
What I think is happening is that DS is getting assigned to (as opposed to copying) to the TDataSet that is being returned, so in effect, I am FREEING BOTH DS in this routine, and the tQuery in GetDBGenericData, when I do a free.
How do I "break" the linkage, and then delete the memory associated to ONLY the one I am dynamically creating.
Thanks,
GS
If your DS variable is being assigned another TDataSet by GetDBGenericData, you should neither Create or Free it. You're just using it to refer to an existing dataset.
procedure TForm1.Button2Click(Sender: TObject);
var
DS : TDataSet;
UserNameField: TField; // Minor change for efficiency
begin
DS := GetDBGenericData(dbSOURCE, 'LIST_ALL_SCHEMAS', [] );
// Call FieldByName only once; no need to create or
// free this either.
UserNameField := DS.FieldByName('USERNAME');
while not DS.Eof do
begin
ShowMessage(UserNameField.AsString);
DS.Next;
end;
// I'd probably remove the `Close` unless the function call
// above specifically opened it before returning it.
DS.Close;
end;
In my app I have different forms that use the same datasource (so the queries are the same too), defined in a common datamodule. Question is, is there a way to know how many times did I open a specific query? By being able to do this, I could avoid close that query without closing it "every where else".
Edit: It's important to mention that I'm using Delphi3 and it is not a single query but several.
The idea is to use the DataLinks property of the TDataSource.
But, as it is protected, you have to gain access to it. One common trick is to create a fake descendant just for the purpose of casting:
type
TDataSourceHack = class(TDataSource);
Then you use it like:
IsUsed := TDataSourceHack(DataSource1).DataLinks.Count > 0;
You can get creative using a addref/release like approach. Just create a few functions and an integer variable in your shared datamodule to do the magic, and be sure to call them..partial code follows:
TDMShared = class(tDataModule)
private
fQueryCount : integer; // set to 0 in constructor
public
function GetQuery : tDataset;
procedure CloseQuery;
end;
function TDMShared.GetQuery : tDataset;
begin
inc(fQueryCount);
if fQueryCount = 1 then
SharedDatsetQry.open;
Result := shareddatasetqry; // your shared dataset here
end;
procedure TDMShared.CloseQuery;
begin
dec(fQueryCount);
if fQueryCount <= 0 then
shareddatasetqry.close; // close only when no refs left.
end;
EDIT: To do this with multiple queries, you need a container to hold the query references, and a way to manipulate them. a tList works well for this. You will need to make appropriate changes for your TDataset descendant, as well as create a FreeAndNil function if you are using an older version of Delphi. The concept I used for this was to maintain a list of all queries you request and manipulate them by the handle which is in effect the index of the query in the list. The method FreeUnusedQueries is there to free any objects which no longer have a reference...this can also be done as part of the close query method, but I separated it to handle the cases where a specific query would need to be reopened by another module.
Procedure TDMShared.DataModuleCreate(Sender:tObject);
begin
dsList := tList.create;
end;
Function TDMShared.CreateQuery(aSql:String):integer;
var
ds : tAdoDataset;
begin
// create your dataset here, for this example using TADODataset
ds := tAdoDataset.create(nil); // self managed
ds.connection := database;
ds.commandtext := aSql;
ds.tag := 0;
Result := dsList.add(ds);
end;
function TDMShared.GetQuery( handle : integer ) : tDataset;
begin
result := nil;
if handle > dsList.count-1 then exit;
if dsList.Items[ handle ] = nil then exit; // handle already closed
result := tAdoDataset( dsList.items[ handle ]);
Inc(Result.tag);
if Result.Tag = 1 then
Result.Open;
end;
procedure TDMShared.CloseQuery( handle : integer );
var
ds : tAdoDataset;
begin
if handle > dsLIst.count-1 then exit;
ds := tAdoDataset( dsList.items[ handle ]);
dec(ds.Tag);
if ds.Tag <= 0 then
ds.close;
end;
procedure TDMShared.FreeUnusedQueries;
var
ds : tAdoDataset;
ix : integer;
begin
for ix := 0 to dsList.Count - 1 do
begin
ds := tAdoDataset(dsLIst.Items[ ix ]);
if ds.tag <= 0 then
FreeAndNil(dsList.Items[ix]);
end;
end;
procedure TDMShared.DataModuleDestroy(Sender: TObject);
var
ix : integer;
begin
for ix := 0 to dsList.count-1 do
begin
if dsLIst.Items[ix] <> nil then
FreeAndNil(dsLIst.Items[ix]);
end;
dsList.free;
end;
Ok, a completely different solution...one that should work for Delphi 3.
Create a new "Descendant Object" from your existing dataset into a new unit, and add some behavior in the new object. Unfortunately I do not have Delphi 3 available for testing, but it should work if you can find the proper access points. For example:
TMySharedDataset = class(tOriginalDataset)
private
fOpenCount : integer;
protected
procedure Internal_Open; override;
procedure Internal_Close; override;
end;
TMySharedDataset.Internal_Open;
begin
inherited Internal_Open;
inc(fOpenCount);
end;
TMySharedDataset.Internal_Close;
begin
dec(fOpenCount);
if fOpenCount <= 0 then
Inherited Internal_Close;
end;
Then just include the unit in your data module, and change the reference to your shared dataset (you will also have to register this one and add it to the palette if your using components). Once this is done, you won't have to make changes to the other units as the dataset is still a descendant of your original one. What makes this all work is the creation of YOUR overridden object.
You could have a generic TDataSet on the shared datamodule and set it on the OnDataChange, using the DataSet property of the Field parameter
dstDataSet := Field.DataSet;
This way, when you want to close the dataset, close the dataset on the datamodule, which is a pointer to the correct DataSet on some form you don't even have to know
Can a TClientDataSet XML file be restructured without losing data? Are there any demo applications or source code that shows how to do such a restructure?
yes and no, the xml doc is transformed using XLST so it needs to conform to that template to be able to be read by the TClientDataSet.
However that does also mean that you can also transform the doc to any format you like yourself into a separate doc, you just can't load the transformed doc directly into the TClientDataSet.
EDIT:
Oops, forgot to post an example.
This project on code central shows a transform from clientdataset to an ADO recordset.
In order to make changes to CDS structure on disk I used a sub-class outlined below. We write our data in binary format to a stream (before compression/encryption) but it should work much the same for XML format.
If you need to add/remove any fields from your saved dataset or change the field definitions then you just increment the dataset table version. When the dataset is opened each time it compares the saved version number with the current. If the saved table is old it will be copied into the new structure, so if you need to make changes, you will take one performance hit the first time you reload the table, but after that it should load from disk as usual.
So, if you save the CDS back to disk after doing the merge - voila - your XML structure is updated, in a CDS friendly format.
TCDS = class(TCustomClientDataset)
private
fTableVersion: integer;
/// <summary> Copies records from source with potentially different table
/// structure/field defs from self, providing defaults for missing fields</summary>
procedure CopyFromDataset(const ASource: TCustomClientDataset);
/// <summary>Provide a default value, if necessary, for any new fields</summary>
function GetDefaultValue(const AFieldName: string): variant;
public
procedure LoadFromStream(AStream: TStream);
procedure SaveToStream(AStream: TStream);
end;
procedure TCDS.LoadFromStream(AStream: TStream);
var
ATemp: TCDS;
APersistedVersion: integer;
begin
AStream.ReadData(APersistedVersion);
if APersistedVersion = fTableVersion then
begin
Close;
ReadDataPacket(AStream, True);
Open;
end
else if APersistedVersion < fTableVersion then
begin
// It's an old table structure:
// - Load old structure into temp CDS
// - Merge temp CDS records into new structure
ATemp := TCDS.Create;
try
ATemp.Close;
ATemp.ReadDataPacket(AStream, True);
ATemp.Open;
CopyFromDataset(ATemp);
finally
FreeAndNil(ATemp);
end;
end;
end;
procedure TCDS.SaveToStream(AStream: TStream);
begin
AStream.WriteData(fVersionNumber);
WriteDataPacket(AStream, True);
end;
procedure TCDS.CopyFromDataset(const ASource: TCustomClientDataset);
var
ACurrentFieldNames: TStrings;
i: integer;
begin
// Assuming we don't want to keep any records already in dataset
EmptyDataSet;
ACurrentFieldNames := TStringList.Create;
try
Fields.GetFieldNames(ACurrentFieldNames);
for i := 0 to ACurrentFieldNames.Count-1 do
ACurrentFieldNames.Objects[i] := ASource.Fields.FindField(ACurrentFieldNames[i]);
ASource.First;
while not ASource.Eof do
begin
Append;
for i := 0 to Fields.Count-1 do
begin
if Assigned(ACurrentFieldNames.Objects[i]) then
Fields[i].Value := TField(ACurrentFieldNames.Objects[i]).Value
else if Fields[i].Required then
Fields[i].Value := GetDefaultValue(ACurrentFieldNames[i]);
end;
Post;
ASource.Next;
end;
finally
FreeAndNil(ACurrentFieldNames);
end;
end;