Reversing the Master-Detail connection in Delphi - database

I have 2 tables which are connected using master-detail connection. I need the connection reversed on creation of Form2 - so the master table becomes the detail table, and the detail table becomes the master table.
I tried doing this, and the program compiles, but doesn't work the way I want it to (the previous connection breaks, but it's not reversed, so the program kinda works like the tables aren't connected at all):
Form1.ADOTableDetail.MasterSource.Destroy;
Form1.ADOTableMaster.MasterSource := Form1.DataSourceDetail;
Form1.ADOTableMaster.MasterFields := 'the_field_that_connects_them';
Any ideas on how I might achieve this?

Don't destroy the MasterSource!
In order to break the relationship do
Form1.ADOTableDetail.MasterSource:= nil;
Form1.ADOTableDetail.MasterFields:= '';
than use this to reroute the MasterDetail
Form1.ADOTableMaster.MasterSource := Form1.DataSourceDetail;
Form1.ADOTableMaster.MasterFields := 'the_field_that_connects_them';
Also never call .Destroy directly, use .Free instead.
Free does an extra check to see if the reference you are Freeing is not nil, preventing some Access Violations.

procedure TForm1.ExchangeMasterDetail;
begin
ADOTableDetail.Close;
ADOTableMaster.Close;
ADOTableMaster.MasterFields := ADOTableDetail.IndexFieldNames;
ADOTableMaster.IndexFieldNames := ADOTableDetail.MasterFields;
ADOTableDetail.IndexFieldNames := '';
ADOTableDetail.MasterFields := '';
ADOTableDetail.MasterSource := nil;
ADOTableMaster.MasterSource := DataSourceDetail;
ADOTableDetail.Open;
ADOTableMaster.Open;
end;

Just set the Master table Active propriety to false.
Then do what you wanna todo and set it back to true.

Related

ADOQuery.Locate Slow, Create Index

I have following line to Locate a row in a Query .
if Query.Locate('Line;Hour;Minute',VarArrayOf([Line-400,AHour,minuteof(Start)]),[]) = true then
This is slow, now I remember that one can add Indexes to the Query so that the Locate itself is multiple times faster. Unfortunately I cannot seem to find the example.
Can someone please help me out?
Regards
Robert
Interesting q.
Update please see update below.
I set up some test data on my SS2014 Sql Server to run some tests using code like this:
ID := 1;
for Line := 1 to 1000 do begin
for AHour := 1 to 24 do begin
for AMinute := 1 to 60 do begin
AdoQuery1.InsertRecord([ID, Line, AHour, AMinute]);
Inc(ID);
end;
end;
end;
end;
Then, I ran some tests like this one
procedure TForm1.LocateTest1(DisableControls, UseSort : Boolean);
var
T1 : Integer;
Line,
AHour,
AMinute : Integer;
begin
AdoQuery1.Sql.Text := 'select * from linetest order by line, ahour, aminute';
AdoQuery1.CursorLocation := clUseClient;
AdoQuery1.Open;
T1 := GettickCount;
if DisableControls then
AdoQuery1.DisableControls;
if UseSort then
AdoQuery1.Recordset.Sort := 'Line,AHour,AMinute';
Line := 1000;
AHour := 23;
for AMinute := 60 downto 1 do begin
if not AdoQuery1.Locate('Line;AHour;AMinute', VarArrayOf([Line, AHour, AMinute]), []) then
Caption := Format('Locate failed %d %d %d', [Line, AHour, AMinute]);
end;
Memo1.Lines.Add('Test1 : ' + IntToStr(GetTickCount - T1));
if DisableControls then
AdoQuery1.EnableControls;
AdoQuery1.Close;
end;
The reason for involving Disable/EnableControls was because of the results I reported
here Why does scrolling through ADOTable get slower and slower?
, that calling DisableControls has a huge impact on scrolling speed even if there are no db-aware
controls involved.
However, it seems that scrolling does not make a major impact on executing Locate() on a TAdoQuery, because calling DisableControls only took about 1.5 seconds of the recorded time of around
26 seconds. Obviously, TAdoQuery.Locate doesn't perform at all well with a large number of rows.
The idea of the UseSort parameter was to see if sorting the RecordSet behind the
AdoQuery made any difference to the speed, but it didn't, the reason being that Locate calls
TCustomAdoDataSet.LocateRecord which uses Sort anyway.
You mentioned adding indexes. Unfortunately, TAdoQuery only supports use of server-side indexes
in executing thq SQL query, not locating records in the retrieved result set. You can add
client-side indexes to TAdoTable, but according to a test similar to the one above, rather to my surprise,
they make
virtually no difference to the speed of Locate().
So, given my results so far, it would seem very possibly quicker to use a parameterised
SELECT to retrieve only the row currently of interest, rather than trying to Locate it
in a large tesult set. Alterantaivel, you could retrieve the result set into a ClientDataSer via a DatasetProvider or into a FireDAC FDMemTable, etc. Ymmv, it depends on what you're doing exactly ...
Update Since posting my original answer, I have a couple of further updates
that it might be useful to include.
One concerns a way of mimicking Locate using calls to the AdoQuery's RecordSet's Find and Filter methods that is significantly faster (around 15 secs) than doing AdoQuery1.Locate repeatedly. I am still probling this and will post another update in a day or two.
The other is to briefly mention doing the Locates doing a FireDAC FDQuery instead of an AdoQuery. This seems to do the same set of Locates as takes around 25 seconds with the AdoQuery in under 9 seconds, using the following code:
Using FDQuery.Locate
procedure TForm2.LocateTest;
var
T1 : Integer;
Line,
AHour,
AMinute : Integer;
begin
FDQuery1.Sql.Text := 'select * from linetest order by line, ahour desc, aminute desc';
//FDQuery1.CursorLocation := clUseClient;
FDQuery1.CursorKind := ckForwardOnly;
FDQuery1.Open;
T1 := GettickCount;
Line := 1000;
AHour := 1;
for AMinute := 1 to 60 do begin
if not FDQuery1.Locate('Line;AHour;AMinute', VarArrayOf([Line, AHour, AMinute]), []) then
Caption := Format('Locate failed %d %d %d', [Line, AHour, AMinute]);
end;
Memo1.Lines.Add('Test1 : ' + IntToStr(GetTickCount - T1));
FDQuery1.Close;
end;
The documentation says that Locate move the cursor to the first row matching a specific search criteria.
If your table have many records, Locate is slow.
Locate is usually used in local databases, but in client-server RDBMs is better thar you use a SQL with WHERE for minimize the search time and minimize the data traffic.

