List all queries connected through ado connection - delphi

I have application that has ADO connection on main form and several plugins that have ADO queries which I connect to this main connection. One problem is that I can't properly design those plugins without their personal connection which becomes messy when I connect plugins to main app. One plugin has plenty of queries.
I can use ConnectionObject to pass plugin's queries through main connection, but this is non-convenient for me, because when main connection needs to reconnect, I can't automatically reconnect all the queries. So I have to reassign those plugins' Connection property to main connection after plugin creation.
I know that one can list all active queries using ADOConnection's DataSets property. But what property should I use if I want to list both active and inactive DataSets? The IDE lists them automatically in designer, so I think there should be a generic way to do this.

Perhaps documentation regarding TADOConnection.DataSets which can be found here has confused you.
It says:
Use DataSets to access active datasets associated with a connection
component.
This might leed to thinking that DataSets keeps only active datasets which is not the case. To test this, just put one TADOConnection and one TADOQuery component on a form and set up TADOQuery.Connection to the instance of your connection, for example ADOConnection1.
To test that DataSets property keeps also inactive datasets you might use this code:
procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
begin
for i := 0 to ADOConnection1.DataSetCount - 1 do
begin
if not ADOConnection1.DataSets[i].Active then
ShowMessage('Inactive dataset!');
end;
end;

Related

Run-time Equivalent to Assign Local Data... for TClientDataSet and TSQLQuery

The TSQLQuery class is unidirectional, so for it to be used as a source for a data-bound TDBGrid, a TClientDataSet needs to be linked between the TSQLQuery and the TDataSource that the TDBGrid is bound to.
I can connect the TSQLConnection and make the TSQLQuery active at design-time, with specified params, and then I can right-click on the CDS and choose the "Assign Local Data..." option to have it grab the data from the TSQLQuery, which then appears in the TDBGrid via the linked TDataSource.
The amount of time between choosing "Assign Local Data..." and the data actually appearing in the grid is very short, so I am looking for the way to replicate this at run-time.
Supposedly, I can set the Data property of the CDS to the Data of the source, but a TSQLQuery has no Data property. There was a post about using a provider, but
DataSetProvider1.DataSet := SQLQuery1;
ClientDataSet1.Data := DataSetProvider1.Data;
throws an access violation,
I did implement the data copy by looping through the TSQLQuery and appending the records to the TClientDataSet, but that was a lot slower than the "Assign Local Data...".
[Edit 1]
All the components I need are hooked up at design-time, and I can make the TSQLConnection active, then the TSQLQuery, then the TClientDataSet and the TDBGrid displays the data from the parameterised query defined in the TSQLQuery.
In the OnChange event of a TComboBox, I need to refresh the query using a different parameter and have the grid show the relevant results, so I Close the TSQLQuery, change the ParamByName value, Open the TSQLQuery and then call ClientDataSet1.Last to highlight the last row in the grid.
That gives me a "Cannot perform this operation on a closed dataset" error, so I use
ClientDataSet1.Active := true;
but that throws an "Access Violation".
All the examples I can find are all about dropping components onto a form, linking them together, and they work. Well, yes they do, but I need to change properties in code at run-time and still have it work, which it just refuses to do.
This is really beginning to frustrate me.
[Edit 2]
So I followed the example on the Embarcadero site for building a VCL Forms dbExpress Database Application, substituting my database connection details for the Interbase one the example uses.
http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Building_a_VCL_Forms_dbExpress_Database_Application
In the designer, everything looked fine and the grid was showing the results from the query, but when I went to run it using F9, I was getting an "Access Violation" thrown from within TCustomClientDataSet.InternalCheck.
Turns out this is a known MIDAS.DLL versioning problem and can be resolved by including MIDASLib in the uses clause of the form. It is just unfortunate that the Datasnap.DBClient code is still throwing Access Violations instead of proper messages, especially since this problem was reported in 2013.
You can use this code. Just change TUniConnection and TUniQuery to what You are using:
Procedure CdsAssignTable(aTableName:String; aCds : TClientDataSet; aCon
:TUniConnection );
Var
aQUery : TUniQuery;
aProvider : TDataSetProvider;
begin
if aCon=Nil then raise Exception.Create('aCon=Nil');
if aCds=Nil then aCds:=TClientDataSet.Create(aCon.Owner);
aQUery:=TUniQuery.Create(Nil);
aQUery.SQL.Text:='select * from '+aTableName;
aQUery.Connection:=aCon;
aQUery.Open;
aProvider:=TDataSetProvider.Create(Nil);
aProvider.DataSet:=aQUery;
aCds.Data:=aProvider.Data;
FreeAndNil(aProvider);
FreeAndNil(aQUery);
End;

Copying Clientdataset records to Database table

