How can I display only specified database results based on TListbox items? - database

I have two forms: frmMakeQuote and frmQuoteTemp
In the frmMakeQuote there are two TListboxes: lboMtrlList and lboSelectedMtrl
lboMtrlList displays the Product Description column from the database.
procedure TfrmMakeQuote.FormCreate(Sender: TObject);
begin
con := TFDConnection.Create(nil);
query := TFDQuery.Create(con);
con.LoginPrompt := false;
con.Open('DriverID=SQLite;Database=C:\Users\kasio\Documents\Embarcadero\' +
'Studio\Projects\ProgramDatabase;');
query.Connection := con;
query.SQL.Text :=
'SELECT [Material Description] FROM MtrlDatabase ORDER BY MtrlID';
try
query.Open;
lboMtrlList.Items.Clear;
while not query.EOF do
begin
lboMtrlList.Items.Add(query.Fields[0].AsString);
query.Next;
end;
finally
query.Close;
end;
end;
When the person double clicks on any 'product' in the lboMtrlList, it's moved to the lboSelectedMtrl. (Basically, it shows the selected 'products'.)
procedure TfrmMakeQuote.lboMtrlListDblClick(Sender: TObject);
begin
lboSelectedMtrl.Items.Add(lboMtrlList.Items.Strings[lboMtrlList.ItemIndex]);
end;
I want to be able to display the Product Description and Price columns from the database, of ONLY the selected 'products' from the lboSelectedMtrl. They should be displayed in the TStringGrid called sgdMaterials on the frmQuoteTemp.
I wrote something like this:
procedure TfrmMakeQuote.performMtrlQuery;
var
i: integer;
begin
for i := 1 to frmMakeQuote.lboSelectedMtrl.ItemIndex do
begin
query.SQL.Text := 'SELECT [Material Description], Price FROM MtrlDatabase ' +
'WHERE [Material Description] = "'
+ frmMakeQuote.lboSelectedMtrl.Items.Strings[1]
+ '" ORDER BY MtrlID';
query.Open;
query.First;
end;
end;
It doesn't show any error, but it doesn't work and displays nothing and I'm aware that it's probably completely wrong.

