FireDAC BatchMove from MemoryTable - delphi

My incoming data is loaded in a TFDMemTable, (the reader). The writer is a TFDQuery.
Incoming data should be inserted if not in the target, otherwise updated. Matches are based on the UUID field.
I am unable to properly define that the UUID field is the key.
Here is a code example - does not work. FBatchMove.Execute fails because cannot it find any key fields.
procedure TSubDB.FindDestRecord(ASender: TObject; var AFound: Boolean);
var
aSrc: TBytes;
begin
SetLength(aSrc, 16);
aSrc := FReader.DataSet.FieldByName('UUID').AsBytes;
AFound := FWriter.DataSet.Locate('UUID', aSrc, []);
end;
function TSubDB.LoadDB(const aFilename: string): boolean;
var
FQry: TFDQuery;
FBatchMove: TFDBatchMove;
FReader: TFDBatchMoveDataSetReader;
FWriter: TFDBatchMoveDataSetWriter;
FMemTable: TFDMemTable;
begin
FQry := TFDQuery.Create(nil);
FQry.Connection := dmFB.myDB;
FQry.FetchOptions.AssignedValues := [evItems];
FQry.FetchOptions.Items := [fiBlobs, fiDetails];
FBatchMove := TFDBatchMove.Create(nil);
FBatchMove.Analyze := [taDelimSep, taHeader, taFields];
FReader := TFDBatchMoveDataSetReader.Create(FBatchMove);
FWriter := TFDBatchMoveDataSetWriter.Create(FBatchMove);
FMemTable := TFDMemTable.Create(nil);
try
FMemTable.LoadFromFile(aFileName, sfBinary);
//Not sure how to make the BatchMove recognize that UUID is the key for OnFindDestRecord
FMemTable.IndexFieldNames := 'UUID';
with FMemTable.Indexes.Add do
begin
Name :='idxUUID';
Fields := 'UUID';
Active := true;
end;
FMemTable.IndexName := 'idxUUID';
FMemTable.IndexesActive := true;
FMemTable.FieldByName('UUID').ProviderFlags := FMemTable.FieldByName('UUID').ProviderFlags + [pfInKey];
FReader.DataSet := FMemTable;
FQry.SQL.Text := 'select * from test';
FWriter.DataSet := FQry;
FBatchMove.OnFindDestRecord := FindDestRecord;
FBatchMove.Mode := dmAppendUpdate;
//None of the above seems to keep the pfInKey in the UUID field's ProviderFlags
FBatchMove.Execute;
FQry.Open;
FQry.Close;
finally
FMemTable.Free;
FWriter.Free;
FReader.Free;
FBatchMove.Free;
FQry.Free;
end;
end;
I would really appreciate a working example of batch move (where the target has data, so the batch move mode is dmAppendUpdate).

The key here is that the writer needs to be a TFDBatchMoveSQLWriter with a TableName set. This way the destination had the primary key defined and it is then used to decide whether to insert or update.
function TSubDB.LoadDB(const aFilename: string): boolean;
var
FQry: TFDQuery;
FBatchMove: TFDBatchMove;
FReader: TFDBatchMoveDataSetReader;
FWriter: TFDBatchMoveSQLWriter;
FMemTable: TFDMemTable;
begin
FQry := TFDQuery.Create(nil);
FQry.Connection := dmFB.myDB;
FQry.FetchOptions.AssignedValues := [evItems];
FQry.FetchOptions.Items := [fiBlobs, fiDetails];
FBatchMove := TFDBatchMove.Create(nil);
FBatchMove.Analyze := [taDelimSep, taHeader, taFields];
FReader := TFDBatchMoveDataSetReader.Create(FBatchMove);
FWriter := TFDBatchMoveSQLWriter.Create(FBatchMove);
FMemTable := TFDMemTable.Create(nil);
try
FMemTable.LoadFromFile(aFileName, sfBinary);
FReader.DataSet := FMemTable;
FQry.SQL.Text := 'select * from test';
FWriter.Connection := dmFB.myDB;
FWriter.TableName := 'test';
FBatchMove.Mode := dmAppendUpdate;
FBatchMove.Execute;
FQry.Open;
FQry.Close;
finally
FMemTable.Free;
FWriter.Free;
FReader.Free;
FBatchMove.Free;
FQry.Free;
end;
end;