What I'm trying to achieve to to insert/copy records from a tClientDataSet to a database table(Database is Sybase ASA).
I also have a form with a cxgrid on it which I can see the records from the cds, so I know there are records in it.
At the click of a button I do the following:
with dmData.cds do
begin
Close;
Open;
First;
while not (EOF) do
begin
dmData.qry1.Open;
dmData.qry1.Insert;
dmData.qry1.FieldByName('field1').AsString := dmData.cds.FieldByName('field1').AsString;
dmData.qry1.FieldByName('field2').AsString := dmData.cds.FieldByName('field2').AsString;
dmData.qry1.FieldByName('field3').AsString := dmData.cds.FieldByName('field3').AsString;
dmData.qry1.Post;
Next;
end;
end;
I don't get any errors after this is done but when looking in the database table there are no records inserted.
I don't know what I'm doing wrong, any help would be much appreciated.
It seems you are trying to do the work that TClientDataSet does for you. In order to have all this working you need three components:
An instance of a dataset able to talk to your data server, already configured to do so
An instance of TDatasetProvider referencing the previous dataset, by using the Dataset property
An instance of TClientDataSet referencing the previous provider, by using the ProviderName property
After all the records in TClientDataset (CDS) were updated, you call ApplyUpdates(0) to send them to the provider. When you call this method, the CDS builds a data pack named Delta with the records that have to be persisted and send it to the provider.
The provider does not know how to persist the records existing in the Delta, so it coworks with the dataset you assigned to it. For each record in the Delta, the corresponding operation is executed over the dataset, so the data server will start receiving commands.
At the end, the provider notifies the CDS that everything was alright (this is named reconciliation), eventually returning keys generated during the insert operations. Those keys will appear in the CDS.
After all that, the status of the changed records will be cleared, in order to report no pending changes (an important something that your code was not doing).
I recomend you read more about DataSnap to really master it. The Delphi help has enough information on that.

Fast Reports Non Database

