Home > Delphi > Applying updates to more than one ClientDataSet in a single transaction with RemObjects SDK

Applying updates to more than one ClientDataSet in a single transaction with RemObjects SDK

Portuguese notice: Escrevi esse artigo em inglês pois o objetivo inicial é ajudar usuários do news da RemObjects com essa dúvida frequente. Mas acredito que está bem fácil de entender, mesmo para quem não domina o idioma. Em todo caso, fiquem livres para postar comentários em português pedindo esclarecimentos de algo que não entenderam.

One of the first tricky things you face off when migrating from 2 to 3 tiers is to get rid of all transaction control on the client side. The client should not start or finish a transaction. As a matter of fact, the client should know nothing at all about transactions. All transaction logic should remain on the server.

This post is intended to show how to create a method in your RemObjects DataSnap server to receive a list of ClientDataSet deltas to update on the server inside a single transaction so that you can rollback all changes in case of an unexpected error during the updates.

This method is useful only when you need to apply ClientDataSets that are not nested, because DataSnap already applies nested datasets in a single transaction by default.

You’re going to need a working RO DataSnap server, since I’m not going to cover the steps involved in building one. We are just going to add a new method to your existing service.

First you need to create some data types in your server RODL. Using RO Service Builder, create a DeltaToApply struct with ProviderName  (string) and Delta (binary) fields:

Then create an array of DeltaToApply:

Then you have to create a method in your service that takes the array parameter. I always add this method to the same service that has all my providers, because we are going to need them to apply the updates:

The implementation of this method is as follow: 

procedure TNewService.ApplyUpdates(var ADeltaArray: DeltaArray);
var
  I: Integer;
  Provider: TDataSetProvider;
  ErrorCount: Integer;
begin
  // Put your code to start transaction
  try
    for I := 0 to ADeltaArray.Count - 1 do
    begin
      Provider := FindProvider(ADeltaArray[I].ProviderName);
      if not Assigned(Provider) then
        raise Exception.Create('Provider not found: ' + ADeltaArray[I].ProviderName);

      Provider.ApplyUpdates(VariantFromBinary(ADeltaArray[I].Delta), 0, ErrorCount);
      if ErrorCount > 0 then
        // Put your code to handle errors
        raise Exception.Create('Errors during applyupdates: ' + Provider.Name);
    end;
    // Put your code to commit the transaction
  except
    // Put your code to rollback the transaction
    raise;
  end;
end;

I have also created a helper function to find the provider by its name:

function TNewService.FindProvider(ProviderName: string): TDataSetProvider;
var
  Component: TObject;
begin
  Component := FindComponent(ProviderName);
  if Component is TDataSetProvider then
    Result := Component as TDataSetProvider
  else
    Result := nil;
end;

Ok, this is it for the server. On the client, you need to create a method that adds all ClientDataSet deltas to your array and sends it to the server:

procedure TClientForm.ApplyUpdates(ClientDataSets: array of TClientDataSet);
var
  Deltas: DeltaArray;
  Delta: DeltaToApply;
  I: Integer;
begin
  Deltas := DeltaArray.Create;
  try
    for I := Low(ClientDataSets) to High(ClientDataSets) do
    begin
      if ClientDataSets[I].ChangeCount = 0 then
        Continue;

      Delta := Deltas.Add;
      Delta.ProviderName := ClientDataSets[I].ProviderName;
      Delta.Delta := BinaryFromVariant(ClientDataSets[I].Delta);
    end;
    CoNewService.Create(ROMessage, ROChannel).ApplyUpdates(Deltas);
  finally
    Deltas.Free;
  end;
end;

The VariantFromBinary and BinaryFromVariant are located in the uROBinaryHelpers unit.

Then you just need to call this method from the client passing all ClientDataSets you want to update on the server using a single transaction:

ApplyUpdates([ClientDataSet1, ClientDataSet2, ClientDataSet3]);

That’s it! I hope it helps. If you find any problems or improvements to this code, please, let me know ASAP so that I can fix.

