I still confused how to handle database violation error. Is this code give best possible error handling? Or any other way to handle error, which also make user clear about why error happen?
procedure TForm2.cxButton1Click(Sender: TObject);
var
sp:TADOStoredProc;
errorMsg : string;
begin
//
sp := TADOStoredProc.Create(nil);
try
sp.Connection := FOrm1.ADOConnection1;
sp.ProcedureName := 'cfg.AddConfiguration';
sp.Parameters.CreateParameter('#RETURN_VALUE', TFieldType.ftInteger, pdReturnValue, 0, 0);
sp.Parameters.CreateParameter('#key', ftString, pdInput, 50, cxTextEdit1.Text);
sp.Parameters.CreateParameter('#caption', ftString, pdInput, 50, cxTextEdit2.Text);
sp.Parameters.CreateParameter('#datatype', ftString, pdInput, 50, cxComboBox1.Text);
sp.Parameters.CreateParameter('#description', ftString, pdInput, 4000, cxMemo1.Text);
try
sp.ExecProc;
except
on e:EOleException do
begin
errorMsg := 'Failed to add new configuration.';
if e.ErrorCode = 2601 then
begin
errorMsg := errorMsg + sLineBreak + 'Possible duplicate in key!';
end;
MessageDlg(errorMsg, mtError, [mbOK], 0);
end;
end;
finally
sp.Free;
end;
end;
The code that you have is very database-dependent. Every RDBMS reports those constraint violations differently. If you only use particular database (judging by the usage of ADO objects) then yes, errorCode can be inspected, however I would have encapsulated these RDBMS-specific errorCode checks in the stored procedure itself and then returned my app-specific error-reason code ans showed user a message thus shielding Delphi layer from ugly db exceptions altogether.
I recommend to move the procedure call and error processing away from the user interface (form "Form2") to a separate method in the data module. I would also make sure that no message boxes / dialogs will be shown from within this method, to support usage in server-side (middle-tier, application server etc.) or non GUI type apps too.
Then I would use either a return code or application specific exceptions to report errors to the calling GUI method.
Related
I have a data module with a global TADOConnection (with the default KeepConnection set the true).
There are numerous datasets and queries in my existing application that use this global TADOConnection.
I was wondering if there is some smart way to resume/retry the ado connection in case of a short network disconnection? (this situation happens sometimes with clients who have a not so stable connections).
Its easy to reproduce what I need. simply open TADOConnection on start-up. open some TADODataSet, and then disable and enable your "Local Area Connection". if you try to refresh the dataset, an EOleException exception is raised
"Connection failure"
or
"[DBNETLIB][ConnectionWrite (send()).]General network error. Check
your network documentation"
If I restart the application all is good.
No events are fired by the TADOConnection at the time of network disconnections. and TADOConnection.Connectedremains true
of course I could use a try/catch for every TDataSet.Open or Execute but I'm looking for some "centralized" solution for my large application.
so in case of "Connection failure" I could know which dataset is trying to open, and retry.
No never firing at the time of network disconnections. But you can check connection before every command. So if AdoConnection disconnected, you can reconnect and execute your command after this.
If you wanna centralized solution and you have 1 Connection, you can do that like this;
Const
ConnectionTestString=' '; //Yes, it's work on Sql Server. If doesnt your db, you can use 'Select 1'
Procedures;
Procedure TDM.GetCommandResult_Conn(CText:String;Connection : TAdoConnection);
var Ado_Ds_Tmp:TAdoCommand;
Begin
Ado_Ds_Tmp:=TAdoCommand.Create(self);
try
Ado_Ds_Tmp.Connection:=Connection;
Ado_Ds_Tmp.ParamCheck := False;
Ado_Ds_Tmp.CommandText:=CText;
try
Ado_Ds_Tmp.Execute;
except
DM.RaiseExceptionCreate('Error ! Command, ('+StrToList(CText, ' ')[0]+')');
end;
finally
Ado_Ds_Tmp.Destroy;
end;
end;
procedure TDM.ADOConnection1WillExecute(Connection: TADOConnection;
var CommandText: WideString; var CursorType: TCursorType;
var LockType: TADOLockType; var CommandType: TCommandType;
var ExecuteOptions: TExecuteOptions; var EventStatus: TEventStatus;
const Command: _Command; const Recordset: _Recordset);
var
ErrorLogFileName : string;
ErrorFile : TextFile;
ErrorData : string;
Msg : String;
begin
try
if (CommandText<>ConnectionTestString) then begin
DM.GetCommandResult_Conn(ConnectionTestString, Connection);
end;
except
try
try
Connection.Connected := False;
except
end;
try
Connection.ConnectionString := AdoConnectionString;
Connection.Mode:=cmShareDenyNone;
finally
try
Connection.Connected := True;
// If you wanna log for frequency
ErrorLogFileName := ChangeFileExt(Application.ExeName,'.error.log');
AssignFile(ErrorFile, ErrorLogFileName);
if FileExists(ErrorLogFileName) then
Append(ErrorFile)
else
Rewrite(ErrorFile);
try
ErrorData := Format('%s : %s : %s (%s / %s)',[DateTimeToStr(Now), 'Disconnected but we reconnect.', '', 'UserName : '+DBUser, 'Client : '+GetComputerNetName]);
WriteLn(ErrorFile,ErrorData);
finally
CloseFile(ErrorFile)
end;
except
DM.RaiseExceptionCreate('ReConnection Failed!');
end;
end;
except
end;
end;
end;
Any question?
The idea could be to catch the connection error then manage it with retries.
The Suggestion:
This function returns the exception descripion
function GetStrException(ExceptObject: TObject; ExceptAddr: Pointer):String;
var
Buffer: array[0..1023] of Char;
begin
ExceptionErrorMessage(ExceptObject, ExceptAddr, Buffer, SizeOf(Buffer));
Result:=Buffer;
end;
this is a simple idea to test!
procedure TForm1.Button2Click(Sender: TObject);
var
s,error:String;
begin
//a select as an example
S := 'SELECT COUNT(*) FROM MyTable';
TRY
WITH ADOQuery1 DO BEGIN SQL.Clear; SQL.Add(s);OPEN;END;
Memo1.Lines.ADD(ADOQuery1.Fields[0].AsString);
EXCEPT
error:=(GetStrException(ExceptObject,ExceptAddr));
//using MySql my error case is when the connection is lost, so the error is "Server has gone away"
if pos(error,'has gone away')>0 then
begin
Showmessage('Connection Error, please try again');
try
sleep(1000)
AdoConnection1.close;
AdoConnection1.open;
except
Showmessage('The connection Error persists, please contact the support');
end;
end;
END;
end;
A good solution is centralize the insert/update and select procedure, then
catch the error and try to adjust the situation one or two times before
show message to the user
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;
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.
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.
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;