Delphi: Displaying a subset of a data set in data-aware controls - delphi

I've got an in-memory dataset with several fields, one of which is a primary key that another dataset references as a foreign key. Thing is, the master dataset can have multiple references to the detail dataset. (This is modeling an object that contains a dynamic array of other objects.)
If there was only one of each sub-object, I could make the right association with the KeyFields and LookupKeyFields properties of the reference field in the master dataset, but that's only designed to return one result. I want to load all the records whose primary key matches the right ID key and display them in a listbox.
I thought a TDBListBox would help with this, but it turns out that's not what they do. So how would I populate a listbox or similar control with the result set of a multiple-match check like that for further editing? I want something similar to the result of a SQL query like this:
select field1, field2, field3
from client_dataset
where client_dataset.primary_key = master_dataset.id
Only thing is, this is done entirely with in-memory datasets. No real databases are being used here. Does anyone know how this can be done?

The dataset has a Filter property which can be set with a condition. You also have to set the filtered flag on true. And with he datacontrols you can select which fields are visible.
So:
var
c : TColumn;
begin
clientdataset.Filter := Format('primary_key = %d', [master_dataset.id]);
clientdataset.Filtered := True;
c := DBGrid1.Columns.Add;
c.FieldName := 'field1';
c := DBGrid1.Columns.Add;
c.FieldName := 'field2';
c := DBGrid1.Columns.Add;
c.FieldName := 'field3';
end;
Should do the trick.

Related

How to allow selecting a NULL value in a TDBLookupComboBox?

I have a TDBLookupComboBox showing a TStringField of kind fkLookup, which allows Null values (from a nullable Integer database column).
The dropdown list displays the items from the assigned LookupDataSet, which comes from a joined table. If the field is Null, none of the list items are displayed, the combobox is empty. If the field has a value, the correct description is shown.
I can reset it to Null by pressing the assigned NullValueKey.
That's all ok, but the users prefer using the mouse. So I could provide a clear button, but I think an additional row on top of the list would be much better. How can I do that?
You can put the empty row in your query, and if you need it sorted you can make it appear at the top in your list like this:
select 0 as sort,
convert(int, null) as UserID,
'Clear' as Name
union all
select 1 as sort,
u.UserID,
u.Name
from tblUser u
order by sort, Name
The sort column will make it appear at the top, after that you can sort on whatever you need.
This is not exactly what was requested, but it might be the better solution for developers having a DevExpress VCL ExpressEditors subscription: there is a hidden feature in TcxDBLookupComboBox which can provide a nice clear button inside the combobox!
It works exactly like in the TcxButtonEdit, where you can add buttons at designtime, just that this Buttons property isn't exposed in TcxDBLookupComboBox, so it can only be set at runtime :(
procedure TForm1.FormCreate(Sender: TObject);
begin
AddClearButton(cxDBLookupComboBox1.Properties);
end;
procedure TForm1.AddClearButton(Prop: TcxCustomEditProperties);
begin
with Prop.Buttons.Add do begin
Kind:= bkText;
Caption:= #$2715; //Unicode X-symbol
end;
Prop.OnButtonClick:= ClearButtonClick;
end;
procedure TForm1.ClearButtonClick(Sender: TObject; AButtonIndex: Integer);
begin
if AButtonIndex = 1 then
with TcxCustomEdit(Sender) do begin
EditValue:= Null;
PostEditValue;
end;
end;
This might also work with other edit controls, however, at least with TcxDBSpinEdit it does not.
You can use as your LookupDataSet a query with the next SQL (Firebird syntax)
SELECT CAST (NULL AS INTEGER) AS ID, CAST ('clear' AS VARCHAR(80)) AS NAME FROM table_name
UNION
SELECT ID, NAME FROM table_name
However, in this implementation clear item will be at the end of the list.
Added after our discussion in the chat
I'm afraid we never reach to have null-value field's behavior like normal fields, because null is not a value, as was correctly mentioned here: https://petercai.com/null-is-not-a-value/ . We can only make some workarounds for it.
For example, we can see custom displayed value for null such as SELECT CASE WHEN OurField IS NULL THEN '(empty)' ELSE OurField END AS OurField. And we can set null with artificial menu item. But this is not a full, complex solution.

How to modify calculated field with TADODataSet?

