Duplicate key error when updating key field in a set of records - delphi

I have a master detail relation between two tables. Troq is the master table and Troq_Alma is the detail table. Troq_Alma primary key is formed by Troq primary key (Troq.cod) and "Num_Orden". This "Num_Orden" field is not only part of the PK of the table but is the priority of that specific Alma out of the several Almas for one Troq. (Troq_Alma is the table that relates the different Troq's with their related Alma's)
And at a certain point, the user needs to modify this priority therefore modify the primary key of a set of records (The set of almas related to one specific troq). In the form there are two buttons (Spinbuttons) related with one TDBGrid. If I push the "Up" button, selected Troq_Alma should decrease "Num_Orden" by 1 and the Troq_alma with that value in "Num_Orden" would increase its value by 1. So in the "image" we have of the table in the dbgrid, there is no duplicate key.
Naturally when I do Apply this updates: TFDQ_Troq_Alma.ApplyUpdates(-1);
I get duplicate key error which I find very logical as in any way that firedac would try to make this update, the first modification is going to throw duplicate key error until the amendment is made for that other record that previously had that primary key, being the fact that it still has that primary key.
I really don't know if there is any "fair" solution to this problem, the only thing that I imagined was to first add some amount to all "num_orden" for one specific Troq do the update and then update again to the originally modified values, which is a really odd job, but, on my short knowledge of delphi and firedac, I really don´t appear to find any other way to solve it.
Working on Delphi XE6 with cached Updates against Postgres 11.8 Database with firedac.
In case it could be of any interest, here is the code for both spin buttons (Up and Down):
procedure TFRM_Mant_TROQ.SpinBut_Troq_AlmaDownClick(Sender: TObject);
var iValActNumOrd, iValNumOrd2 : Integer;
RegActual: TBookMark;
iValMax: Integer;
begin
RegActual := DM_Mant_Troq.FDQ_Troq_Alma.GetBookmark;
iValMax := DM_DatosComun.MaxVal_FDQ(DM_Mant_Troq.FDQ_Troq_Alma, 'num_orden');
DM_Mant_Troq.FDQ_Troq_Alma.GotoBookmark(RegActual);
iValActNumOrd := DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value;
if iValActNumOrd < iValMax then begin
DM_Mant_Troq.FDQ_Troq_Alma.Next;
iValNumOrd2 := DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value;
DM_Mant_Troq.FDQ_Troq_Alma.Edit;
DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value := iValActNumOrd;
// DM_Mant_Troq.FDQ_Troq_Alma.Post;
DM_Mant_Troq.FDQ_Troq_Alma.GotoBookmark(RegActual);
DM_Mant_Troq.FDQ_Troq_Alma.Edit;
DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value := iValNumOrd2;
DM_Mant_Troq.FDQ_Troq_Alma.Post;
end;
end;
procedure TFRM_Mant_TROQ.SpinBut_Troq_AlmaUpClick(Sender: TObject);
var iValActNumOrd, iValNumOrd2 : Integer;
RegActual: TBookMark;
iValMin: Integer;
begin
RegActual := DM_Mant_Troq.FDQ_Troq_Alma.GetBookmark;
iValMin := DM_DatosComun.MinVal_FDQ(DM_Mant_Troq.FDQ_Troq_Alma, 'num_orden');
DM_Mant_Troq.FDQ_Troq_Alma.GotoBookmark(RegActual);
iValActNumOrd := DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value;
if iValActNumOrd > iValMin then begin
DM_Mant_Troq.FDQ_Troq_Alma.Prior;
iValNumOrd2 := DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value;
DM_Mant_Troq.FDQ_Troq_Alma.Edit;
DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value := iValActNumOrd;
DM_Mant_Troq.FDQ_Troq_Alma.GotoBookmark(RegActual);
DM_Mant_Troq.FDQ_Troq_Alma.Edit;
DM_Mant_Troq.FDQ_Troq_Alma.FieldByName('NUM_ORDEN').Value := iValNumOrd2;
DM_Mant_Troq.FDQ_Troq_Alma.Post;
end;
end;
This code can probably be improved but as I mention, I believe this code is working alright, the matter on my point of view is on how to indicate firedac to "eventually avoid" this unique key validation.
Actually Troq.Cod, Num_Orden is not the primary key of the table Troq_Alma, but there is a unique index on this two fields and I really like this unique index for database integrity purposes.

