Occasionally our clients get an error when saving changes they made. The changes are saved with the remoteDataAdapter.Update(DataSet) method. You can find a logging from one of my clients in the attachment.
Log.2022-03-18.zip (2.9 KB)
Since version .1531 I got this error multiple times from multiple clients with a range of different datasets. All the CRUD operations are done with the automatic commands from DataAbstract (screenshot from one of the DataTables).
Is this a known problem? We never had this issue before. There’s only so much we can do, because all I do is fire the Update method on the remoteDataAdapter which exists for more then 12 years already.
Thanks for looking into it.
The exception you see
RemObjects.DataAbstract.DAException: An exception occurred on the server: No rows were affected by this update
means that the UPDATE command was executed, however its WHERE part was unable to locate the row to change.
This can happen due to a variety reasons, so a bit more information is required (if you need to keep some of this info private then please send a mail to support@ ):
- What data types are used for the key fields in failing tables? F.e. MS SQL can round DateTime ad Double values when they are written to the database, causing table row location issues.
- Which exactly DB server version is used?
- Which exactly Data Abstract version is used at server side?
- What is the server app platform (.NET Framework, .NET Core, .NET or Delphi)?
- Would it be possible to add logging code to inspect what exactly is being sent to the DB server?
- Al the key fields are autinc integers or bigints.
- SQL Server 2016
- Data Abstract version .1531
- .NET Framework 4.8
- If you can tell me what exactly to log, I can do that
I also get more support tickets from customers where select query’s just fail. How exactly would you log this serverside?
Thanks in advance.
This is a full source code of a DataService with error logging:
public class DataService : RemObjects.DataAbstract.Server.DataAbstractService
private StringBuilder _messageBuffer;
this._messageBuffer = new StringBuilder(2048);
this.BeforeExecutingGetDataReader += DataService_BeforeExecutingGetDataReader;
this.BeforeExecuteCommand += DataService_BeforeExecuteCommand;
this.ProcessError += DataService_ProcessError;
// Required for designer support
private void InitializeComponent()
dataStreamer = new RemObjects.DataAbstract.Bin2DataStreamer();
scriptProvider = new RemObjects.DataAbstract.Scripting.EcmaScriptProvider();
this.AcquireConnection = true;
this.ServiceDataStreamer = dataStreamer;
this.ServiceSchemaName = "WinFormsApplication1Dataset";
this.ScriptProvider = scriptProvider;
private void LogCommand(IDbCommand command)
if (command.Parameters.Count > 0)
foreach (IDataParameter parameter in command.Parameters)
private void DataService_BeforeExecuteCommand(RemObjects.DataAbstract.Server.DataAbstractService sender, RemObjects.DataAbstract.Server.DataAbstractServiceExecuteCommandEventArgs e)
private void DataService_BeforeExecutingGetDataReader(RemObjects.DataAbstract.Schema.ServiceSchema sender, RemObjects.DataAbstract.Schema.BeforeExecutingGetDataReaderEventArgs e)
private void DataService_ProcessError(object sender, RemObjects.DataAbstract.Server.DeltaChangeErrorEventArgs e)
// TODO Write this._messageBuffer to error log
// TODO Write e.Exception.ToString() and any additional exception properties to error log
The service uses 2 event handlers to log all ADO.NET commands executed (please note that log will also include parameter values that might contain sensitive info).
In case of an exception the
ProcessError event handler should write the collected commands along with the exception info into some error log (concrete implementation of this method is up to you).
This approach will also help with the failing select queries because the
ProcessError event is raised for all unhandled service exceptions.
Won’t the StringBuilder go berserk and out of capacity if all commands are logged into it? What about memory? Thousands of commands are executed per minute.
The StringBuilder is per-instance. If you use default service settings then the service instance is recreated on each request (this is relatively cheap operation, way cheaper than on Delphi)
If your service instance is reused, then it is possible to use the Deactivation event to clean up the String Builder instance
And String Builder here is way better than the usual string concatenation (no unnecessary sting table entries). Of course there will be additional memory traffic, like with any other way of logging the commands.