Delphi: How to access clientdataset via delta - delphi

On the TDatasetProvider.OnBeforeUpdateRecord, how do I
access the source or originating clientdataset of the
sent DeltaDS parameter?
procedure TdmLoanPayment.dpLoanPaymentBeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
var
sourceCDS: TClientDataset;
begin
sourceCDS := DeltaDS.???;
...
end;
I need to access some properties from the corresponding clientdataset. Setup is TSQLDataset/TDatasetProvider/TClientDataset.
Edit:
The cause of all this hassle is, I wanted to derive a component from TDatasetProvider and assign a default OnBeforeUpdateRecord.

I think SourceDS is what are looking for.
The Sender parameter identifies the provider that is applying updates.
The SourceDS parameter is the dataset from which the data originated.
If there is no source dataset, this value is nil (Delphi) or NULL
(C++). The source dataset may not be active when the event occurs, so
set its Active property to true before trying to access its data.
The DeltaDS parameter is a client dataset containing all the updates
that are being applied. The current record represents the update that
is about to be applied.
The UpdateKind parameter indicates whether this update is the
modification of an existing record (ukModify), a new record to insert
(ukInsert), or an existing record to delete (ukDelete).
The Applied parameter controls what happens after exiting the event
handler. If the event handler sets Applied to true, the provider
ignores the update: it neither tries to apply it, nor does it log an
error indicating that the update was not applied. If the event handler
leaves Applied as false, the provider tries to apply the update after
the event handler exits.
for example:
procedure TdmLoanPayment.dpLoanPaymentBeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
begin
ShowMessage(TClientDataSet(SourceDS).Name); // get source name
...
end;
Edit
or
procedure TdmLoanPayment.dpLoanPaymentBeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
begin
if SourceDS.Name = 'Name1'then
...do something ...
if SourceDS.Name = 'Name2'then
...do something ...
end;