Related

Revisited: TClientDataset "Missing data provider or data packet"

With a dynamically created TFDQuery,TClientDataSet, and TDataSetProvider I bump into the "Missing data provider or data packet" with this code:
procedure ResetSavedPasswords(ADataModuleDataBaseAdmin : TDataModuleDataBaseAdmin);
var
lQuery : TFDQuery;
lCDS : TClientDataSet;
lProvider : TDataSetProvider;
begin
lFrmBezig := TFormBezig.Create(nil);
lQuery := TFDQuery.Create(nil);
lProvider := TDataSetProvider.Create(Application);
lCDS := TClientDataSet.Create(nil);
try
lQuery.Connection := ADataModuleDataBaseAdmin.FDConnectionTimeTell;
lQuery.CachedUpdates := true;
lProvider.Options := lProvider.Options - [poUseQuoteChar];
lProvider.DataSet := lQuery;
lProvider.Name := 'prvResetSavedPW';
lCDS.ProviderName := lProvider.Name;
lQuery.SQL.Text := Format('select %s,%s from <owner>%s',[sMedMedID,sMedSavedPassword,SMedTabelNaam]),ADataModuleDataBaseAdmin;
lCDS.Open;
Note that the created TDataSetProvider has an owner, based on this answer:
If DatasetProvider has no owner, ClientDataSet can not obtain a reference to the provider
But I still get the error. Opening the TFDQuery first shows me it has data.
What can be the reason?
Using FireDAC with Delphi 10.4. Sydney in a Win32 app.
It turns out that TClientDataSet needs an owner too:
lCDS := TClientDataSet.Create(Application);
This is obvious from the code that triggered the exception:
function TCustomClientDataSet.GetAppServer: IAppServer;
var
ProvComp: TComponent;
DS: TObject;
begin
if not HasAppServer then
begin
if ProviderName <> '' then
if Assigned(RemoteServer) then
FAppServer := RemoteServer.GetServer
else if Assigned(ConnectionBroker) then
FAppServer := ConnectionBroker.GetServer
else
begin
if Assigned(Owner) then
begin
ProvComp := Owner.FindComponent(ProviderName);
if Assigned(ProvComp) and (ProvComp is TCustomProvider) then
begin
DS := GetObjectProperty(ProvComp, 'DataSet');
if Assigned(DS) and (DS = Self) then
DatabaseError(SNoCircularReference, Self);
FAppServer := TLocalAppServer.Create(TCustomProvider(ProvComp));
end;
end;
end;
if not HasAppServer then
DatabaseError(SNoDataProvider, Self);
end;
Result := FAppServer;
end;
The Assigned(Owner) fails, so the code does not bother looking for the TDataSetProvider

Adding and sorting items under a specefic header with a TListView dynamically

I have the following data: http://qs.quantumsoftware.co.za/rust/items.json
What I'm trying to achieve is setting each item under it's specific header and doing all this dynamically. I did this a while back and can't remember how I did this.
So basically The Item displayName under the category with listview items and headers.
EDIT: I forgot to mention that the text in the category field is also dynamic and thus I don't know the text in the category field making it hard or impossible to search for.
Seems like I got it figured out. I needed to load all the categories into an array and then create the header and then only add a item if the header matches the category field.
Code:
procedure TDownloadItems.Execute;
var
jdata, jcategories: TdJSON;
http: TIdHTTP;
LItem: TListViewItem;
sCategories: TStringList;
I: Integer;
arrCategories: array of string;
begin
SetLength(arrCategories, 0);
http := TIdHTTP.Create(nil);
try
jdata := TdJSON.Parse(http.Get('http://qs.quantumsoftware.co.za/rust/items.json'));
try
sCategories := TStringList.Create;
try
sCategories.Sorted := True;
sCategories.Duplicates := dupIgnore;
for jcategories in jdata do
begin
sCategories.Add(jcategories['category'].AsString);
end;
SetLength(arrCategories, sCategories.Count);
for I := 0 to sCategories.Count - 1 do
arrCategories[I] := sCategories[I]
finally
sCategories.Free;
end;
Synchronize(
procedure
var
acategory: string;
jItems: TdJSON;
begin
frmMain.lvRustItems.BeginUpdate;
for acategory in arrCategories do
begin
with frmMain.lvRustItems.Items.Add do
begin
Text := acategory;
Purpose := TListItemPurpose.Header;
end;
for jItems in jdata do
begin
if jItems['category'].AsString = acategory then
begin
LItem := frmMain.lvRustItems.Items.Add;
LItem.Text := jItems['displayName'].AsString;
LItem.Data[TMultiDetailAppearanceNames.Detail1] := jItems['name'].AsString;
LItem.Data[TMultiDetailAppearanceNames.Detail2] := 'Rarity: ' + jItems['rarity'].AsString;
LItem.Data[TMultiDetailAppearanceNames.Detail3] := 'Stackable: ' + jItems['stackable'].AsString;
end;
end;
end;
frmMain.lvRustItems.EndUpdate;
end);
finally
jdata.Free;
end;
finally
http.Free;
end;
end;