Your loop inside of performMtrlQuery() is wrong. If nothing is actually selected in lboSelectedMtrl, its ItemIndex will be -1 and the loop will not iterate through any items. Not only that, but even if an item were selected, your loop would not iterate through ALL of the available items. Also, when you are indexing into the Strings[] property, you are using a hard-coded 1 instead of the loop variable i.
As for your TStringGrid, why not use a TDBGrid instead, and tie it to a DataSource that is filtering the database by the desired items? In any case, performMtrlQuery() is not doing anything to populate the grid at all, whether it is to store the search results in the grid directly, or to store the results in a list somewhere that frmQuoteTemp can then read from.
Try this instead:
procedure TfrmMakeQuote.performMtrlQuery;
var
i: integer;
begin
for i := 0 to frmMakeQuote.lboSelectedMtrl.Items.Count-1 do
begin
query.SQL.Text := 'SELECT [Material Description], Price FROM MtrlDatabase' +
' WHERE [Material Description] = "'
+ frmMakeQuote.lboSelectedMtrl.Items.Strings[i]
+ '" ORDER BY MtrlID';
query.Open;
query.First;
// do something with query.Fields[0] and query.Fields[1] ...
query.Close;
end;
end;
That being said, searching your materials by their descriptions is not the most efficient search option. You should search by their IDs instead. I would suggest an alternative approach to accomplish that - use your TListBox controls in virtual mode (Style=lbVirtual) instead, and store your search results in separate TStringList objects. That way you can store both IDs and Descriptions together in memory while displaying the descriptions in the UI and using the IDs in queries.
Try something more like this:
procedure TfrmMakeQuote.FormCreate(Sender: TObject);
begin
allmaterials := TStringList.Create;
selectedmaterials := TStringList.Create;
con := TFDConnection.Create(Self);
con.LoginPrompt := false;
con.Open('DriverID=SQLite;Database=C:\Users\kasio\Documents\Embarcadero\Studio\Projects\ProgramDatabase;');
query := TFDQuery.Create(con);
query.Connection := con;
query.SQL.Text := 'SELECT MtrlID, [Material Description] FROM MtrlDatabase ORDER BY MtrlID';
try
query.Open;
while not query.EOF do
begin
allmaterials.Add(query.Fields[0].AsString + '=' + query.Fields[1].AsString);
query.Next;
end;
finally
query.Close;
end;
lboMtrlList.Count = allmaterials.Count;
end;
procedure TfrmMakeQuote.FormDestroy(Sender: TObject);
begin
allmaterials.Free;
selectedmaterials.Free;
end;
// lboMtrlList OnData event handler
procedure TfrmMakeQuote.lboMtrlListData(Control: TWinControl; Index: Integer; var Data: string);
begin
Data := allmaterials.ValueFromIndex[Index];
end;
// lboSelectedMtrl OnData event handler
procedure TfrmMakeQuote.lboSelectedMtrlData(Control: TWinControl; Index: Integer; var Data: string);
begin
Data := selectedmaterials.ValueFromIndex[Index];
end;
procedure TfrmMakeQuote.lboMtrlListDblClick(Sender: TObject);
var
Idx: Integer;
begin
Idx := lboMtrlList.ItemIndex;
if Idx = -1 then Exit;
if selectedmaterials.IndexOfName(allmaterials.Names[Idx]) <> -1 then Exit;
selectedmaterials.Add(allmaterials.Strings[Idx]);
lboSelectedMtrl.Count := selectedmaterials.Count;
end;
procedure TfrmMakeQuote.performMtrlQuery;
var
i: integer;
begin
for i := 0 to selectedmaterials.Count-1 do
begin
query.SQL.Text := 'SELECT [Material Description], Price FROM MtrlDatabase' +
' WHERE MtrlID = '
+ selectedmaterials.Names[i];
query.Open;
query.First;
// do something with query.Fields[0] and query.Fields[1] ...
query.Close;
end;
end;
Lastly, if you switch to a single TCheckListBox or TListView control instead of 2 TListBox controls, you can take advantage of their ability to have checkboxes on each item, then you don't need to deal with the OnDblClick event anymore, and don't need to show two copies of your materials in your UI. The user can just check the desired items before invoking performMtrlQuery().
I would also suggest using a virtual TListView for the search results instead of a TStringGrid. The UI will look better (TStringGrid is not the best looking UI control), and you can utilize memory more efficiently (TStringGrid can be a memory hog if you have a lot of data to display).

Related

How to migrate from ADO Filtering code to Firedac

I have this code:
datamodule1.tbabonne.Filter := '';
if (scGPEdit2.Text) = '' then exit ;
try
ref_Abonne:= QuotedStr (scGPEdit2.Text + '*');
if (scGPEdit2.Text <> '') then
datamodule1.tbabonne.Filter:= Format('(ref_Abonne LIKE %s)', [ref_abonne])
else
datamodule1.tbabonne.Filtered := Trim((scGPEdit2.Text)) <> '' ;
except
abort;
end;
//edit1.Text := '';
end;
My question is :
the code Above didn't work with Firedac while is working as charm in ADO
In FireDAC filters, the wildcards are _ for a single character and % for multiple characters - see http://docwiki.embarcadero.com/Libraries/Sydney/en/FireDAC.Comp.Client.TFDQuery.Filter which gives this example
You can use standard SQL wildcards such as percent (%) and underscore (_) in the condition when you use the LIKE operator. The following filter condition retrieves all Countries beginning with 'F'
Country LIKE 'F%'
So you need to adjust your line
ref_abonne:= QuotedStr (scGPEdit2.Text + '*');
accordingly, to use the LIKE operator and the % wildcard.
Just guessing but maybe ADO used the * wildcard and = operator to insulate e.g. VB users from SQL wildcards and syntax.
UpdateHere is a sample project which uses the FireDAC % wildcard and LIKE operator
in a filter. Take careful note of the inline comments.
TForm1 = class(TForm)
// Create a new VCL project and drop the following components
// onto it. There is no need to set any of their properties
FDMemTable1: TFDMemTable;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
edFilter: TEdit;
// Use the Object Inspector to create the following event handlers
// and add the code shown in the implementation section to them
procedure FormCreate(Sender: TObject);
procedure edFilterChange(Sender: TObject);
public
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.edFilterChange(Sender: TObject);
begin
UpdateFilter;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
DBGrid1.DataSource := DataSource1;
DataSource1.DataSet := FDMemTable1;
// Adjust the following line to suit the location of Employee.Fds on your system
FDMemTable1.LoadFromFile('D:\D10Samples\Data\Employee.Fds');
FDMemTable1.IndexFieldNames := 'LastName;FirstName';
FDMemTable1.Open;
FDMemTable1.First;
// Make the filter disregard string case
FDMemTable1.FilterOptions := [foCaseInsensitive];
UpdateFilter;
end;
procedure TForm1.UpdateFilter;
var
FilterExpr : String;
begin
FilterExpr := edFilter.Text;
if FilterExpr <> '' then
FilterExpr := 'LastName Like ' + QuotedStr(FilterExpr + '%');
FDMemTable1.Filter := FilterExpr;
FDMemTable1.Filtered := FDMemTable1.Filter <> '';
end;
Then just compile and run