delphi DBGrid cell not empty

In my form, i have a TDBGRid, TDatasource , MessageTable and 2 button. I have a button to add new row in my DBGRID :
MessageTable.Append;
MessageTable.Edit;
MessageTable.FieldByName('FieldName').AsString := sName;
MessageTable.Post;
The second button is used to delete a current row :
MessageTable.Edit ;
MessageTable.Delete ;
How can ensure all Cell not empty before the post?
If there is an empty Cell, i need to ignore this row !
how can I do that?
You should use the features the dataset (in this case, TTable) already give you instead of trying to reinvent the wheel. TDataSet provides an event (OnBeforePost) that is specifically designed for this purpose.
Click on your TTable, and then switch to the Events tab in the Object Inspector. Find the OnBeforePost event and double click it to generate the event shell in the Code Editor. You'll see something like this:
procedure TForm1.Table1BeforePost(DataSet: TDataSet);
begin
// DataSet is the TDataSet (TTable, TQuery, TADOQuery, etc.) to which
// event is attached
end;
You can do all of your validations needed before a record is actually written to the database here. For instance, if you want to make sure every single field has something in it, you can do this:
procedure TForm1.Table1BeforePost(DataSet: TDataSet);
var
i: Integer;
begin
// You can replace DataSet with your actual table variable name, but using it
// this way allows you to use this same event for more than one table if you want.
for i := 0 to DataSet.FieldCount - 1 do
if DataSet.Fields[i].IsNull then
raise Exception.CreateFmt('Field %s has no value', DataSet.Fields[i].FieldName);
end;
If you want to make sure only certain fields have values, or that the value is within a certain range, you can access the field directly:
procedure TForm1.Table1BeforePost(DataSet: TDataSet);
begin
if DataSet.FieldByName('MyField').IsNull then
Abort; // This silently cancels the post without telling the user
if DataSet.FieldByName('AField').AsInteger = 0 then
raise Exception.Create('AField must not be 0');
end;
Now you don't have to do anything at all in your TDBGrid. If the user hits DownArrow on the last row and a new row is inserted, and they enter incomplete or wrong data, the events above will take care of it. They'll also work if you use two buttons (one to insert or edit and the other to post), because the events will handle everything else.
procedure TForm1.ButtonInsertClick(Sender: TObject);
begin
Table1.Insert; // Or Append - if you have an index on the table they're the same thing
end;
procedure TForm1.ButtonPostClick(Sender: TObject);
begin
Table1.Post; // This is 100% of the code you need here
end;