Delphi TTreeNode recursively append child nodes to parent node

I have an assignment in "project management". I have to assign modules which can also be sub-modules, so I want to append recursively sub-modules to modules.
Example:
P(project) Modules(M1,M2,M3,M4). Under M1 Module there will be sub-modules(M1S1,M1S2,M1S3), and under sub-module1 (M1S1) there can be many sub-modules (M1S1S1, M1S1S2, M1S1S3) and so on.
I have done this code using Recursion and TTreeNode but i feel the problem is with condition statement.
procedure TForm2.BitBtn1Click(Sender: TObject);
begin
lGlblProjID := 1;
lGlblProjName := 'Project';
ADOConnectionListner.Connected := true;
try
if ADOConnectionListner.Connected then
begin
RootNode := TreeView2.Items.Add(nil, lGlblProjName);
getSubChild(lGlblProjID, RootNode);
end;
except
on E: Exception do
begin
ShowMessage('Exception Class = ' + E.ClassName);
end;
end;
end;
procedure TForm2.getSubChild(var Pid: Integer; var SubRoot: TTreeNode);
var
lcount, I, lcurrentID: Integer;
lcurrentName: String;
lModuleNode: TTreeNode;
begin
// ShowMessage(IntToStr(Pid)+ ' '+SubRoot.Text);
ADOQuery1.SQL.Clear;
ADOQuery1.SQL.Add('SELECT * FROM treetab Where parent_id =:value1');
ADOQuery1.Parameters.ParamByName('value1').Value := Pid;
ADOQuery1.Active := true;
lcount := ADOQuery1.RecordCount;
for I := 0 to lcount - 1 do
begin
lcurrentID := ADOQuery1.FieldByName('id').AsInteger;
lcurrentName := ADOQuery1.FieldByName('name').AsString;
ShowMessage(' id ' + IntToStr(lcurrentID) + ' dd ' + lcurrentName); // print valu of i
if ((lcurrentID <> 0)and (SubRoot.Text <> '') ) then //or
begin
lModuleNode := TreeView1.Items.AddChild(SubRoot, lcurrentName);
getSubChild(lcurrentID, lModuleNode);
end else // if
// lcurrentID = 0
ShowMessage('end reached');
// TreeView1.Items.AddChild(SubRoot, ADOQuery1.FieldByName('name').AsString);
ADOQuery1.Next;
//*********
end;
end;
I want to retrieve all the sub-modules for a particular project like in this case project with id=1 only.
Your problem seems to be the non-local ADOQuery1 which gets cleared at entry on each recursive call. Therefore you loose all remaining records from a previous query. You should arrange a local storage for the query results.
Something like (untested):
procedure GetSubChild()
type
TTempRecord = record
id: integer;
name: string;
end;
TTempArray = array of TTempRecord;
var
lcount, I, lcurrentID: Integer;
lcurrentName: String;
lModuleNode: TTreeNode;
recs: TTempArray
begin
// ...
// query the db
// ...
lcount := ADOQuery1.RecordCount;
SetLength(recs, lcount);
for i := 0 to lcount-1 do
begin
recs[i].id := ADOQuery1.FieldByName('id').AsInteger;
recs[i].name := ADOQuery1.FieldByName('name').AsString;
ADOQuery1.Next;
end;
for i := 0 to lcount-1 do
begin
lcurrentID := recs[i].id;
lcurrentname := recs[i].name;
// ...
// add to treeview
// call recursively GetSubChild()
// ...
end;
end;