If we accept that you have a good reason to change a unique key then you need to ensure that you avoid a key violation. It looks like you are working with data cached locally in a TFDQuery and then applying the updates.
The problem with that approach is that the TFDQuery remembers the loaded value of the field and the current value, so your intermediate changes are lost, resulting in a key violation.
A quick way to avoid that problem is to ApplyUpdates after every change. This could be a problem for you if you are remote from the main store.
If you need to assign a temporary value to an indexed field, you need to ensure that that value is unique. If the data schema allows it you could just negate the value of the Integer field (if it's not defined as unsigned). Although there could be another record with that value, if your convention is to only allocate these values temporarily you should avoid a key conflict.

Related

Can one create an MS Access table in Delphi (e.g., FireDAC) from the field defs of an existing table without using SQL?

I wanted to create a new mdb file containing tables based on the structure of previously existing tables. I knew that I can use newTable.FieldDefs.Add() to recreate the fields one by one in a loop. But since there is already an oldTable fully stocked with the correct FieldDefs, that seemed terribly inelegant. I was looking for a single-statement solution!
I had found that newTable.FieldDefs.Assign(oldTable.FieldDefs) would compile (and run) without error but it left newTable with zero defined fields. This caused me to erroneously conclude that I didn't understand that statement's function. (Later I found that it failed only when oldTable.open had not occurred, which could not happen when the database was not available, even though the FieldDefs had been made Persistent and were clearly visible in the Object Inspector)
Here is my original code after some sleuthing:
procedure TForm2.Button1Click(Sender: TObject);
var
fname: string;
Table: TFDTable;
FDConn: TFDConnection;
begin
fname := 'C:\ProgramData\mymdb.mdb';
if FileExists(fname) then DeleteFile(fname);
{ Make new file for Table }
FDMSAccessService1.Database := fname;
FDMSAccessService1.DBVersion := avAccess2000;
FDMSAccessService1.CreateDB;
{ Connect to new file }
FDConn := TFDConnection.Create(nil);
FDConn.Params.Database := fname;
FDConn.Params.DriverID := 'MSAcc';
FDConn.Connected := true;
{ Set up new Table using old table's structure }
Table := TFDTable.Create(nil);
try
{ ADOTable1 has been linked to an existing table in a prior
database with Field Defs made Persistent using the Fields
Editor in the Object Inspector. That database will not be
available in my actual use scenario }
try
ADOTable1.open; // Throws exception when database file not found
except
end;
Table.Connection := FDConn;
{ specify table name }
Table.TableName := ADOTable1.TableName;
Table.FieldDefs.Assign(ADOTable1.FieldDefs); // No errors reported
ShowMessageFmt('New Table %s has %d fields',[Table.TableName,
Table.FieldDefs.Count]);
{ Reports correct TableName but "0 fields" with table not open
(i.e. file not found). Reports "23 fields" with table open }
{ Set Table definition into new mdb file }
Table.CreateTable(False); // Throws exception when 0 fields found
finally
Table.Free;
end;
end;
It turned out that the solution was to use a ClientDataSet originally linked to the same old database instead of the ADOTable. See the working solution below in my answer.
Edit: A final note. I had hoped to use this FireDAC approach, as indicated here, to get around the lack of a TADOTable.CreateTable method. Alas, although the "solutions" above and below do work to create a new TADOTable, that table's field definitions are not faithful replicas of the original table. There may be a combination of the myriad TFDTable options that would get around this, but I was not able to discover it so I reverted to creating my ADO tables with SQL.
Thanks to #Ken White's (unfortunately deleted) pointer, I now think that I have a solution to my original question about cloning the field defs from an old table into a newly created database. My original problem stemmed from the fact that the FieldDefs function for a table evidently does not return the actual stored field data if the table is not "open" (i.e., connected to the relevant database). Since my use scenario would not have a valid database available I could not "open" the table. However, ClientDataSets have an additional option to "StoreDefs" along with editor options to "Fetch Params" and "Assign Local Data". With those settings saved, the ClientDataSet renders its FieldDefs properties without being "open". Using that approach it seems that I can clone the stored field defs to a new table without needing a currently valid database to read them from. Thanks again, Ken, you saved a lot of my remaining hair! I sure wish that Embarcadero would do a better job of rationalizing their help files. They removed BDE from the default installation of Rio while still pointing in their help file discussion on creating Access tables to its TTable type as the way to create new tables and then never point to the equivalent capabilities in FireDAC (or elsewhere) which they continue to support. I wasted a lot of time because of this "oversight"!
Here is my working code after Ken's tip:
procedure TForm1.Button1Click(Sender: TObject);
var
i: integer;
fname: string;
Table: TFDTable;
FDConn: TFDConnection;
begin
fname := 'C:\ProgramData\mymdb.mdb';
if FileExists(fname) then DeleteFile(fname);
FDMSAccessService1.Database := fname;
FDMSAccessService1.DBVersion := avAccess2000;
FDMSAccessService1.CreateDB;
FDConn := TFDConnection.Create(nil);
FDConn.Params.Database := fname;
FDConn.Params.DriverID := 'MSAcc';
FDConn.Connected := true;
Table := TFDTable.Create(nil);
try
Table.Connection := FDConn;
{ specify table name }
Table.TableName := 'ATable';
{ The existingClientDataSet has been linked to a table in the
prior, no longer valid, database using StoreDefs, Fetch Params,
and Assign Local Data in the Object Inspector }
Table.FieldDefs.Assign(existingClientDataSet.FieldDefs);
ShowMessageFmt('New Table has %d fields', [Table.FieldDefs.Count]);
Table.CreateTable(False);
finally
Table.Free;
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.

Storing Component Name as a String for later use

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.

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