On my form I have a number of TMyQuery Components. Their names identify which MySQL Tables they work with. For example, COMPONENTSTABLE works with the COMPONENTS TABLE, etc.
There are about 30 tables, but that might change in the future.
I also use a basic String List to read field names from a Table called TIMESTAMPS. This table is updated via triggers when an UPDATE, INSERT, or DELETE occurs. Each field within the TIMESTAMPS Table refers to which Table was modified. There's only one record in the table! Based on the field values I can see which table changed so I can refresh it rather than refreshing all of them.
I don't want to do this;
If fieldbyname['COMPONENTSTABLE'] <> CurrentTimeStamp
then ComponentsTable.Refresh;
If fieldbyname['ORDERSTABLE'] <> CurrentTimeStamp
then OrdersTable.Refresh;
{ and so on forever }
What I want to do is;
Right now I have a String List with "Names / Values". Each "Name" is the Fieldname within the Table and "Value" is the TIMESTAMP provided by MySQL Triggers.
I've got the following;
For Idx := 0 to MyStringList.Count -1 do
Begin
If MyStringlist.ValueFromIndex[Idx] <> SomethingElse then
Begin
with (MyStringList.Names[Idx] as tMyQuery).Refresh;
End;
End;
I've got the String List functioning, the Names, the Values etc are all correct.
My question is this;
Is there a way I can use a String ("Names" column in the list) to refer to an Object if that Object exists?
I already have a function I use to refresh individual tables by passing an Object to it, but that's an Object and easy to work with. I'd like to pass the "Object" based on it's name retrieved from a String.
I hope this makes sense and you can follow what I'm after.
I am not sure what your question actually is. In the first part of the answer I assume that you don't really care about names of the objects but rather want some automated way of getting all the tables available refer to a field in another table. Below that, I answer your question about referring to an object if you know its name.
Automated way of handling all tables
It depends on what class your objects are.
From your description, I assume your TMyQuery are TComponent descendants owned by the form. Then the solution is very simple, as each TComponent has both a public Name and a list of owned components Components. You can then use something like this:
var
i: integer;
MyQuery: TMyQuery;
begin
for i := 0 to Pred(MyForm.ComponentCount) do
if MyForm.Components[i] <> TimeStampsTable then
if MyForm.Components[i] is TMyQuery then
begin
MyQuery := TMyQuery(MyForm.Components[i]);
if TimeStampsTable.FieldByName(MyQuery.Name).AsDateTime >= LastAccess then ...
end;
end;
Note that you may want to add extra checks, e.g. to make sure that MyQuery.Name is not empty or that it exists as a field in TimeStampsTable.
If your objects are only TObjects, then there is no "standard" name property and no standard registration of these objects. Name can be handled, apparently your component already has one so it's just a question of a proper type coercion, but object registration is a different matter. You may have to create some kind of a global list for all your created TMyQuery instances.
Getting an object instance based on that object's name
function TMyForm.GetQueryByName(const Name: string): TMyQuery;
var
Obj: TObject;
begin
Result := nil;
Obj := Self.FindComponent(Name);
if Obj <> nil then
if Obj is TMyQuery then
Result := TMyQuery(Obj);
end;
Or you could simply loop over all Components and use your own Name matching.
While the first part of the accepted Answer from #pepak isn't what I was looking for ( I've used similar code in the app previously and found it slow ), the second part of the Answer pointed my in the right direction.
My (thanks to Pepak) eventual solution was;
Function RefreshQueryByName(Const Name: String): Boolean;
Var
Obj: TComponent;
Begin
Result := False;
Obj := Self.FindComponent(Name);
If Obj <> nil Then
If Obj Is TMyQuery Then
With Obj As TMyQuery Do
If Active Then
Begin
Refresh;
Result := True;
End;
End;
Which I use by by passing a String I get from a Field Value that identifies which table I want to refresh.
Now, my Database App automatically refreshes a table changed by other users. It will now refresh any of the 30 tables of they are modified by another user without refreshing all tables.
Thanks for your help Pepak, I've accepted your answer and hope it is useful to others.
Related
How can I use record in TDictionary?
TMyRec = record
a: Integer;
b: Integer;
end;
...
dictionary = TDictionary<String, TMyRec>.create();
...
dictionary[key].a := 30;<<<
Here the compiler gives an error: "Left side cannot be assigned to". How can I solve this problem without creating a separate function for writing myFunc(a, b: Integer): TMyRec?
dictionary[key] returns a copy of the record held by the dictionary. The compiler prevents you from modifying that because it would serve no purpose.
As an aside, older versions of the program would accept your code and it was very confusing that the modification to the record would be lost. You'd make an assignment but nothing visible changed because what you assigned was a nameless local variable.
Clearly you intend to modify the record held in the collection. In order to do that you need to assign the entire record. Read the record from the collection into a local variable. Modify the local variable. Write the updated value back to the collection. Like so:
var
rec: TMyRec;
...
rec := dictionary[key];
rec.a := 30;
dictionary[key] := rec;
One of the frustrating aspects of this is that the code needs to perform two dictionary lookups, even though we know that the second one will find the same record as the first one. Not even the mighty Spring4d dictionary can do this with a single lookup.
David Heffernans answer is what you're after, but I would like to offer an additional warning. Records can have properties just like classes, with getters and setters, and if your record has such properties your code will compile, but it will still not change the actual record value.
TMyRec = record
private
FA : integer;
procedure SetA(const Value: integer);
function GetA : integer;
public
{ Warning: When used on result from dictionary lookup, only the COPY will be
altered, not the actual record in the dictionary! }
property A : integer read GetA write SetA;
end;
A very simple workaround is to use the List property of the record.
You can say:
dictionary.list[key].a := 30;
This will access the dynamic array that backs up the TList via the List property. The compiler already supports direct access to a dynamic array.
If you can login to quality.embarcadero.com, you can see the full discussion of this issue raised as: RSP-23136: We should be able to assign a value to one element in a list of records - posted Dec 18, 2018 and resolved Nov 21, 2019.
The issue was closed with the comment:
"This works as expected. Alternative coding style was provided."
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.
Delphi - NOVICE programmer - (meaning don't expect advanced concepts to be already known )
I am working on an application, part of which is a scheduling function. I look at a group of appointments. These appointments fall on a specific date. I need to create a consolidated view of all the appointments on each date. By this, I mean that I create a "grouping" of data about Jan 17th, this is going to be how many appointments are on that date, when they start, when they stop, etc. This may be an array, a Record, a class, don't know at this point. I may have one for the 17th, then the next one is for the 22nd, then the next is for the 24, and then I may have one every day for the next 35 days... I will have either 0 or 1 structures/containers per day, and I can see having 3 to 5 months months of these...Which ones exist and which ones don't exist will be very fluid. This structure is a consolidated view, meaning that I get some of the information from one appointment, and then some of the information from the next appointment, so as I am reading each appointment, I need to be able to find this structure quickly.
I need these structure to be memory based, so they are fast. They MAY be of different sizes (depending on the number of appointments on that day.
REQUIREMENTS
So, I need to be able to create these structures on the fly. (for example, I have just read an appointment for Jan 22, and need to update the structure, but it doesn't exists yet, so I need to make it on the fly).
I need to be able to find them them quickly. (Hash on TDate maybe).
Each one of the structures will hold multiple data types (boolean, TDateTime, TStringlist, etc).
I have Delphi 2010 if that helps...
What structure am I looking for? Easy, fast, already included with D2010 (or free) are all important.
Thanks
GS
UPDATED INFO:
So it appears the TDictionary or TObjectDictionary is the way to go...
I have decided (I think...) to use a record to hold my base information, and then store those records in either TDictionary or TObjectDictionary. I am running into a challenge in both of them. In TDictionary, I cannot figure out how to free the records when I am done with them (since they are pointers), and with TObjectDictionary, I cannot create it with a record type... Any help appreciated. Code samples are...
// Create the base record definition that will be put in the TObjectDictionary/TDictionary
type
TSummaryAppt = record
Date : TDate;
SA_ID : Integer;
BusyFlag : Array[1..36] of Boolean; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes : integer;
end;
If I go TObjectDictionary, this will not work...
var
Dic : TObjectDictionary<Integer,TSummaryAppt>;
begin
Dic := TObjectDictionary<Integer,TSummaryAppt>.Create([doOwnsKeys, doOwnsValues]);
The Create line fails on execution (compiles fine) with Invalid Class Typecast.
TDictionary appears to be a little friendlier, but I have to deallocate my memory....
var
Dic : TDictionary<Integer,TSummaryAppt>;
rec : ^TSummaryAppt;
p : TSummaryAppt;
i : Integer;
begin
Dic := TDictionary<Integer,TSummaryAppt>.Create;
// Now add some records. THese have to be created dynamically
// because I dont know at compile time how many there are.
new(rec);
rec.Date := now;
rec.SA_ID := 3;
Dic.Add(1, rec^);
new(rec);
rec^.Date := now;
rec^.SA_ID := 5;
Dic.Add(2, rec^);
new(rec);
rec^.Date := now;
rec^.SA_ID := 7;
Dic.Add(3, rec^);
// Test ...
for p in Dic.Values do begin
ShowMessage(IntToStr(p.SA_ID));
end;
// Now free everything. HERE IS WHERE I AM HAVING PROBLEMS...
// What should I be doing?
for p in Dic.values do
p.dispose;
Dic.Values.Free;
Dic.Keys.Free;
Dic.Free;
end;
Any and all help appreciated. What should I be doing different?
Thanks so much!
GS
Regarding your update to the question (which really needs to be a new question), here's what you need to do.
First of all, since both your key and value for the dictionary are value types you need to use TDictionary<K,V>. When you add items to the dictionary, a copy is made of both key and value so this means you need not do any dynamic allocation.
Your code should look like this:
type
TSummaryAppt = record
Date: TDate;
SA_ID: Integer;
BusyFlag: Array[1..36] of Boolean; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes: Integer;
end;
....
var
Dic: TDictionary<Integer, TSummaryAppt>;
rec: TSummaryAppt;
....
// create the dictionary
Dict := TDictionary<Integer, TSummaryAppt>.Create;
....
// initialise rec, in your code you would put real values in
FillChar(rec, SizeOf(rec), 0);
rec.Date := now;
rec.SA_ID := 3;
Dict.Add(1, rec);
rec.Date := now;
rec.SA_ID := 5;
Dict.Add(2, rec);
//etc.
When you have finished with the dictionary all you need to do is free it. The dictionary owns all the contents and will clean up.
Dict.Free;
You may well prefer to wrap up the functionality of the dictionary and expose it through a higher-level interface. So you may have an Add method that received as parameters all the fields of the value to be added. And you may want an update method that received just the mutable fields.
Do you know the generic TDictionary collection?
class TAppointmentCalendar = TDictionary<TDate, TAppointments>
end;
TAppointments also can be a generic class, based on Generics.Collections.TObjectList:
class TAppointments = TObjectList<TAppointment>
end;
These classes can be extended as needed, for example to add properties or data aggregation methods.
Then instantiate a Calendar
Cal := TAppointmentCalendar.Create;
Cal.Add(MyDate, AppointmentsForThisDay);
or retrieve Appointments
var
Appointments: TAppointments
begin
Cal.TryGetValue(ADate, Appointments);
...
TDictionary performs hash based Key lookups.
First of all, as a novice in data structures, it is worth buying and reading this great book: The Tomes of Delphi: Algorithms and Data Structures - By Julian Bucknall.
Here are some potential layout to implement your application.
A. NoSQL database. For instance, take a look at our BigTable Open Source components.
The root component TSynBigTableis available to records within a file-based database. It is light, and very optimized for speed.
Two of their children do handle fields within records. The field layout can change on the fly. See TSynBigTableRecord and TSynBigTableMetaData.
B. Use a regular SQL database, either like TClientDataSet or direct via SQL (you'll find some Open Source components on a static SQLite3 engine (static, i.e. with no external dll needed).
Using SQLite3 as application data, is a very good idea. It is the main purpose of this library - it is used by a lot of programs, like FireFox or Chrome, or even in most Cell phones OS.
In order to make queries fast, you'll have to create indexes on some columns (e.g. date field), and therefore query results will be immediate.
I do not recommend "to be able to create these structures on the fly". It is not a good programming practice IMHO - or it will become very complex: it would need to store the field layout within the records, so it is not a good path for a novice Delphi programmer.
You should better make your data structures open enough to handle any kind of data. With SQLite3 you can serialize your data as BLOB or text (e.g. with JSON). This is e.g. what we allow in our Open Source mORMot ORM - it is client-server, but can be used stand alone. I'd recommend taking a look at our framework documentation, especially the SAD document which tries to present some design approach, like test-driven, ORM or SOA.
C. If you want a TDictionary kind of storage, take a look at our TDynArray wrapper, which handle the same methods but has some unique features like automated serialization or multiple indexes (not handled by TDictionary), which are mandatory for your request.
AFTER QUESTION UPDATE
Some code using TDynArray:
type
TSummaryAppt = record
Date : TDate;
SA_ID : Integer;
BusyFlag : Array[1..36] of Boolean; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes : integer;
end;
TSummaryApptDynArray = array of TSummaryAppt;
var rec: TSummaryAppt;
SAs: TSummaryApptDynArray;
SA: TDynArray;
F: TFileStream;
begin
SA.Init(TypeInfo(TSummaryApptDynArray),SAs);
rec.Date := now;
rec.SA_ID := 3;
SA.Add(rec); // rec is now added in SAs[]
assert(length(SAs)=1); // or SA.Count=1
assert(SAs[0].SA_ID=3);
for rec in SAs do // will work like any dynamic array
ShowMessage(IntToStr(p.SA_ID));
F := TFileStream.Create('datafile',fmCreate);
SA.SaveToStream(F); // a TDictionary won't do that
F.Free;
SA.Clear;
assert(length(SAs)=0); // or SA.Count=0
F := TFileStream.Create('datafile',fmOpenRead);
SA.LoadFromStream(F); // a TDictionary won't do that
F.Free;
assert(length(SAs)=1); // or SA.Count=1
assert(SAs[0].SA_ID=3);
for rec in SAs do // will work like any dynamic array
ShowMessage(IntToStr(p.SA_ID));
// you need nothing to free the memory, since both are handled by the compiler
end;
Of course, your array may be stored directly in one block, since it contains only plain data (double, integer, booleans); but our TDynArray wrapper is able to handle any string or other dynamic array within.
Some code using our ORM:
type
TBusyFlag = set (1..36);
TSummaryAppt = class(TSQLRecord)
private
fDate : TDate;
BusyFlag : TBusyFlag; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes : integer;
published
// already contains an ID: integer field
property Date : TDate read fDate write fDate;
property BusyFlag : TBusyFlag read fBusyFlag write fBusyFlag ; // 15 minute periods...
property PCTFree: Double read fPCTFree write fPCTFree;
property LargestFreeMinutes : integer read fLargestFreeMinutes write fLargestFreeMinutes;
end;
// then initialize the database model and use your database:
Model := TSQLModel.Create([TSummaryAppt]);
Client := TSQLRestClientDB.Create(Model,nil,'FileName',TSQLRestServerDB);
Client.Server.CreateMissingTables(0); // will create the database if needed
...
rec := TSummaryAppt.Create;
rec.Date := Now;
rec.ID := 3; // but the ORM may create one unique ID for you
Client.Add(rec);
rec.Date := 0;
Client.Retrieve(3,rec);
...
rec.Free;
Client.Free;
Model.Free;
Our ORM is here used locally, all in one executable, creating a SQLite3 database for data storage. But if you change TSQLRestClientDB into TSQLite3HttpClient and TSQLRestServerDB+TSQLite3HttpServer, you'll be able to use your data remotely, via standard JSON (and also from an AJAX application). Without modifying your client code. And if you want to store your data with something else than SQLite3 (even up to Oracle, or an in-memory database), you can.
This example is of course simplified, but basically I have a main form that triggers another form (frmSettings) with
function Execute(var aSettings: TSettings):Boolean
TSettings is my own object created in main form for keeping track of the settings.
In this newly opened form (frmSettings) I fetch a TMyObjectList that is a descendant from TObjectList.
It's filled with TMyObj.
I then fill a TListBox with values from that TMyObjectList.
the code:
...
FMyObjectList : TMyObjectList;
property MyObjectList: TMyObjectList read getMyObjectList;
...
function TfrmSettings.getMyObjectList: TMyObjectList ;
begin
If not Assigned(FMyObjectList) then FMyObjectList := TMyObjectList.Create(True)
Result := FMyObjectList;
end;
function TfrmSettings.Execute(var aSettings: TSettings): Boolean;
begin
//Fill myObjectList
FetchObjs(myObjectList);
//Show list to user
FillList(ListBox1, myObjectList);
//Show form
ShowModal;
Result := self.ModalResult = mrOk;
if Result then
begin
// Save the selected object, but how??
// Store only pointer? Lost if list is destroyed.. no good
//Settings.selectedObj := myObjectList.Items[ListBox1.ItemIndex];
// Or store a new object? Have to check if exist already?
If not Assigned(Settings.selectedObj) then Settings.selectedObj := TMyObj.Create;
Settings.selectedObj.Assign(myObjectList.Items[ListBox1.ItemIndex];);
end;
end;
procedure TfrmSettings.FillList(listBox: TListBox; myObjectList: TMyObjectList);
var
i: Integer;
begin
listBox.Clear;
With myObjectList do
begin
for i := 0 to Count - 1 do
begin
//list names to user
listBox.Items.Add(Items[i].Name);
end;
end;
end;
procedure TfrmSettings.FormDestroy(Sender: TObject);
begin
FreeAndNil(FMyObjectList);
end;
Storing just the pointer doesn't seem as a good idea, as triggering the settings form again, recreates the list, and the original object would be lost even if user hits "cancel"
So storing a copy seems better, using assign to get all the properties correct. And first checking if I already have an object.
If not Assigned(Settings.selectedObj) then Settings.selectedObj := TMyObj.Create;
Settings.selectedObj.Assign(myObjectList.Items[ListBox1.ItemIndex];);
Should I move those two lines to a method instead like Settings.AssignSelectedObj(aMyObj:TMyObj)
Does this look correct or am I implementing this the wrong way?
Something more/less needed?
I need some guidelines so I feel more secure that I don't open up for memory leaks and other trouble.
Other than that reviewing the code a bit, the real question is: Is this the correct way to store my SelectedObject in the settings class?
Is this the correct way to store the selected object in the settings?
Probably not. Your settings class should not depend on the form in any way. What if you decide to create and destroy your form dynamically each time the user opens the settings? In this case your settings would hold an invalid object reference.
IMHO it is better to store the object list in the settings together with the index of the selected object. The form should just access the settings, fill the list box and modify the selected object index after the user confirmed with OK.
You are producing a memory leak in your code. You create a TObjectList as a local variable but you never free it. And if you free the local variable, the object references in the listbox will be invalid. You have two options:
Store the object list as a member variable of your form, create in the FromCreate event handler and destroy it in the FormDestroy event handler. You can then safely use object references in your list box.
Store the object list somewhere outside and pass it into the form as a parameter of the Execute method. In this scenario, you can also safely use object references.
I would rename myObjectList to GlobalObjectList, and move it out of the class. It can be declared in the form, but create/free in the initialization/finalization sections. During initialization, after you create the list, populate it from the ini file (or wherever you store it). Now you can access it from anywhere that has your unit in the Uses.
What about the serialization of TSettings? Put your settings in some published properties, then let the RTTI save its content:
type
TSettings = class(TPersistent)
public
function SaveAsText: UTF8String;
end;
function TSettings.SaveAsText: UTF8String;
begin
var
Stream1, Stream2: TMemoryStream;
begin
Stream1 := TMemoryStream.Create;
Stream2 := TMemoryStream.Create;
try
Stream1.WriteComponent(MyComponent);
ObjectBinaryToText(Stream1, Stream2);
SetString(result,PAnsiChar(Stream2.Memory),Stream2.Size);
finally
Stream1.Free;
Stream2.Free;
end;
end;
Then your settings can be stored in a text file or text string.
It's just one solution. But storing settings as text is very handy. We use such an approach in our framework, to store settings via a code-generated user interface. A settings tree is created, from a tree of TPersistent instances.
I am helping out my company with some old delphi 7 code.
There is a record declared at the start that is used throughout to store all the data we want outputted.
type
TOutput_Type = record
result: String;
resultoffset: String;
selected: boolean;
resultcategory: integer;
end;
and then an array of this is declared
Output: array of TOutput_Type;
The length is set at the start to a large value, as actual length is unknown.
This array is used all over the place, but unfortunately the value selected is not always set when used.
My problem is I am adding in a summary of the data, but because selected is not set, delphi seems to give it a random true or false status.
Is there a way of setting all instances of selected as true at the start? Seems like a simple enough thing to do, but I'm not a delphi programmer so am unsure if its possible? I know I can go through and add in selected := true every time a new record is made, but I'd like to do it cleanly at the start if possible....
Thanks in advance
After calling SetLengt for Output variable you must first initiate the new record parts (because new allocated memory isn't defined) in for loop.
Something like:
OldLength := Length(Output);
SetLength(Output, NewLength);
for n := OldLength to NewLength -1 do
Output[n].selected := True;
Records, unlike objects, aren't initialized upon creation, so you need to initialize them yourself. Since you're on Delphi 7, you can't use records with methods, so what I'd do is make an initialization function, something like this:
type
TOutputArray: array of TOutput_Type;
function CreateOutputArray(length: integer): TOutputArray;
var
i: integer;
begin
SetLength(result, MyArbitraryItemCount);
FillChar(result[0], Length(Output)*SizeOf(TOutput_Type), 0);
for i := 0 to high(result) do
result[i].selected := true;
end;
I'd go for the factory method like in the question dcp linked to. Parameterless constructors aren't allowed for records, so you would always have to specify some parameters, which might be annoying if you don't really need them.
If this is all about initializing the content of the large array once at the start you could also use this:
SetLength(Output, MyArbitraryItemCount);
FillChar(Output[0], Length(Output)*SizeOf(TOutput_Type), 1);
Then everything is 1. Including selected :) Of course you could also use a for-loop...