How to use Listview correctly in delphi?

My code is the below, it's working correctly but, but after compiling program i see all the fullname and country listed vertically something like :
_________________________________
Fullname1
Country1
Fullname2
Country2
Fullname3
Country3
etc...
SQLQuery1.SQL.Text := 'SELECT * FROM users where user_age="'+age+'"';
SQLQuery1.Open;
rec := SQLQuery1.RecordCount;
SQLQuery1.First; // move to the first record
ListView1.Visible := false;
if rec>0 then
begin
while(not SQLQuery1.EOF)do begin
ListView1.Visible := true;
// do something with the current item
ListView1.AddItem('Full name: '+SQLQuery1['fullname'], Self);
ListView1.AddItem('Country: '+SQLQuery1['cntry'], Self);
// move to the next record
SQLQuery1.Next;
end;
But i want something Like :
First: add the column headers:
var
Col: TListColumn;
begin
Col := ListView1.Columns.Add;
Col.Caption := 'Name';
Col.Alignment := taLeftJustify;
Col.Width := 140;
Col := ListView1.Columns.Add;
Col.Caption := 'Country';
Col.Alignment := taLeftJustify;
Col.Width := 140;
end;
then add the records as follows:
var
Itm: TListItem;
begin
// start of your query loop
Itm := ListView1.Items.Add;
Itm.Caption := SQLQuery1['fullname'];
Itm.SubItems.Add(SQLQuery1['cntry']);
// end of your query loop
end;
Update:
Of course, in order to get the list as in your screenshot, you need to set the ListView's ViewStyle property to vsReport
Your code should look like that:
var
ListItem: TListItem;
...
ListView.Items.BeginUpdate;
try
while(not SQLQuery1.EOF)do begin
ListItem:= ListView.Items.Add;
ListItem.Caption:= 'Full name: '+SQLQuery1['fullname'];
with ListItem.SubItems do begin
Add('Country: '+SQLQuery1['cntry']);
// if you need more columns, add here
end;
SQLQuery1.Next;
end;
finally
ListView.Items.EndUpdate;
end;
You should also set ListView.Style to vsReport to show listview as grid.
I'm not sure how to get the listview to multiline, but I do know you're not using the Query correctly.
As it stands your code has an SQL-injection hole and the implicit reference to 'fieldbyname' inside the loop makes it slow.
var
FullName: TField;
Country: TField;
ListItem: TListItem;
begin
//Use Params or suffer SQL-injections
SQLQuery1.SQL.Text := 'SELECT * FROM users where user_age= :age';
SQLQuery1.ParamByName('age').AsInteger:= age;
SQLQuery1.Open;
if SQLQuery1.RecordCount = 0 then Exit;
//Never use `FieldByName` inside a loop, it's slow.
FullName:= SQLQuery1.FieldByName('fullname');
Country:= SQLQuery1.FieldByName('cntry');
ListView1.Style:= vsReport;
SQLQuery1.First; // move to the first record
SQLQuery1.DisableControls; //Disable UI updating until where done.
try
ListView1.Items.BeginUpdate;
//ListView1.Visible := false;
while (not SQLQuery1.EOF) do begin
//Code borrowed from #Serg
ListItem:= ListView.Items.Add;
ListItem.Caption:= 'Full name: '+Fullname.AsString;
ListItem.SubItems.Add('Country: '+Country.AsString);
SQLQuery1.Next;
end; {while}
finally
SQLQuery1.EnableControls;
ListView1.Items.EndUpdate;
end;
end;
The Delphi documentation contains this example that does exactly what you want.
procedure TForm1.FormCreate(Sender: TObject);
const
Names: array[0..5, 0..1] of string = (
('Rubble', 'Barney'),
('Michael', 'Johnson'),
('Bunny', 'Bugs'),
('Silver', 'HiHo'),
('Simpson', 'Bart'),
('Squirrel', 'Rocky')
);
var
I: Integer;
NewColumn: TListColumn;
ListItem: TListItem;
ListView: TListView;
begin
ListView := TListView.Create(Self);
with ListView do
begin
Parent := Self;
Align := alClient;
ViewStyle := vsReport;
NewColumn := Columns.Add;
NewColumn.Caption := 'Last';
NewColumn := Columns.Add;
NewColumn.Caption := 'First';
for I := Low(Names) to High(Names) do
begin
ListItem := Items.Add;
ListItem.Caption := Names[I][0];
ListItem.SubItems.Add(Names[I][2]);
end;
end;
end;
For all that the Delphi documentation is much maligned, it often has very useful examples like this. The gateway page to the examples is here and the examples are even available on sourceforge so you can check them out using your favourite svn client.
Procedure TForm1.GetUsers;
var
ListItem: TListItem;
begin
try
ListView1.Items.BeginUpdate;
try
ListView1.Clear;
MySQLQuery.SQL.Clear;
MySQLQuery.SQL.Add('select * from users;');
MySQLQuery.Open;
while (not MySQLQuery.EOF) do
begin
ListItem := ListView1.Items.Add;
ListItem.Caption:= VarToSTr(MySQLQuery['username']);
with ListItem.SubItems do
begin
Add(VarToSTr(MySQLQuery['password']));
Add(VarToSTr(MySQLQuery['maxscore']));
end;
MySQLQuery.Next;
end;
MySQLQuery.Close;
finally
ListView1.Items.EndUpdate;
end;
except
on E: Exception do
MessageDlg(PWideChar(E.Message), TMsgDlgType.mtError, [TMsgDlgBtn.mbOK], 0);
end;
end;