If you trace out of the call to your DataSetProvider1BeforeUpdateRecord, you'll
see that the dataset passed as the SourceDS parameter is the Source dataset of
the UpdateTree, and that is, AFAICS, the dataset that the DataSet property of
the Provider is set to. Of course, this is not the CDS from which the Delta
has been derived (in my test case it's actually a TAdoQuery).
Looking at the source code in Provider.Pas, I can't immediately see a
way to find the identity of the Delta's source CDS. I don't think that is particularly surprising because the Provider's operation is invoked by a CDS and not vice versa, and all the data the Provider needs from the CDS is its Delta.
On the other hand, it's a pretty fair bet that the BeforeUpdateRecord event has
been triggered by the most recent, still-pending, call to ApplyUpdates on one of your CDSs, so if
you make a note of that in their BeforeApplyUpdates event(s), that will probably
tell you what you want to know. I'd expect that to work for a single-level update, but it might be more tricky if the UpdateTree is operating on nested CDSs.
If your CDSs all have individual Providers, but the providers share a BeforeUpdateRecord event, you could identify the CDS for a given provider using code like this:
function TCDSForm.FindCDSForProvider(DataSetProvider: TDataSetProvider):
TClientDataSet;
var
i : Integer;
begin
Result := Nil;
for i := 0 to ComponentCount - 1 do begin
if Components[i] is TClientDataSet then
if TClientDataSet(Components[i]).ProviderName = DataSetProvider.Name then begin
Result := TClientDataSet(Components[i]);
Exit;
end;
end;
end;

Related

Delphi - Can you close all of the database tables?

Can you close all database tables except some? Can you then reopen them? I use an absolute database that is similar to BDE. If this is possible, how can I do so many?
Yes, of course you can. You could iterate the Components property of your form/datamodule, use the is operator to check whether each is an instance of your table type and use a cast to call Open or Close on it.
The following closes all TABSDataSet tables on your form except one called Table1.
procedure TForm1.ProcessTables;
var
ATable : TABSDataSet; // used to access a particular TABSDataSet found on the form
i : Integer;
begin
for i := 0 to ComponentCount - 1 do begin
if Components[i] is TABSDataSet then begin
ATable := TABSDataSet(Components[i]);
// Now that you have a reference to a dataset in ATable, you can
// do whatever you like with it. For example
if ATable.Active and (ATable <> Table1) then
ATable.Close;
end;
end;
end;
I've seen from the code you've posted in comments and your answer that you
are obviously having trouble applying my code example to your situation. You
may find the following code easier to use:
procedure ProcessTables(AContainer : TComponent);
var
ATable : TABSTable;
i : Integer;
begin
for i := 0 to AContainer.ComponentCount - 1 do begin
if AContainer.Components[i] is TABSTable then begin
ATable := TABSTable(AContainer.Components[i]);
// Now that you have a reference to a dataset in ACDS, you can
// do whatever you like with it. For example
if ATable.Active then
ATable.Close;
end;
end;
end;
Note that this is a stand-alone procedure, not a procedure of a particular
form or datamodule. Instead, when you use this procedure, you call it passing
whatever form or datamodule contains the TABSTables you want to work with as the
AContainer parameter, like so
if Assigned(DataModule1) then
ProcessTables(DataModule1);
or
if Assigned(Form1) then
ProcessTables(Form1);
However, the downside of doing it this was is that it is trickier to specify which tables, if any, to leave open, because AContainer, being a TComponent, will not have any member tables.
Btw, your task would probably be easier if you could iterate through the tables in a TABSDatabase. However I've looked at its online documentation but can't see an obvious way to do this; I've asked the publishers, ComponentAce, about this but haven't had a reply yet.

How to insert records with DataSnap

In many tutorials i read how to select data from a database in a datasnap client, p.e. to complete a dbgrid.
But i need now to know how to insert or update a row, p.e "new client". Can everybody recommends me a book or tutorial?
I have an sqlconnection on a clientdatamodule on the clientside apart from clientclassesunit. I was prooving wuth an SQLQuery with an insert SQL Statement but it doen't function.
On the other han i have on the server side:
procedure TServerMethods1.nuevocheque(idcliente,numero,cuenta,idbanco : integer; fr,fc, titular:string ;importe:Double;cobrado:Boolean);
var
ucheque:integer;
begin
with qicheque do
begin
Open;
ParamByName('idcliente').AsInteger:=idcliente;
ParamByName('numero').AsInteger:=numero;
ParamByName('fr').AsDate:=StrToDate(fr);
ParamByName('fc').AsDate:=StrToDate(fc);
ParamByName('importe').AsFloat:=importe;
ParamByName('titular').AsString:=titular;
ParamByName('cobrado').AsBoolean:=cobrado;
ParamByName('cuenta').AsInteger:=cuenta;
ExecSQL();
end;
end;
With this method i try to insert, the statement is into SQL property of the component.
On the client side, i have a TSQLServerMethod wich calls "nuevocheque":
procedure TForm4.BGuardarClick(Sender: TObject);
var
idcliente,numero,cuenta,idbanco:integer;
titular:string;
cobrado:Boolean;
fr,fc:string;
importe:Double;
begin
ClientModule1.nuevocheque.Create(nil);
with ClientModule1.nuevocheque do
begin
idcliente:=1;
numero:=StrToInt(ENumero.Text);
cuenta:=StrToInt(Ecuenta.Text);
idbanco:=1;
titular:=ENombre.Text;
cobrado:=False;
importe:=StrToFloat(EMonto.Text);
fr:=EFechaEmision.Text;
fc:=EFechacobro.Text;
end;
end;
But it doesn´t function.
Thank for your help
Well, i achieve inserting data into mysql database i had desgined.
This is te code in delphi into a button:
procedure TForm4.BGuardarClick(Sender: TObject);
var
idcliente,numero,cuenta,idbanco:integer;
titular:string;
cobrado:Boolean;
fr,fc:string;
importe:Double;
a:TServerMethods1Client;
interes:Double;
begin
a:=TServerMethods1Client.Create(ClientModule1.SQLConnection1.DBXConnection);
begin
idcliente:=Unit3.id;
numero:=StrToInt(ENumero.Text);
cuenta:=StrToInt(Ecuenta.Text);
idbanco:=lcbbanco.KeyValue;
titular:=ENombre.Text;
cobrado:=False;
if (EP.Text<>'') then
begin
importe:=StrToFloat(EHC.Text);
end
else
begin
importe:=StrToFloat(EMonto.Text);
end;
fr:=EFechaEmision.Text;
fc:=EFechacobro.Text;
end;
a.nuevocheque(idcliente,numero,cuenta, idbanco,fr,fc,titular,importe,cobrado);
end;
I've called to method create() with the SQL component such as M Diwo said me.
Im too hapy. Thanks to all
I don't know what you use as database connection, for my own convenience I have slightly modified for dbGO (parameters passed by variant).
Also I have made a function from the server method, like this the client can be notified that there has been a problem (with the query, connection,...). Here is the server method:
//server
function TServerMethods1.NuevoCheque(idcliente, numero, cuenta,
idbanco: integer; fr, fc, titular: string; importe: Double;
cobrado: Boolean): Boolean;
begin
try
with qicheque, Parameters do
begin
Close;
ParamByName('idcliente').Value:=idcliente;
ParamByName('numero').Value:=numero;
ParamByName('fr').Value:=StrToDate(fr);
ParamByName('fc').Value:=StrToDate(fc);
ParamByName('importe').Value:=importe;
ParamByName('titular').Value:=titular;
ParamByName('cobrado').Value:=cobrado;
ParamByName('cuenta').Value:=cuenta;
ExecSQL();
end;
Result := true;
except
Result := false;
//raise; <-- uncomment if you want to handle this properly in your code
end;
end;
For the client I suppose you generated a proxy unit that generally creates an object called ServerMethods1 ?
You must pass the client dbx connection to this - I say this because I saw you put nil in your code.
// client
procedure TfrmClient.BGuardaClick(Sender: TObject);
var
sm : TServerMethods1Client; // <-- generated by proxy generator
idcliente,numero,cuenta,idbanco : integer;
fr,fc, titular : string ;
importe : Double;
cobrado : Boolean;
begin
sm := TServerMethods1Client.Create(SQL.DBXConnection);
if sm.nuevocheque(idcliente,numero,cuenta,idbanco, fr,fc, titular, importe, cobrado) then
// ok
else
// error
sm.Free;
end;
hth
You can use calls to remote methods, but they won't automatically update your data aware controls automatically. Datasnap is able to handle it. First, you need to add/update/remove data on the client. It happens in the local cache managed by the TClientDataset, even when you "Post".
When you're ready, you need to "apply" changes to the remote server calling the Apply() method.
When you call it, the provider component on the server receives a "delta" with the record to change from the client dataset, and will automatically generate the needed INSERT/UPDATED/DELETE SQL statements.
If you don't like them, or you need to perform more complex processing, you can use the provider events to perform the needed operations yourself for each changed record and then tell the provider you did it to avoid the automatic processing. Then the provider passes back the "delta" to the client, where it is used to updated the data aware controls. You can also modify the "delta" before it is passed back.
Read in the documentation the explanation of the Datasnap architecture - it's a multistep design where several components work to allow for a multi-tier implementation.

Delphi: Access Nested Dataset master information when applying update

Can I access the parent dataset information (like MyField.NewValue) in the BeforeUpdateRecord event of a provider when applying the updates to the nested dataset?
Reason:
When I apply updates to a CDS that has a nested detail, the master PK is generated by the underlying query (TIBCQuery) and propagated to the master CDS.
But the new key is not visible in the BeforeUpdateRecord of the detail as the field is updated in the AfterUpdateRecord:
DeltaDS.FieldByName(FieldName).NewValue := SourceDS.FieldByName(FieldName).NewValue)
and the delta is not merged yet.
It looks like the DeltaDS parameter of the BeforeUpdateRecord event contains only information to the nested dataset when the call occurs for the details.
It would be nice if I could do something like:
DeltaDS.ParentDS.FieldByName('FIELDNAME').NewValue.
Edit:
When using nested datasets the BeforeUpdateRecord event is called twice, once for the master and once for the detail (if we have one record of both). When the event is called for the detail, is there a way to access the master information contained in the DeltaDS ?
We can't access the data of the master CDS at that moment as the changes are not already merged. I hope this is not adding more confusion.
You can use the provider's Resolver to look up the corresponding TUpdateTree:
function FindDeltaUpdateTree(Tree: TUpdateTree; DeltaDS: TCustomClientDataSet): TUpdateTree;
var
I: Integer;
begin
Result := nil;
if Tree.Delta = DeltaDS then
Result := Tree
else
for I := 0 to Tree.DetailCount - 1 do
begin
Result := FindDeltaUpdateTree(Tree.Details[I], DeltaDS);
if Assigned(Result) then
Break;
end;
end;
You can use this in your OnBeforeUpdate handler:
var
Tree, ParentTree: TUpdateTree;
begin
if SourceDS = MyDetailDataSet then
begin
Tree := FindDeltaUpdateTree(TDataSetProvider(Sender).Resolver.UpdateTree, DeltaDS);
if Assigned(Tree) then
begin
ParentTree := Tree.Parent;
// here you can use ParentTree.Source (the dataset) and ParentTree.Delta (the delta)
end;
end;
end;