I have a TADODataset executed with (only for example):
SELECT id, name, lastname, name + ' ' + lastname as fullname
FROM persons
ORDER BY lastname
After I open the dataset, I can modify "name" and "lastname" fields, but can't modify"fullname", because it's calculated.
I try to open TADODataset to TClientDataset via DataProvider, but it takes too long (there are about 100K records in source dataset):
SrcDS.FieldDefs.Update;
for i := 0 to Pred(SrcDS.FieldDefs.Count) do
SrcDS.FieldDefs[i].CreateField(SrcDS).ReadOnly := false;
DestDS := TClientDataset.Create(nil);
DestDS.SetProvider(SrcDS);
DestDS.Open;
DestDS.SetProvider(nil);
All in all, i want to have an independent dataset with changeable fields.
How can i modify calculated fields in the dataset?
You have to calculated the field in Delphi. Create a new field by rightclicking on the TADODataset component, select New Field, give it a name and set it's type to 'calculated'.
In the OnCalculateFields-Event simply write:
Procedure TMyDataModule.MyDatasetCalculate(Sender : TDataset);
Begin
MyDataSetFullName.AsString := MyDatasetFirstName.AsString+' '+MyDataSetLastName.AsString;
End;
Update: Regarding your second problem (100.000 records): If you load them into your ADODataset using LockType = ltBatchOptimistic, it will be fast enough and nothing is saved to the database, unless you call the UpdateBatch Method.
If this is still too slow, try using the async load feature (See the ExecuteOptions)

Get name of table that TField is from