How to auto fill in editlabel field with specific database row delphi

Hi there I have a problem
I need to auto fill in information from the database, but if i do it like this:
leemail.text := dm.atInlog['email'];
lenaam.text := dm.atInlog['naam'];
leAdres.text := dm.atInlog['adres'];
lePostcode.text := dm.atInlog['postcode'];
leTelefoonnummer.text := dm.atInlog['telefoon'];
leWoonplaats.Text := dm.atInlog['Woonplaats']
It just takes the first row. I want a specific row.
I can make it work with a button like this:
procedure TfmKlant.BTGegevensClick(Sender: TObject);
begin
//vraag gegevens van gebruiker op
dm.atInlog.Open;
while (not gevonden) and (not dm.atInlog.eof) do
begin
if dm.atInlog['email'] = fminloggen.inlognaam
then
begin
// plaats gegevens in de textboxen
gevonden := true;
leemail.text := dm.atInlog['email'];
lenaam.text := dm.atInlog['naam'];
leAdres.text := dm.atInlog['adres'];
lePostcode.text := dm.atInlog['postcode'];
leTelefoonnummer.text := dm.atInlog['telefoon'];
leWoonplaats.Text := dm.atInlog['Woonplaats']
end
else dm.atInlog.Next;
end;
But It does not do this in create form. How can I auto fill in the labeledit with the requested data?
thanks in advance
You could use TDataSet.Locate or Lookup:
type
TfmKlant = class(TForm)
// ... other declarations
private
procedure ShowData(p_Email: string);
end;
...
procedure TfmKlant.FormCreate(Sender: TObject);
begin
// assuming the data set is already open, and fminloggen.inlognaaem is already set
if dm.atInLog.Locate('email', fminloggen.inlognaam, []) then
begin
ShowData(fminloggen.inloognam);
end;
end;
procedure TfmKlant.ShowData(p_Email: string);
begin
gevonden := true;
leemail.text := dm.atInlog['email'];
lenaam.text := dm.atInlog['naam'];
leAdres.text := dm.atInlog['adres'];
lePostcode.text := dm.atInlog['postcode'];
leTelefoonnummer.text := dm.atInlog['telefoon'];
leWoonplaats.Text := dm.atInlog['Woonplaats']
end;

Resources