DataSetProvider - DataSet to ClientDataSet

EDIT: It seems as if the DataSetProvider doesn't have the functionality I need for this project, so I'll be implementing a custom class for loading the data into the ClientDataSet.
I am trying to take data from a TMSQuery which is connected to my DB and populate a ClientDataSet with some of that data using a DataSetProvider.
My problem is that I will need to modify some of this data before it can go into my ClientDataSet. The ClientDataSet has persistent fields that will not match up with the raw DB data. I can't even get a string from the DB into a memo field in ClientDataSet.
The ClientDataSet is a part of my data tier so I will need to conform the data from the DB to the ClientDataSet field by field (well most will be able to go right through, but many will require routing and/or conversion).
Does anyone have experience with this?
You're looking for the TDataSetProvider.BeforeUpdateRecord event. Write an event handler for this event and you can take manual control of how the data is applied back to the data base.
Something like this
procedure TDataModule1.DataSetProvider1BeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean);
begin
{ Set applied to tell DataSnap that you have applied this record yourself }
Applied := True;
case UpdateKind of
ukModify:
begin
Table1.Edit;
{ set the values of the fields something like this }
if not VarIsEmpty(DeltaDS.FieldByName('NewValue')) then
Table1['SomeField'] := DeltaDS.FieldByName('SomeField').NewValue;
Table1.Post;
end;
ukInsert:
begin
Table1.Insert;
{ set the values of the fields }
Table1['SomeField'] := DeltaDS['SomeField']
Table1.Post;
end;
ukDelete:
if Table1.Locate('PrimaryKeyField', DeltaDS['PrimaryKeyField'], []) then
Table1.Delete;
end; // case
end;
You can modify the data going to the ClientDataSet by implementing the TDataSetProvider.OnGetData event.
procedure TDataModule1.DataSetProvider1GetData(Sender: TObject; DataSet: TCustomClientDataSet);
begin
DataSet.First;
while not DataSet.Eof do begin
DataSet.Edit;
DataSet['Surname'] := UpperCase(DataSet['Surname']);
DataSet.Post;
DataSet.Next;
end; // while
end;
When applying updates from the ClientDataSet you can use the TDataSetProvider.OnUpdateData event. Like the OnGetData event you are operating on the whole dataset rather than a single record.
procedure TDataModule1.DataSetProvider1UpdateData(Sender: TObject; DataSet: TCustomClientDataSet);
begin
DataSet.First;
while not DataSet.Eof do begin
DataSet.Edit;
DataSet['Surname'] := LowerCase(DataSet['Surname']);
DataSet.Post;
DataSet.Next;
end; // while
end;
This OnUpdateData event is called before the OnBeforeUpdateRecord event. Also the OnGetData and OnUpdateData events operate on the whole dataset while OnBeforeUpdateRecord is called once for each modified record.
If I need a ClientDataSet to have data that doesn't exactly match the database schema, I write a query for the TQuery component that returns the data in the format that I want. I then write my own, separate, Delete, Insert, Refresh, and Update queries for the TQuery component.
Alternatively, you could create a view on the database and use the view in your TQuery component.
If you want a custom ClientDataSet that is independent of the database, what you need is an in-memory dataset. If you don't have an in-memory dataset component, Google for "TClientDataSet as in-memory dataset". What you end up with though, is basically a glorified list view component. Of course, you can hook into the OnUpdateRecord of the in-memory dataset to know when to update your real dataset.