Delphi Database Connection Using ACCESS and ADO connections

Okay so basically I've been working on my computing project for a while now and I've got 90% of it working however I'm having a problem with Delphi where is says that my database is not connected/ there is a problem connecting however I've already tried writing the information to the screen and this showed me that the items I was looking to pick up where in fact being picked up so the failure is when the items are being input in to the database. This however shouldn't be happening as the System already has database information displayed from that table and the user can physically select things from the database tables within the program however when trying to store the information back into the database it just breaks. Me and my computing teacher can not work it out, any help would be appreciated.
The problem appears on the new orders page. If you'd rather look at the system then you can download it from here https://drive.google.com/folderview?id=0B_iRfwwM9QpHVXJnSkx4U1FjMlk&usp=sharing
procedure Tform1.btnSaveClick(Sender: TObject);
var orderID:integer;
count:integer;
begin
try
//save into the order table first
tblOrder.Open;
tblOrder.Insert;
tblOrder.FieldByName('CustomerID').value:= strtoint(cboCustomer.Text);
tblOrder.Close;
tblOrder.Open;
tblOrder.Last;
orderID:=tblOrder.FieldByName('OrderID').Value;
showmessage(inttostr(orderID));
for count := 1 to nextFree-1 do
begin
if itemOrdered[count,1]<>0 then
begin
tblOrderLine.Open;
tblOrderLine.AppendRecord([orderID, itemOrdered[count,1],itemOrdered[count,2]]);
end;
end;
showmessage('The order has been saved');
except
showmessage('There was a problem connecting to the database');
end;
end;
You're doing far too much open, do something, close, open. Don't do that, because it's almost certain that is the cause of your problem. If the data is already being displayed, the database is open already. If you want it to keep being displayed, the database has to remain open.
I also removed your try..except. You can put it back in if you'd like; I personally like to allow the exception to occur so that I can find out why the database operation failed from the exception message, rather than hide it and have no clue what caused it not to work.
procedure Tform1.btnSaveClick(Sender: TObject);
var
orderID: integer;
count: integer;
begin
//save into the order table first
tblOrder.Insert;
tblOrder.FieldByName('CustomerID').value:= strtoint(cboCustomer.Text);
tblOrder.Post;
orderID:=tblOrder.FieldByName('OrderID').Value;
showmessage(inttostr(orderID));
for count := 1 to nextFree-1 do
begin
if itemOrdered[count, 1] <> 0 then
begin
tblOrderLine.AppendRecord([orderID, itemOrdered[count,1],itemOrdered[count,2]]);
tblOrderLine.Post;
end;
end;
showmessage('The order has been saved');
end;

How to change Clientdataset field datatype at runtime

For Delphi ClientDataSets where fields have been define at design time, is there a way at runtime to change a specific field's datatype ( change the cds.Fields[n].DataType) ?
I have a legacy Delphi 7 program with both SQLDataSet and ClientDataSet fields set at design time (in order to override various properties).
These are connected to a 3rd-party Sybase SQL Anywhere 11 database.
Recently the vendor changed all 'Description' fields from VarChar(128) to long varchar, but only for certain of his customers. So, my code has to support both types of fields when I query on these 'Description' fields.
I was hoping to set conditional compilation on the class field types (then add the fields before opening the SQL/CLient Dataset), but the compiler ignores {$IFDEF } conditionals in the component definition section of the class (which, the more I think about it, makes good sense)!
There are dozens of modules, with hundreds of fields affected, so any insight is appreciated.
I have also faced this problem before, not with CDS, but with TADODataSet using persistent fields at design time. I think that the code below will help you get the idea of how to fix/patch your CDS datasets.
The idea is to query the relevant table schema; get the actual fileds data type; and "change" the persistent field type by un-attaching it from the DataSet and adding a new matching persistent filed instead:
// TData class
procedure TData.DataModuleCreate(Sender: TObject);
var
I: Integer;
begin
for I := 0 to ComponentCount - 1 do
if (Components[I] is TCustomADODataSet) then
DataSetPrepareMemoFields(TDataSet(Components[I]));
end;
procedure TData.DataSetPrepareMemoFields(DataSet: TDataSet);
var
Fld: TField;
I: Integer;
FldName, CompName: string;
AOwner: TComponent;
begin
// Here you need to query the actual table schema from the database
// e.g. ADOConnection.GetFieldNames and act accordingly
// check which DataSet you need to change
// if (DataSet = dsOrders) or ... then...
if DataSet.FieldList.Count > 0 then
for I := DataSet.FieldList.Count - 1 downto 0 do
begin
if DataSet.FieldList.Fields[I].ClassNameIs('TMemoField') and (DataSet.FieldList.Fields[I].FieldKind = fkData) then
begin
// save TMemoField properties
AOwner := DataSet.FieldList[I].Owner;
CompName := DataSet.FieldList[I].Name;
FldName := DataSet.FieldList.Fields[I].FieldName;
// dispose of TMemoField
DataSet.FieldList[I].DataSet := nil; // Un-Attach it from the DataSet
// create TWideADOMemoField instead
Fld := TWideADOMemoField.Create(AOwner); // Create new persistent Filed instead
Fld.Name := CompName + '_W';
Fld.FieldName := FldName;
Fld.DataSet := DataSet;
end;
end;
end;
That said, After I have fixed that issue, I have never ever used persistent fields again.
All my fields are generated in run time. including calculated/lookup/internal fields.