Use SQL Server table-valued parameters without creating a type on the SQL Server

I'm trying to pass several values on a single parameter (table-valued parameter) and it works fine following the sample
C:\Users\Public\Documents\Embarcadero\Studio\21.0\Samples\Object Pascal\Database\FireDAC\Samples\DBMS Specific\MSSQL\TVP
SQL:
create type TVPType as table(Code integer, Name varchar(100), RegDate datetime, Notes varchar(max))
go
create table TVPTab(Code integer, Name varchar(100), RegDate datetime, Notes varchar(max))
go
Delphi:
procedure TForm1.btnQryManualSetupClick(Sender: TObject);
var
oDS: TFDMemTable;
i: Integer;
begin
Start;
FDQuery2.SQL.Text := 'insert into TVPTab (Code, Name, RegDate, Notes) ' +
'select Code, Name, RegDate, Notes from :t';
oDS := TFDMemTable.Create(nil);
oDS.FieldDefs.Add('Code', ftInteger);
oDS.FieldDefs.Add('Name', ftString, 100);
oDS.FieldDefs.Add('RegDate', ftTimeStamp);
oDS.FieldDefs.Add('Notes', ftMemo);
FDQuery2.Params[0].DataTypeName := 'TVPType';
FDQuery2.Params[0].AsDataSet := oDS;
(FDQuery2.Params[0].AsDataSet as TFDMemTable).EmptyDataSet;
for i := 1 to C_Recs do
with FDQuery2.Params[0].AsDataSet do begin
Append;
Fields[0].AsInteger := i;
Fields[1].AsString := 'str' + IntToStr(i * 10);
Fields[2].AsDateTime := Now() + i;
Fields[3].AsString := StringOfChar('x', 1000);
Post;
end;
FDConnection1.StartTransaction;
FDQuery2.Execute;
FDConnection1.Commit;
Done('TVP Qry manual setup');
end;
The problem with this code is that it forces me to create a type (in this case the TVPType type) on the server, and I would like to use these table-valued parameters without having to create any object on the server.
Can it be done ?. I have tried defining the parameter as ftDataset but I get an error: The data type READONLY cannot be found.
procedure TForm1.btnQryManualSetupClick(Sender: TObject);
var
oDS: TFDMemTable;
i: Integer;
begin
Start;
FDQuery2.SQL.Text := 'insert into TVPTab (Code, Name, RegDate, Notes) ' +
'select Code, Name, RegDate, Notes from :t';
oDS := TFDMemTable.Create(nil);
oDS.FieldDefs.Add('Code', ftInteger);
oDS.FieldDefs.Add('Name', ftString, 100);
oDS.FieldDefs.Add('RegDate', ftTimeStamp);
oDS.FieldDefs.Add('Notes', ftMemo);
//FDQuery2.Params[0].DataTypeName := 'TVPType';
FDQuery2.Params[0].DataType := ftDataset;
FDQuery2.Params[0].AsDataSet := oDS;
(FDQuery2.Params[0].AsDataSet as TFDMemTable).EmptyDataSet;
for i := 1 to C_Recs do
with FDQuery2.Params[0].AsDataSet do begin
Append;
Fields[0].AsInteger := i;
Fields[1].AsString := 'str' + IntToStr(i * 10);
Fields[2].AsDateTime := Now() + i;
Fields[3].AsString := StringOfChar('x', 1000);
Post;
end;
FDConnection1.StartTransaction;
FDQuery2.Execute;
FDConnection1.Commit;
Done('TVP Qry manual setup');
end;
No. If your multiple values are just a single row you can look into passing them as a comma-separated string and then use the STRING_SPLIT function, or using temp or permanent tables to store multi-row values.