How can I detect if ApplyUpdates will Insert or Update data?

In the AfterPost event handler for a ClientDataSet, I need the information if the ApplyUpdates function for the current record will do an update or an insert.
The AfterPost event will be executed for new and updated records, and I do not want to declare a new Flag variable to indicate if a 'update' or ' insert' operation is in progress.
Example code:
procedure TdmMain.QryTestAfterPost(DataSet: TDataSet);
begin
if IsInserting(QryTest) then
// ShowMessage('Inserting')...
else
// ShowMessage('Updating');
QryTest.ApplyUpdates(-1);
end;
The application will write a log in the AfterPost method, after ApplyUpdate has completed. So this method is the place which is closest to the action, I would prefer a solution which completely can be inserted in this event handler.
How could I implement the IsInserting function, using information in the ClientDataSet instance QryTest?
Edit: I will try ClientDataSet.UpdateStatus which is explained here.
ApplyUpdates doesn't give you that information - since it can be Inserting, updating and deleting.
ApplyUpdates apply the change information stored on Delta array. That change information can, for example, contain any number of changes of different types (insertions, deletions and updatings) and all these will be applied on the same call.
On TDatasetProvider you have the BeforeUpdateRecord event (or something like that, sleep does funny things on memory :-) ). That event is called before each record of Delta is applied to the underlying database/dataset and therefore the place to get such information... But Showmessage will stop the apply process.
EDIT: Now I remembered there's another option: you can assign Delta to another clientdataset Data property and read the dataset UpdateStatus for that record.
Of course, you need to do this before doing applyupdates...
var
cdsAux: TClientDataset;
begin
.
.
<creation of cdsAux>
cdsAUx.Data := cdsUpdated.Delta;
cdsAux.First;
case cdsAux.UpdateStatus of
usModified:
ShowMessage('Modified');
usInserted:
ShowMessage('Inserted');
usDeleted:
ShowMessage('Deleted'); // For this to work you have to modify
// TClientDataset.StatusFilter
end;
<cleanup code>
end;
BeforeUpdateRecord event on TDataSetProvider is defined as:
procedure BeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS:
TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
Parameter UpdateKind says what will be done with record: ukModify, ukInsert or ukDelete. You can test it like this:
procedure TSomeRDM.SomeProviderBeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
begin
case UpdateKind of
ukInsert :
// Process Insert;
ukModify :
// Process update
ukDelete :
// Process Delete
end;
end;
Note: this event signature is from Delphi 7. I don't know if it changed in later versions of Delphi.
Set the ClientDataSet.StatusFilter to an TUpdateStatus value, and then read ClientDataSet.RecordCount
for example,
ClientDataSet1.StatusFilter := [usDeleted];
ShowMessage(IntToStr(ClientDataSet1.RecordCount));
will return the number of Delete queries that will be executed.
Note two things, however. Setting StatusFilter to usModified always includes both the modified and unmodified records, so you take half of that value (a value of 4 means 2 Update queries will be executed). Also, setting StatusFilter to [] (an empty set) is how you restore to default view (Modified, Unmodified, and Inserted)
Make sure that any unposted changes have been posted before doing this, otherwise unposted changes may not be considered.

Resources