Categories: Delphi Tags:
  1. Frainer
    October 17th, 2006 at 08:56 | #1

    Bom dia Erick, sou acompanhante assiduo de seu blog.
    Como não domino o idioma ingles, gostaria se possivel,
    publicar ou me mandar este post em portugues, o assunto
    me pareceu bem interessante.

    t+ ,

    Frainer Mauri

  2. October 17th, 2006 at 09:53 | #2

    Frainer, não tenho o artigo em português, mas você pode usar um tradutor online para ler. A idéia é criar um método no servidor para aplicar os updates em vários ClientDataSets em uma única transação.

  3. Jeferson Oliveira
    September 5th, 2007 at 18:48 | #3

    Olá Erick!

    Muito bom e útil esse artigo. Parabéns!

    O único incômodo ao utilizar TDataSetProvider.ApplyUpdates ao invés TClientDataSet.ApplyUpdates, é que podemos saber que houve erros, consultando ErrorCount, mas não podemos tratar, ou enviar para a aplicação cliente, detalhes do erro, o que normalmente seria feito no evento OnReconcileError do ClientDataSet.

    Uma forma de resolver isso:

    1) Implementar um método para levantar a exceção com a mensagem de erro exata:

    procedure TNewService.ProviderUpdateError(Sender: TObject;
    DataSet: TCustomClientDataSet; E: EUpdateError; UpdateKind: TUpdateKind;
    var Response: TResolverResponse);
    begin
    raise EDatabaseError.Create(E.Message);
    end;

    2) Associar esse método ao evento OnUpdateError de todos os providers do DataModule;

    3) Tratar a exceção na chamada à ApplyUpdates:

    try
    Provider.ApplyUpdates(VariantFromBinary(ADeltaArray[i].Delta), 0, ErrorCount);
    except
    on E: Exception do
    begin
    raise Exception.Create(Format(‘Ocorreram %d erros na atualização do provider “%s”. Erro: %s’,
    [ErrorCount, ADeltaArray[i].ProviderName, E.Message]));
    end;
    end;

    Fica aí essa dica que talvez seja útil para outros desenvolvedores.

    Um abraço!

  4. September 5th, 2007 at 20:00 | #4

    Vale pela dica Jeferson, tratar os erros sem dúvida é muito importante.

  5. Fellipe
    January 25th, 2008 at 08:48 | #5

    Não entendi esta parte: Tratar a exceção na chamada à ApplyUpdates:
    Como faria isso?

  6. Roberto Novakosky
    March 27th, 2010 at 10:53 | #6

    Estou procurando algo parecido com o código acima, gostaria de fazer uma só transação com diversos ClientDataSets, mas que o ChangeLog seja restaurado de todos os CDS caso algum apply dê erro. O código abaixo funciona para a transação, mas os CDS não ficam com seus changeLogs pendentes novamente…
    (Usando ADO) Transação de teste:


    Várias operações com vários CDS… cds1, cds2, cds3

    try
    ADOConn.BeginTrans;
    if cds1.ChangeCount > 0 then
    if cds1.ApplyUpdates(0) 0 then //OK !!!
    raise Exception.Create(‘Erro ….’);

    //Forço aqui um erro depois do primeiro ApplyUpdates para entrar no rollback de propósito para este teste:
    raise Exception.Create(‘Erro para teste….’); //Força ir para except

    if cds2.ChangeCount > 0 then
    if cds2.ApplyUpdates(0) 0 then
    raise Exception.Create(‘Erro ….’);

    //
    ADOConn.CommitTrans; OK !!!
    except
    on E:Exception do
    begin
    ADOConn.RollbackTrans; OK !!! Continua tudo certinho no banco de dados, õ aplly do cds1 não efetivou no banco de dados.
    // * Mas aqui o cds1.ChangeCount está com valor zero, pois o ChangeLog do CDS1 foi esvaziado no momento do ApplyUpdate dele, e não foi restaurado após o fato do rollback..
    end;
    end;

    * Já que houve o rollback, eu gostaria de restaurar o changelod do clientDataSet para que as alterações se tornem pendentes nele novamente, alguma idéia de como fazer isso ?