I'm using a TDataSet where the CommandText property is set to an SQL query. I have also made the following function which creates part of an SQL query based on the fields of TDataSet. It is however incomplete. As you can see I still need to get the name of the table that a TField is from. How do I achieve this?
function GetDataSetFieldsMSSQL(Dataset: TDataSet): String;
var
I, L: Integer;
TableName: String;
begin
Result := '';
L := Dataset.Fields.Count;
if (L > 0) then
begin
TableName := ... // Name of the table for the Dataset.Fields[0] field.
Result := '[' + TableName + '].[' + Dataset.Fields[0].FieldName + ']';
I := 1;
while (I < L) do
begin
TableName := ... // Name of the table for the Dataset.Fields[I] field.
Result := Result + ',[' + TableName + '].[' + Dataset.Fields[I].FieldName + ']';
Inc(I);
end;
end;
end;
You can use the Delphi Function GetTableNameFromQuery(SQL : String):String; from the DBCommon unit. Just Add The DBCommon on the uses. =)
Maybe there is no solution at all for a simple TDataSet?
I believe not. Because an TDataset can source its' data not only from RDBMS' tables.
It can be:
an RSS feed
An XML file. Example: TCliendataset is an TDataset descendant that can read XML from its'
own format or using an XMLTransformProvider.
It can be an SQL for reading an Excel spreadsheet or a text file if you have an ODBC driver for
that and configured the datasource.
Sky (and the imagination of Delphi's programmers around the world) is the limit for what a field can represent in an TDataset.
You have some alternatives, since you are using an ADODataset:
Parsing the commandText of ADOCommand
Using the BASETABLENAME property of ADORecordSet (as in kobik's comment)
Guessing by convention ( Abelisto's answer )
As I know there is no any way to get the name of the table from the SQL query component.
However you can give aliases for fields, for example: "select foo_field as foo_dot_foo_field from foo" and then replace them to the correct syntax: "Result := '[' + StringReplace(DataSet.Fields[0].FieldName, 'dot', '].[', [rfReplaceAll]) + ']'"
What you are trying to do is impossible if you have no knowledge or control over the SQL used in the query.
The query could contain calculated/computed fields or could be returning fields from a view etc. Furthermore the database might have several tables that contain the same field names.
If possible you can query the SQL server view INFORMATION_SCHEMA.COLUMNS and that way try to figure out what table a fieldname is from. However if the field names are not unique this might also prove impossible.

Custom sort order for dataset after executing query?

I want the result set of a database query to have a certain order. The information I want to order by is not contained in the database, but dynamically generated in code (so I cannot use ORDER BY).
Is there a way to sort a dataset after executing the database query? (I don't need indexed access but only want to iterate over all records.)
With a ClientDataset you are able to change the order after executing.
Settings IndexFieldNames sorts the dataset.
You can find information here how to connect a clientdataset to another dataset in the same application.
object DataSetProvider1: TDataSetProvider
DataSet = MyAdsQuery
Left = 208
Top = 88
end
object ClientDataSet1: TClientDataSet
Aggregates = <>
Params = <>
ProviderName = 'DataSetProvider1'
Left = 296
Top = 88
end
There is a possibility that shares similarities with Jens' answer (+1) but gets to the result in a slightly different fashion.
Given an existing table:
create table somedata (id integer, name char(20));
insert into somedata values ( 1, 'Tim' );
insert into somedata values ( 2, 'Bob' );
insert into somedata values ( 3, 'Joe' );
If you know the desired short order (either by processing the table or some query result from it), create a temp table that has some key value to match the desired rows from the original table and then the sort order data:
create table #sortorder( id integer, sortvalue integer );
Set the sortvalue field in the temp table to contain the desired order (it could be any sortable data type - doesn't have to be integer):
insert into #sortorder values ( 1, 15 );
insert into #sortorder values ( 2, 12 );
insert into #sortorder values ( 3, 5 );
Then generate the results with a join against the table that provides the sort order:
select sd.* from somedata sd, #sortorder so
where sd.id = so.id
order by so.sortvalue;
AFAIK the only reliable way to sort a dataset is to use ORDER BY.
I would:
Add a dummy order_tag field to your query.
Dump the results to temporary table.
Declare a cursor to iterate over the temporary table and set the order_tag using your custom logic and UPDATE #temp_table statements.
Select the data from the temporary table and order by the tag field.
The main trick here would be to use an Internal calc field (FieldKind = fkInternalCalc) if they are supported by your TDataset sub-class. If they aren't, use a TClientDataset as an intermediate.
DFM:
object ClientDataSet1SortField: TIntegerField
FieldKind = fkInternalCalc
FieldName = 'SortField'
end
pas:
procedure TForm1.FormCreate(Sender: TObject);
begin
ADOConnection1.Open('dbuser', 'Hunter2');
ClientDataSet1.SetProvider(ADOQuery1); // set ClientDataset provider. This will create a TLocalAppServer provider "in the background"
ClientDataSet1.Open;
randomize;
while not ClientDataSet1.Eof do
begin
ClientDataSet1.edit;
ClientDataSet1SortField.AsInteger := random(100);
// as ClientDataSet1SortField is fkInternalCalc it doesn't need to be in the query result set, but can be assigned and used for sorting
ClientDataSet1.Post;
ClientDataSet1.Next;
end;
clientdataset1.IndexFieldNames := 'SortField';
end;

How do you add a lookup field to a dataset?

I've got a dataset that I need a lookup field for. Problem is, this dataset's structure is defined by the result of a query. I can't add the field as a TFieldDef before setting .Active = true; because it gets overwritten, and I can't add it after running the query because you can't alter the structure of an open dataset.
There has to be some way to do this. Does anyone know how?
EDIT: There seems to be some confusion about what I'm looking for. I'm not looking for a lookup at query time. I'm looking for a lookup field, a TField object where FieldKind = fkLookup, so that it can be used with a data-aware lookup combo box, for editing the data after the query has returned its result. This has nothing whatsoever to do with the SQL and everything to do with Delphi's dataset model and data-aware controls.
The easiest way is to define persistent fields at design time.
You could also modify your SQL statement to get the calculated values from the server.
You need to create the fields yourself before you open the dataset.
First get all your field definitions from the database
DataSet.FieldDefs.Update;
Then loop through each fielddef and create the normal field, and also create the lookup field where appropriate. Simplified version of the code like this;
for I := 0 to DataSet.FieldDefs.Count - 1 do
begin
// Allocate a normal field
AField := DataSet.FieldDefs[I].CreateField(DataSet);
// Add lookup field if we have another table to look it up from
if (??? this is the key field of a lookup table) then
begin
AField := TStringField.Create(DataSet.Owner);
AField.FieldName := ???;
AField.DataSet := DataSet;
AField.FieldKind := fkLookup;
AField.KeyFields := ???;
AField.LookupKeyFields := ???;
AField.LookupDataSet := ???;
AField.LookupResultField := ???;
end;
end;
Then you can open the dataset.
You have two datasets on your form (say tblOrder,tblCustomer)
One field in the order is a foreign key to the customer table customerId
The Key field of the customer table is Id, Name = Name
Add all fields (right click on the datasets, fields editor , add all fields.
Then right click on the order table and choose fields editor then right click new field.
Name = myLookup,Type is string,Size is xx, FieldType = Lookup.
Key field = customerid,dataset = tblCustomer, lookup Key = Id,Result field = Name.
Now your lookup field is defined.
To make it work in an editor (say in a TDBLookupCombo)
Add a datasources to the form dsOrder
Connect it to tblOrder.
Now set datasource = dsOrder,Field = myLookup
You don't have to set the lookup source...
Let you have table Orders with field (among others) CustomerId (of type Integer), and table Customers with fields CustomerId (of type Integer) and CustomerName (of type String). Then, in Delphi IDE, drop on the form datasets table_main and table_lookup (for tables Orders and Customers respectively). Open fields editor for table_main, add (among others) field CustomerId, then create new field with field properties:
name: CustomerName (for example)
type: integer
field type: lookup
and with lookup properties:
dataset: table_lookup
Key fields: CustomerId
Lookup Keys: CustomerId
Result Fields: CustomerName
Hope it is still relevant now.

Resources