Error when using remoteDataAdapter.Update

Hello,

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.

Greetings,
Wouter

Hello

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?

Regards

Hello,

  • 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.

Greetings,
Wouter

Hello

This is a full source code of a DataService with error logging:

using System.Data;
using System.Text;

namespace WinFormsApplication1
{
    [RemObjects.SDK.Server.Service]
    [RemObjects.SDK.Server.ServiceRequiresLogin]
    public class DataService : RemObjects.DataAbstract.Server.DataAbstractService
    {
        private StringBuilder _messageBuffer;

        public DataService()
        {
            this.InitializeComponent();

            this._messageBuffer = new StringBuilder(2048);
            this.BeforeExecutingGetDataReader += DataService_BeforeExecutingGetDataReader;
            this.BeforeExecuteCommand += DataService_BeforeExecuteCommand;
            this.ProcessError += DataService_ProcessError;
        }

        // Required for designer support
        private void InitializeComponent()
        {
            RemObjects.DataAbstract.Bin2DataStreamer dataStreamer;
            dataStreamer = new RemObjects.DataAbstract.Bin2DataStreamer();
            RemObjects.DataAbstract.Scripting.EcmaScriptProvider scriptProvider;
            scriptProvider = new RemObjects.DataAbstract.Scripting.EcmaScriptProvider();
            this.AcquireConnection = true;
            this.ServiceDataStreamer = dataStreamer;
            this.ServiceSchemaName = "WinFormsApplication1Dataset";
            this.ScriptProvider = scriptProvider;
        }

        private void LogCommand(IDbCommand command)
        {
            this._messageBuffer.AppendLine("Command:");
            this._messageBuffer.AppendLine(command.CommandText);

            if (command.Parameters.Count > 0)
            {
                this._messageBuffer.AppendLine("Parameters:");
                foreach (IDataParameter parameter in command.Parameters)
                {
                    this._messageBuffer.Append(parameter.ParameterName);
                    this._messageBuffer.Append(": ");
                    this._messageBuffer.AppendLine(parameter.Value?.ToString());
                }
            }
        }

        private void DataService_BeforeExecuteCommand(RemObjects.DataAbstract.Server.DataAbstractService sender, RemObjects.DataAbstract.Server.DataAbstractServiceExecuteCommandEventArgs e)
        {
            this.LogCommand(e.Command);
        }

        private void DataService_BeforeExecutingGetDataReader(RemObjects.DataAbstract.Schema.ServiceSchema sender, RemObjects.DataAbstract.Schema.BeforeExecutingGetDataReaderEventArgs e)
        {
            this.LogCommand(e.Command);
        }

        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.

1 Like