Getting database field value not working

I am using the ado connection a adoquery and dsr and adotable
I have a database with bookingnumbers as a field in the table client.
I would like to get the last bookingnumber in the field and store it in a variable.
The booking number is saved as text on access.
So far I have:
Var
sNum : string;
....
sNum := Datamodule1.tblClient['BookingNumber'].Last;
but it is not working.
please help?
It is not a good idea to try and find the maximum value of a field by navigating the dataset, especially if the dataset isn't necessarily ordered by the field in question. Try something like this instead:
function TForm1.GetMaxBookingNumber : Integer;
var
Q : TAdoQuery;
begin
Q := TAdoQuery.Create(Nil);
Q.Connection := DataModule1.AdoConnection1; // or whatever the name of your connection is
try
Q.SQL.Text := 'SELECT MAX(BookingNumber) FROM CLIENT';
Q.Open;
// the `not IsNull` in the following allows for the table being empty
if not Q.Fields[0].IsNull then
Result := Q.Fields[0].AsInteger
else
Result := -1;
finally
Q.Free;
end;
end;
So here is what i wanted :
var
sNum : string;
procedure button click
begin
with Datamodule1 do
begin
qryClient.SQL.Add('SELECT BookingNumber FROM Client');
qryClient.Open;
qryClient.last;
sNum := qryClient['BookingNumber'];
end;
end;

Eof not triggering

I have a function where I get data from a DB, my test data set returns 6500 rows (I extracted the formatted SQL statement from the SQLText Variable and ran it as a test), but then when I run the following code Eof never triggers and I have seen over 100k rows imported.
ADOQuery := TADOQuery.Create(nil);
ADOQuery.ConnectionString := CONNECT_STRING;
// Build SQL Query
SQLText := Format( 'Select Temp.Serial, Temp.QCSample , Temp.Scrap , Temp.StationID , Temp.Defect , Temp.AddData , Temp2.Serial as Parent_Serial ' +
'from TAB_ELEMENT as Temp ' +
'left join TAB_ELEMENT as Temp2 on Temp.Parent_Id = Temp2.Element_Id ' +
'where Temp.Batch_ID = %d and Temp.StationID = 0 ',[iSearchID]);
ADOQuery.SQL.Clear; // Clear query of garbage values
ADOQuery.SQL.Text := SQLText; // Add query text to query module
ADOQuery.Open;
// Handle Results
iIndexPos := 0;
tDataImport.BeginUpdate;
while not ADOQuery.Eof do
begin
tDataImport.Items[iIndexPos].Serial := ADOQuery.FieldByName('Serial').AsString;
tDataImport.Items[iIndexPos].QCStatus := ADOQuery.FieldByName('QCSample').AsBoolean;
tDataImport.Items[iIndexPos].Scrap := ADOQuery.FieldByName('Scrap').AsInteger;
tDataImport.Items[iIndexPos].StationID := ADOQuery.FieldByName('StationID').AsInteger;
tDataImport.Items[iIndexPos].Defect := ADOQuery.FieldByName('Defect').AsBoolean;
tDataImport.Items[iIndexPos].AddData := ADOQuery.FieldByName('AddData').AsString;
tDataImport.Items[iIndexPos].ParentSerial := ADOQuery.FieldByName('Parent_Serial').AsString;
inc(iIndexPos);
end;
So in summery running this query with these parameters I expect 6500 rows, when I run this it never ends even after 100k+ rows are processed.
Open() places the cursor on the first record and sets Eof accordingly. You are not calling Next() to advance the cursor to the next record and update Eof, so you are processing the same record over and over:
ADOQuery.Open;
while not ADOQuery.Eof do
begin
//...
ADOQuery.Next; // <-- add this!
end;
On a side note, you should be using a parameterized query instead of a formatted SQL query string. It is safer, faster, and more efficient on the DB:
ADOQuery := TADOQuery.Create(nil);
ADOQuery.ConnectionString := CONNECT_STRING;
ADOQuery.SQL.Text := 'Select Temp.Serial, Temp.QCSample , Temp.Scrap , Temp.StationID , Temp.Defect , Temp.AddData , Temp2.Serial as Parent_Serial ' +
'from TAB_ELEMENT as Temp ' +
'left join TAB_ELEMENT as Temp2 on Temp.Parent_Id = Temp2.Element_Id ' +
'where Temp.Batch_ID = :iSearchID and Temp.StationID = 0 ';
with ADOQuery.Parameters.ParamByName('iSearchID') do
begin
DataType := ftInteger;
Value := iSearchID;
end;
ADOQuery.Open;
try
iIndexPos := 0;
tDataImport.BeginUpdate;
try
while not ADOQuery.Eof do
begin
tDataImport.Items[iIndexPos].Serial := ADOQuery.FieldByName('Serial').AsString;
tDataImport.Items[iIndexPos].QCStatus := ADOQuery.FieldByName('QCSample').AsBoolean;
tDataImport.Items[iIndexPos].Scrap := ADOQuery.FieldByName('Scrap').AsInteger;
tDataImport.Items[iIndexPos].StationID := ADOQuery.FieldByName('StationID').AsInteger;
tDataImport.Items[iIndexPos].Defect := ADOQuery.FieldByName('Defect').AsBoolean;
tDataImport.Items[iIndexPos].AddData := ADOQuery.FieldByName('AddData').AsString;
tDataImport.Items[iIndexPos].ParentSerial := ADOQuery.FieldByName('Parent_Serial').AsString;
inc(iIndexPos);
ADOQuery.Next;
finally
tDataImport.EndUpdate;
end;
end;
finally
ADOQuery.Close;
end;

