Using FireDac's onUpdateRecord and optionally execute default statements - delphi

I have a query that returns rows with an outer join. This causes records to exist in the results that don't really exist in the table. When those rows are changed FireDac sees the change as an Update instead of an insert. That behavior makes sense from FireDac's side because it has no way to tell the difference.
I am overriding the OnUpdateRecord event to catch those rows that are marked wrong and perform the insert myself. That part is working great. What I can't figure out is how to tell FireDac to perform it's normal process on other records. I thought I could set the AAction to eaDefault and on the return FireDac would continue to process the row as normal. However, that does not seem to be the case. Once OnUpdateRecord is in place it looks like FireDac never does any updates to the server.
Is there a way to tell FireDac to update the current row? Either in the OnUpdateRecord function by calling something - or maybe a different return value that I missed?
Otherwise is there a different way to change these updates into inserts? I looked at changing the UpdateStatus but that is read only. I looked at TFDUpdateSql - but I could not figure out a way how to only sometimes turn the update into an insert.
I am using CachedUpdates if that makes any difference.
Here is what I have for an OnUpdateRecord function:
procedure TMaintainUserAccountsData.QueryDrowssapRolesUpdateRecord(
ASender: TDataSet; ARequest: TFDUpdateRequest; var AAction: TFDErrorAction;
AOptions: TFDUpdateRowOptions);
{^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^}
begin
if (ARequest = arUpdate) and VarIsNull(ASender.FieldByName('Username').OldValue) then
begin
ASender.Edit;
ASender.FieldByName('RoleTypeID').Value := ASender.FieldByName('RealRoleTypeID').Value;
ASender.Post;
MGRDataAccess.ExecSQL('INSERT INTO DrowssapRoles (Username, RoleTypeID, HasRole) VALUES (:Username, :RoleTypeID, :HasRole)',
[ASender.FieldByName('Username').AsString, ASender.FieldByName('RoleTypeID').AsInteger,
ASender.FieldByName('HasRole').AsBoolean]);
AAction := eaApplied;
end
else
begin
// What do I do here to get the default FireDac actions?
end;
end;

Related

Making TJvAppDBStorage work with FireDAC/Firebird

Using either Delphi 10.2 or 10.3, FireDAC and either Firebird 2.5 or 3.0: I've used the JVCL TJvAppStorage components for years and never had a problem with them, either to INI/XML storage or to a table in an AbsoluteDB database. I'm trying to migrate an app from AbsoluteDB to Firebird via FireDAC, and can't get the TJvAppDBStorage to write entries - it returns no errors, but nothing actually gets written into the table.
I have a datamodule containing the FireDAC connection and driver components, the JvAppDBStorage, a TDataSource and a TFDTable component. The FDB exists containing an appropriate table, the TFDTable is open on that table, the JvAppDBStorage has its properties set to match the table's fields, and the TFDTable, datasource and JvAppDBStorage are properly linked. (This all mirrors what has existed and worked against AbsoluteDB.) A call to dmStorage.FBStorage.WriteString(dmStorage.FBStorage.ConcatPaths(['General', 'LastStarted']), FormatDateTime(StdDTFmtStr, Now)); does not throw any exceptions, but nothing actually gets written into the table. Doing a normal append/set fields/post construct via the TFDTable works properly.
Any help appreciated!
Steve
There are 3 critical parts to this:
The underlying table must have a PK. I have an ID column with an associated sequence with before-insert trigger to make an auto-inc column for my PK, and an index that's a merge of the SectionID and KeyID fields to speed up the Locate() calls.
When creating the TFDTable object, you must create a new TFDTransaction object and attach it to the TFDTable and the TFDConnection.
On the TFDTable, you must set UpdateOptions.UpdateMode to upWhereKeyOnly. If you only do the first two steps, reads and appends will work but edits will not.
function TFDDBMgr.MakeTableObj(const ATblName: String): TFDTable;
begin
Result := TFDTable.Create(Self);
with Result do begin
//hook up all our needed stuff
Connection := Self.Connection;
TableName := ATblName;
CachedUpdates := False;
{...}
//!!.SS 09/11/19: REQUIRED for TJvAppDBStorage use,
//also requires that a PK be defined on the table
UpdateOptions.UpdateMode := upWhereKeyOnly;
Transaction := TFDTransaction.Create(Result);
Transaction.Connection := Self.Connection;
end;
end;

Delphi DBGrid date format for Firebird timestamp field