How to make ADO parameter to update SQL Server datetime column?

edit I should have mentioned that I'm trying to do this with Delphi 2006.
OK, I think I have hit on a question with no previous answers.
I have a SQL Server database with columns of type datetime. When I try to insert a row with a parametrized command, I get
Exception class EOleException with message
'[Microsoft][ODBC SQL Server Driver]Optional feature not implemented'.
My insert procedure looks like this:
procedure TForm1.btnZoomClick(Sender: TObject);
const
InsCmd = 'insert into dbo.foo (added, edited, editor, narrative) ' +
'values (:dateAdded, :dateEdited, :theEditor, :theNarrative);';
begin
dmDbToy2.DataModule2.ADOCommand1.CommandText := InsCmd;
with DataModule2.ADOCommand1.Parameters do
begin
// the following line was an attempt to trick VarAsType into making a
// adDbTimeStamp: VarAsType is having none of it.
// FindParam('dateAdded').Value := VarAsType(VarFromDateTime(Now), 135);
FindParam('dateAdded').Value := VarFromDateTime(Now);
FindParam('dateEdited').Value := Null;
FindParam('theEditor').Value := 'wades';
FindParam('theNarrative').Value := Null;
end;
DataModule2.ADOCommand1.Execute;
end;
I found some postings via google which seem to indicate that SQL Server wants a adDbTimeStamp type to update these columns, but VarAsType does not want to make one for me.
Is there a way to create a value for the dateAdded and dateEdited parameters in the code sample?
In the comments thread on the original question, user RRUZ made a suggestion that turned out to resolve the issue: The problem was with the provider. Namely, I was using the OLEDB Provider for ODBC rather than the OLEDB Provider for SQL Server. Changing the provider as suggested made the 'Optional feature not implemented' error message go away and enabled the insert to work with a simple assignment of TDateTime to TParameter.Value, thusly:
FindParam('dateAdded').Value := Now;
Set the datatype for the parameter, it might do a difference in how the parameters is treated. I would also recommend that you use ParamByName instead of FindParam. With ParamByName you get a Param xx not found exception if the parameters does not exist in the Parameters collection. FindParam returns nil if it is not found. I have never needed to use any variant conversion stuff when assigning parameters for a TADOCommand so think you should remove that as well.
Try this.
with ParamByName('dateAdded') do
begin
DataType := ftDateTime;
Value := Now;
end;
with ParamByName('dateEdited') do
begin
DataType := ftDateTime;
Value := Null;
end;
with ParamByName('theEditor') do
begin
DataType := ftString; // or ftWideString if you use nchar/nvarchar
Value := 'wades';
end;
with ParamByName('theNarrative') do
begin
//DataType := ftString // Don't know datatype here
Value := Null;
end;
Just set the parameter as a datetime. I do it all the time in ADO and other conection layers
DataModule2.ADOCommand1.Parameters.ParamByName('dateAdded').Value := Now();
//other code
DataModule2.ADOCommand1.Parameters.ParamByName('dateEdited').Value := Null;
//other code
DataModule2.AdoCommand1.Execute;

Resources