I have been using Report Builder for some years, but I am getting tired of the cha-ching, cha-ching. It is a great reporting tool for "non database" reports.
I have started playing around with Fast Reports and I am utterly flustered with it. It seems a great reporting tool for databases but big question mark concerning complex "non database" reports. Their demos and help are horrible.
Wished I could show a report I am talking about. The report is a serial communications report that has operating system information which of course is single in nature. It has 4 distinct table which has installed serial ports and USB Serial Device tables. It also has a summary memo.
Has anyone successfully designed a report of the above configuration in Fast Reports? And yes, I have posted the same query with Fast Reports. Just want other opinions.
Thanks in advance.
I've extended the option provided by #jrodenhi's answer, which seems to be the proper way to do what you want (thus I've made this answer a community wiki). So here are the two options you can use (and I think there will be more of them):
1. Define custom variables
The answer by #jrodenhi shows how to add report variables in code. I'll try to show here, how to define them in report designer.
Open report designer
Go to menu Report / Variables...
In the Edit Variables window create a new category by clicking the Category button. Then you can rename the category the same way as you do e.g. for files in Windows Explorer:
Then you can declare a custom variable by clicking Variable button. You can give to the variable some meaningful name the same way as you do e.g. for files in Windows Explorer:
After this save your changes by clicking OK button:
Then you'll get back to the report designer, where you can find your just declared variables in the Data Tree pane tab Variables from where you can drag and drop the variables to the report:
After you refine all component positions and properties you can close the report designer and go back to the Delphi IDE. There you can write a handler for the OnGetValue event of your report and if its VarName parameter equals to your variable, change its Value parameter to the value you want to give to the associated report component:
procedure TForm1.frxReport1GetValue(const VarName: string; var Value: Variant);
begin
if VarName = 'MyVariable' then
Value := 'This is a new value!';
end;
2. Modify report components directly from Delphi code
There is an option of direct access to report components from your Delphi code. For instance, to find a certain report component by name you can use the FindObject method of the TfrxReport object. Then you can follow the usual pattern of checking if the returned reference is of type of the control you want to access and if so, you can access the instance by typecasting, as usually.
For example to find a TfrxMemoView object of name Memo1 on the frxReport1 report and modify its text you may write:
var
Component: TfrxComponent;
begin
Component := frxReport1.FindObject('Memo1');
if Component is TfrxMemoView then
TfrxMemoView(Component).Memo.Text := 'New text';
end;
Most of the reports I write are for payroll. In addition to the table based data that makes up the bulk of the report, there is usually a significant amount of singular data, such as user and employer information that is frequently most easily entered as a series of variables taken from a filter dialog that runs before the report. Once the filter has run I iterate through all of its variables and add them to the report variables. Maybe you could pick up your singular system variables and do the same.
This is my routine that runs just before I prepare a report. It not only adds variables, it also adds functions defined in Delphi that can be used in the report:
procedure TFastReportDriver.SetVariables(sVariables: String; var frxReport: TfrxReport);
var i,
iDetail: Integer;
sl: TStringList;
sVariable,
sValue: String;
begin
sl := TStringList.Create;
sl.CommaText := sVariables;
frxReport.Variables.Clear;
frxReport.Variables[' Filter'] := Null;
for i := 0 to sl.Count - 1 do begin
sVariable := sl.Names[i];
sValue := Qtd(sl.ValueFromIndex[i]);
frxReport.Variables.AddVariable('Filter', sVariable, sValue);
end;
frxReport.AddFunction('procedure CreateCheck(hPaystub, iCheckNo: Integer)');
frxReport.AddFunction('function NumberToWords(cAmount: Currency): String');
frxReport.OnUserFunction := repChecksUserFunction;
sl.Free;
end;
Then, in your code, you call frxReport.DesignReport and at run time you can drag and drop your variables onto your report:
And, if you have defined any functions like above, they show up under the functions tab.
I've had success with this method:
Use an in-memory table, such as TkbmMemTable, TdxMemTable or any in-memory table.
Link the in-memory table to the TfrDataSet.
Populate the in-memory table using usual TDataset methods and run the report.
You might want to take a look at using NexusDB as a way to hold data for the report. NexusDB is a database which besides being a full client server database can also run as a totaly in-memory database with full SQL support and no external dlls or anything required as it is compiled into your app. They have a free embedded version that will satisfy your needs. I use it for many things, it honestly a wonderful tool to have in your toolbox.

How to create a report using fast Reports with out connecting directly to a database

I have been asked by my company to update the reporting functionality of a paticular application written in delphi and using Quick reports to use FastReports instead.
The current implementation pulls all the data out of the database, does a lot of work to organise and calculate the required data for the reports and stores all this in several different objects. The Quick Report OnNeedData events are then used to fill out the bands until there is no more data (signified by setting MoreData = false)
The problem I'm having is Fast Reports seems to need a band to be connected to an actual data source, which I don't have. Also fastReports doesn't seem to have an event similar to OnNeedData.
Is there anyway to fill out the values of a data band in code and have it print again until all data is printed without connecting the band to a dataset?
I appologise for the vagueness of this question, I am very new to reporting software and any suggestions of where to go and what to look at would be greatly appreciated.
Fast reports use a intermediary object descending from _TFrxDataSet to connect the report engine which the data it prints.
To connect a report to a data source managed by the program itself, you use a TfrxUserDataSet component, which let's you see a data set inside the report, but you manually specify the column names in the Fields (TStrings) property and manage and supply values programatically writing event handlers for the following events:
OnCheckEOF is functionally equivalent to the OnNeedData, if there's no more to print, you set the EOF var parameter to true
OnFirst you do whatever you have to do to start walking for the data.
OnGetValue and OnNewGetValue you provide values for each of the different columns of the current row
OnNext, OnPrior you move your current row one next or one prior position.
As you see, the row/column concept (a DataSet) is used to provide the data to the report, but you can pull your data from any structure you use to store the result of your calculations (lists, arrays, or any other object/structure/file etc.)
Inside the report, you link the band to this logical DataSet and you use standard components to print the column values of this DataSet.
If you already have the data in a DataSet, for example a in-memory DataSet after your calculations, better use a TfrxDBDataset to directly bind your report to that source of data.
you can use TfrxUserDataSet.There is a demo named 'printstringlist' under folder 'demos'.
In our project we have implemented our own class inherited from TfrxCustomQuery. This new query class simply redirects its SQL statements to our application' internal query engine. We registered this new class in FastReport palette (used frxDsgnIntf.frxObjects.RegisterObject* for FR version 3 and 4) and now it is used in all our report templates instead of TfrxADOQuery or other built-in dataset classes.
Here is another alternative:
I've been using FastReport for years. I sometimes run into a similar situation. For offline tabular reports I use an in-memory dataset. I purchased DevExpress long time ago and so I have TdxMemData. But even without it, you should be happy using the TClientDataset component.
Besides that, the TfrxUserDataset is an alternative which I use when showing lists of objects.
There is a possibility to do it like this, it is slow though ,
Code:-
var
FRX: TfrxReport;
procedure NewPage;
begin
MyPage := TfrxReportPage.Create(FRX);
MyPage.CreateUniqueName;
MyPage.PaperSize := DMPAPER_A4;
end;
procedure ...(AText: string);
var
frMemo : TfrxMemoView;
begin
frMemo := TfrxMemoView.Create(MyPage);
frMemo.CreateUniqueName;
frMemo.Text := AText;
end;
Regards
Herman

Concurrency control

Hello
I would like to know the best way to implement concurrency control in 3 tier application?
May first thought is:
A client wants to edit a record from a dataset.
send a request to the server asking a lock on that record
the server accepts/denies the edit request based on lock table
Based on this scenario the locks should have a reference to both the record locked and client using that record.
The client has to send periodic keep alive messages to the server. The keep alive is used to free locked records in case we lost the client in meddle of editing operation.
I will be using Delphi with datasnap. Maybe this is a novice question but I have to ask!!
I'm building on jachguate's Optimistic Concurrency Control answer to answer a question posed in comments.
I prefer to use OCC wherever I can because the implementation is easier. I'm going to talk about a three tier app using an object persistence framework. There are three levels to my prferred scheme:
row or object level control, where a unique version ID is stored on each object. If you try to update the object the version id is automatically changed. If your version id doesn't match what's already there your update fails.
field or column level locking. You send a complete copy of the original object as well as the updated one. Each field in your update has the actual and old values compared before the new value is applied. It's possible to ask the user to resolve the conflicts rather than discarding them, but this becomes messy as the amount of data in the commit increases.
pessimistic locking. Each object has a lock owner which is usually null (the object is not locked). When you want to edit the object you first lock it. The problem here is that locks need to be tidied up and the business rules around that can be ugly (what timeout is desirable).
The advantage of this is that most of the time the low-cost OCC path is taken. For things that happen a lot but with low contention the benefits are significant. Think of product tracking in a warehouse - products move all the time, but very rarely do identical items move at the same time, and when they do resolving is easy (quantity left = original less my removal and your removal). For the complex case where (say) a product is relocated it probably makes sense to lock the product while it's in transit (because that mirrors the physical situation).
When you do have to fall back to locking, it's often useful to be able to notify both users and have a communication channel. At least notify the user who wants the lock when it's available, preferably allow them to send a message to the lock holder and possibly even allow them to force the lock. Then notify the lock loser that "Jo Smith has taken you lock, you lose your changes". Let office politics sort that one out :)
I usually drive the fallback process by user complaints rather than bug reports. If users are complaining that they lose their edits too often in a particular process, change it. If users complain that records are locked too often, you will have to refactor your object mappings to increase lock granularity or make business process changes.
I Design my applications with a Optimistic concurrency control in mind, by no locking any record when a user want to edit it nor trying to control concurrency.
Important calculations and updates are done server side (Application or database) after proper built-in database locking functionality is set while processing the updates applied by the client. DataSnap automatic transaction rollback prevent these locks to block other concurrent users in case of failure.
With DataSnap, you have total control to prevent data loss when two users edits collide by appropriate using the ProviderFlags for your fields. Set the pfInWhere for any field you want to check automatically to have the same value at edition/deletion time as when the record was read.
Additionally, when a collision occurs you can react programatically at the Application Server (provider OnUpdateError event), at the client (TClientDataSet OnReconcileError event) or even ask the user for proper conflict resolution (take a look at the ReconcileErrorDialog in the New Item repository).
In the meantime, IMHO avoiding the complexity required to maintain lock lists, client lists, locks-per-client-lists, keep-alive messages, robust application server failure recovery and all possible glitches you'll end with a cleaner and better solution.
The approach given by jachgate is great, and probably better, but in case you do want to implement this, you will need a TThreadList on the server that is created when the service is started. Use the TThreadList because it's thread-safe. You can have on TThreadList per table so that you can minimize the performance hit of navigating the lists.
To control what's locked, you'll need an object that's created and passed to the list
TLockedItem = class(TObject)
public
iPK: Integer;
iClientID: Integer;
end;
To do the actual locking, you'd need something like this:
function LockItem(pPK, pClientID: Integer): Boolean;
var
oLockedItem: TLockedItem;
oInternalList: TList;
iCont: Integer;
bExists: Boolean;
begin
bExists := False;
if (Assigned(oLockedList)) then
begin
oInternalList := oLockedList.LockList;
try
if (oInternalList.Count > 0) then
begin
iCont := 0;
while ((not bExists) and (iCont < oInternalList.Count)) do
begin
oLockedItem := TLockedItem(oInternalList[iCont]);
if (oLockedItem.iPK = pPk) then
bExists := True
else
Inc(iCont);
end;
end;
finally
oLockedList.UnlockList;
end;
if (not bExists) then
begin
oLockedItem := TLockedItem.Create;
oLockedItem.iPK := pPK;
oLockedItem.iClientID := pClientID;
oInternalList := oLockedList.LockList;
try
oInternalList.Add(oLockedItem);
finally
oLockedList.UnlockList;
end;
end;
end;
Result := bExists;
end;
That's just an ideia of what you'd need. You would have to do an unlock method with similar logic. You'd probably need a list for the clients, that would keep a point of each TLockItem held by each client, in case of lost connection. This is not a definitive answer, just a push on the direction, in case you want to implement this approach.
Good luck

Resources