I display the content of a Firebird database into a TDBgrid. The database has a 'TIMESTAMP' data type field that I would like to display with date/time format:
'YYYY/MM/DD HH:mm:ss'. (Now its displayed as 'YYMMDD HHmmss')
How to achieve this?
I tried this:
procedure TDataModule1.IBQuery1AfterOpen(DataSet: TDataSet);
begin
TDateTimeField(IBQuery1.FieldByName('timestamp_')).DisplayFormat := 'YYYY/MM/DD HH:mm:ss';
end;
But this causes some side effects at other parts of the program, so its not an alternative. For example at the 'IBQuery1.Open' statement I get the '...timestamp_ not found...' debugger message in the method that I clear the database with.
function TfrmLogger.db_events_clearall: integer;
begin
result := -1;
try
with datamodule1.IBQuery1 do begin
Close;
With SQL do begin
Clear;
Add('DELETE FROM MEVENTS')
end;
if not Prepared then
Prepare;
Open; //Exception here
Close;
Result := 1;
end;
except
on E: Exception do begin
ShowMessage(E.ClassName);
ShowMessage(E.Message);
Datamodule1.IBQuery1.close;
end;
end;
end;
I get the same exception message when trying to open the query for writing into the database.
*EDIT >>
I have modified the database clear as the following:
function TfrmLogger.db_events_clearall: integer;
var
IBQuery: TIBQuery;
IBTransaction: TIBTransaction;
DataSource: TDataSource;
begin
result := -1;
//Implicit local db objects creation
IBQuery := TIBQuery.Create(nil);
IBQuery.Database := datamodule1.IBdbCLEVENTS;
DataSource := TDataSource.Create(nil);
DataSource.DataSet := IBQuery;
IBTransaction := TIBTransaction.Create(nil);
IBTransaction.DefaultDatabase := datamodule1.IBdbCLEVENTS;
IBQuery.Transaction := IBTransaction;
try
with IBQuery do begin
SQL.Text := DELETE FROM MSTEVENTS;
ExecSQL;
IBTransaction.Commit;
result := 1;
end;
except
on E : Exception do
begin
ShowMessage(E.ClassName + ^M^J + E.Message);
IBTransaction.Rollback;
end;
end;
freeandnil(IBQuery);
freeandnil(DataSource);
freeandnil(IBTransaction);
end;
After clearing the database yet i can load the records into the dbgrid, seems like the database has not been updated. After the program restart i can see all the records been deleted.
The whole function TfrmLogger.db_events_clearall seems very dubious.
You do not provide SQL_DELETE_ROW but by the answer this does not seem to be SELECT-request returning the "resultset". So most probably it should NOT be run by ".Open" but instead by ".Execute" or ".ExecSQL" or something like that.
UPD. it was added SQL_DELETE_ROW = 'DELETE FROM MEVENTS'; confirming my prior and further expectations. Almost. The constant name suggests you want to delete ONE ROW, and the query text says you delete ALL ROWS, which is correct I wonder?..
Additionally, since there is no "resultset" - there is nothing to .Close after .Exec.... - but you may check the .RowsAffected if there is such a property in DBX, to see how many rows were actually scheduled to be deleted.
Additionally, no, this function DOES NOT delete rows, it only schedules them to be deleted. When dealing with SQL you do have to invest time and effort into learning about TRANSACTIONS, otherwise you would soon get drown in side-effects.
In particular, here you have to COMMIT the deleting transaction. For that you either have to explicitly create, start and bind to the IBQuery a transaction, or to find out which transaction was implicitly used by IBQuery1 and .Commit; it. And .Rollback it on exceptions.
Yes, boring, and all that. And you may hope for IBX to be smart-enough to do commits for you once in a while. But without isolating data changes by transactions you would be bound to hardly reproducible "side effects" coming from all kinds of "race conditions".
Example
FieldDefs.Clear; // frankly, I do not quite recall if IBX has those, but probably it does.
Fields.Clear; // forget the customizations to the fields, and the fields as well
Open; // Make no Exception here
Close;
Halt; // << insert this line
Result := 1;
Try this, and I bet your table would not get cleared despite the query was "opened" and "closed" without error.
The whole With SQL do begin monster can be replaced with the one-liner SQL.Text := SQL_DELETE_ROW;. Learn what TStrings class is in Delphi - it is used in very many places of Delphi libraries so it would save you much time to know this class services and features.
There is no point to Prepare a one-time query, that you execute and forget. Preparation is done to the queries where you DO NOT CHANGE the SQL.Text but only change PARAMETERS and then re-open the query with THE SAME TEXT but different values.
Okay, sometimes I do use(misuse?) explicit preparation to make sure the library fetches parameters datatypes from the server. But in your example there is neither. Your code however does not use parameters and you do not use many opens with the same neverchanging SQL.text. Thus, it becomes a noise, making longer to type and harder to read.
Try ShowMessage(E.ClassName + ^M^J + E.Message) or just Application.ShowException(E) - no point to make TWO stopping modal windows instead of one.
Datamodule1.IBQuery1.close; - this is actually a place for rolling back the transaction, rather than merely closing queries, which were not open anyway.
Now, the very idea to make TWO (or more?) SQL requests going throw ONE Delphi query object is questionable per se. You make customization to the query, such as fixing DisplayFormat or setting fields' event handlers, then that query is quite worth to be left persistently customized. You may even set DisplayFormat in design-time, why not.
There is little point in jockeying on one single TIBQuery object - have as many as you need. As of now you have to pervasively and accurately reason WHICH text is inside the IBQuery1 in every function of you program.
That again creates the potential for future side effects. Imagine you have some place where you do function1; function2; and later you would decide you need to swap them and do function2; function1;. Can you do it? But what if function2 changes the IBQuery1.SQL.Text and function1 is depending on the prior text? What then?
So, basically, sort your queries. There should be those queries that do live across the function calls, and then they better to have a dedicated query object and not get overused with different queries. And there should be "one time" queries that only are used inside one function and never outside it, like the SQL_DELETE_ROW - those queries you may overuse, if done with care. But still better remake those functions to make their queries as local variables, invisible to no one but themselves.
PS. Seems you've got stuck with IBX library, then I suggest you to take a look at this extension http://www.loginovprojects.ru/download.php?getfilename=uploads/other/ibxfbutils.zip
Among other things it provides for generic insert/delete functions, which would create and delete temporary query objects inside, so you would not have to think about it.
Transactions management is still on you to keep in mind and control.