Index out of range (-1) when I click on any item in the TListBox

The TListBox called lboMtrlList is populated with records from the database. The data displays properly when I run the application. When I click on any item in the list, shows the error:
Index out of range (-1)
despite the list not being empty.
Here's the code for populating the lboMtrlList:
procedure TfrmMakeQuote.FormCreate(Sender: TObject);
begin
con := TFDConnection.Create(nil);
query := TFDQuery.Create(con);
con.LoginPrompt := false;
con.Open('DriverID=SQLite;Database=C:\Users\katiee\Documents\Embarcadero\Studio\Projects\ProgramDatabase;');
query.Connection := con;
performQuery;
query.SQL.Text :=
'SELECT [Material Description] FROM MtrlDatabase ORDER BY MtrlID';
try
query.Open;
lboMtrlList.Items.Clear;
while not query.EOF do
begin
lboMtrlList.Items.Add(query.Fields[0].AsString);
query.Next;
end;
finally
query.Close;
end;
//ledtDesc.Height := 81;
//ledtNotes.Height := 51;
end;
I want to be able to double click on an item in the lboMtrlList and move it to another TListBox called lboSelectedMtrl. Here's the code:
procedure TfrmMakeQuote.lboMtrlListDblClick(Sender: TObject);
begin
lboMtrlList.Items.Add(lboSelectedMtrl.Items.Strings[lboSelectedMtrl.ItemIndex]);
end;
I want to be able to double click on an item in the lboMtrlList and move it to another TListBox called lboSelectedMtrl.
Your code is doing the opposite of that. It is trying to move an item from lboSelectedMtrl to lboMtrlList. You are getting the bounds error because there is no item selected in lboSelectedMtrl (lboSelectedMtrl.ItemIndex is -1).
Swap the ListBox variables, and add some error checking:
procedure TfrmMakeQuote.lboMtrlListDblClick(Sender: TObject);
var
Idx: Integer;
begin
Idx := lboMtrlList.ItemIndex;
if Idx <> -1 then
lboSelectedMtrl.Items.Add(lboMtrlList.Items.Strings[Idx]);
end;

Resources