Saving deleted records to another table

I am deleting records from one table (based on a condition) like :
procedure TForm3.AdvGlowButton1Click(Sender: TObject);
begin
if MessageDlg('Are you sure???' , mtConfirmation, [mbYes, mbNo], 0) = mrNo then
Abort else
Case cxRadioGroup1.ItemIndex of
0: begin
with Form1.ABSQuery1 do begin
Form1.ABSQuery1.Close;
Form1.ABSQuery1.SQL.Clear;
Form1.ABSQuery1.SQL.Text :='delete from LOG where status="YES" ';
Form1.ABSQuery1.ExecSQL;
Form1.ABSTable1.Refresh;
end;
end;
End;
end;
However,I want to save these deleted records in another table that I have created for the purpose (LOG_ARCHIVE) which is identical to the LOG table. So how do I save these deleted records over there ?
If you were using a database that supported it, you could use a BEFORE DELETE trigger. However, according to a search on the Absolute Database documentation, there's no support for CREATE TRIGGER and a search on triggers at the same site returns nothing about them either.
The lack of trigger support probably just leaves you with performing an INSERT into the other table first, before doing the DELETE from your LOG table. According to the documentation again, a query is able to be used as the source of data for an INSERT (see the second example on the linked page). This means you can do something like this:
ABSQuery1.SQL.Text := 'insert into LOG_ARCHIVE'#13 +
'(select * from LOG where status = ''Yes'')';
ABSQuery1.SQL.ExecSQL;
ABSQuery1.Close;
{
No need to use SQL.Clear here. Setting the SQL.Text replaces
what was there before with new text.
}
ABSQuery1.SQL.Text :='delete from LOG where status=''YES''';
ABSQuery1.ExecSQL;
You really should wrap this entire operation in a transaction (Delphi example here), so that in case something fails both the INSERT and DELETE can be undone. (For instance, if the INSERT works putting the rows in the LOG_ARCHIVE file, but the DELETE then fails for some reason, you have no way to remove the rows you inserted into the archive file.) A transaction can be started before you do the INSERT, rolled back if it (or the DELETE fails or committed if both of them succeed.

delphi Ado (mdb) update records

I´m trying to copy data from one master table and 2 more child tables. When I select one record in the master table I copy all the fields from that table for the other. (Table1 copy from ADOQuery the selected record)
procedure TForm1.copyButton7Click(Sender: TObject);
SQL.Clear;
SQL.Add('SELECT * from ADoquery');
SQL.Add('Where numeracao LIKE ''%'+NInterv.text);// locate record selected in Table1 NInterv.text)
Open;
// iniciate copy of record´s
begin
while not tableADoquery.Eof do
begin
Table1.Last;
Table1.Append;// how to append if necessary!!!!!!!!!!
Table1.Edit;
Table1.FieldByName('C').Value := ADoquery.FieldByName('C').Value;
Table1.FieldByName('client').Value := ADoquery.FieldByName('client').Value;
Table1.FieldByName('Cnpj_cpf').Value := ADoquery.FieldByName('Cnpj_cpf').Value;
table1.Post;
table2.next;///
end;
end;
//How can i update the TableChield,TableChield1 from TableChield_1 and TableChield_2 fields at the same time?
do the same for the child tables
TableChield <= TableChield_1
TableChield1 <= TableChield_2
thanks
The fields will all be updated at the same time. The actual update is performed when you call post (or not even then, it depends if the Batch Updates are on or off).
But please reconsider your logic. It would be far more efficient to use SQL statements (INSERT) in order to insert the data to the other table
SQL.Clear;
SQL.Add('INSERT INOT TABLE_1(C, client, Cnpj_cpf)');
SQL.Add('VALUES(:C, :client, :Cnpj_cpf)');
Then just fill the values in a loop.
SQL.Parameters.ParamByName('C').Value := ADoquery.FieldByName('C').Value;
SQL.Parameters.ParamByName('client').Value := ADoquery.FieldByName('client').Value;
SQL.Parameters.ParamByName('Cnpj_cpf').Value := ADoquery.FieldByName('Cnpj_cpf').Value;
SQL.ExecSQL;
You can also do the Updade - Insert pattern if the data can alredy be in the target table.
Like This:
if SQL.ExecSQL = 0 then
begin
// no records were update, do an insert
end;
And also the indication that you are copying data from table 1 to table 2 could be a sign of design flaw. But I can't say that for sure without knowing more. Anyway data duplication is never good.
I believe the asker was thinking about the data integrity, that means, ensure that only all the tables will updated or none...
The way I know to achieve this with security is executing all this updates (or inserts, a.s.o.) using SQL commands inside a transition.

ADO, Adonis, Update Criteria

On a form, I have a Quantum Grid and some db-aware editcomponents. When appending a new record in the grid, typing some editvalues both in the grid and the separate editcompoennts, I get an error:
EOleException: Row cannot be located for updating. Some values may have been chenged since it was last read
After some googling, I think changing the 'Update Criteria'-property from adCriteriaAllCols to adCriteriaKey may be the right solution. But how, and when, do I do that on a Adonis query?
if your dataset contains an autoincrement field or one or more fields have default values then this may be the problem. After calling Post fields change their value in the db but that may not be detected by your dataset
Although I don't use Adonis, I believe there is a possibility it use the same mechanism TClientDataset (CDS) uses to identify the fields that compose the primary key: the property TField.ProviderFlags.
I believe it could be a good place to start looking.
Actually ADO provides dynamic property to control Query Based Update (QBU) behavior
Most of code is covered in ADOInt.pas, the appropriate event is OnAfterOpen for recordset properties (any TADODataSet), and OnCreate for connection properties (TADOConnection).
I guess ‘Update Criteria’ is not the solution in this case, since it deals with WHERE clause to specify fields to be used for updates.
You may change 'Update Resync' as follows:
//After open a TCustomADODataSet
TCustomADODataSet(DataSet).Properties['Update Resync'].Value :=
adResyncAutoIncrement + adResyncUpdates + adResyncInserts;
if you have join table in your query :
TADOQuery cannot be edit and post data in sql database.
you can create other TADOQuery and select by primary key without any join table and edit and then post data in sql database.
I had a similar problem - Updating a row where all values were the same would result in the error you're getting. I added the flag "Option=2" to the end of my ado connection and this fixed the issue.
This is old. but my situation may happens for someone else. so I post this answer.
This Error happened for me too.
I was using Access database and using some TADOTable on the form.
the relation was master-detail and I connected all of the tables with the IDE Designer together.
my tables were tbl_Floor,tbl_FloorParts,tbl_Seat which tbl_Floor was master of tbl_FloorParts and tbl_FloorParts was master of tbl_Seat.
So for solving this error I did this trick.
procedure Tfrm_Main.UpdateTblFloor(...);
var
FID:Integer;
q:TADOQuery
begin
FID:=tbl_Floor.FieldByName('FID').AsInteger;
tbl_Floor.Close;
q:=TADOQuery.Create(nil);
try
q.Connection:=tbl_Floor.Connection;
q.SQL.Add('Update [Floor]');
q.SQL.Add(...);//Set Fields that needed to be updated
q.SQL.Add('where [FID]='+IntToStr(FID));
q.ExecSQL;
finally
q.free;
end;
tbl_Floor.Open;
tbl_Floor.Locate('FID',FId,[loPartialKey]);
end;
and I added these events for tbl_Floor,tbl_FloorParts
procedure Tfrm_Main.tbl_FloorAfterOpen(DataSet: TDataSet);
begin
tbl_FloorParts.Open;
end;
procedure Tfrm_Main.tbl_FloorBeforeClose(DataSet: TDataSet);
begin
tbl_FloorParts.Close;
end;
procedure Tfrm_Main.tbl_FloorPartsAfterOpen(DataSet: TDataSet);
begin
tbl_Seat.Open;
end;
procedure Tfrm_Main.tbl_FloorPartsBeforeClose(DataSet: TDataSet);
begin
tbl_Seat.Close;
end;

Resources