Deep Object Comparison Delphi

Wagner

I would like your opinion on the best way to buy if two objects are equal,
being equal do not send a put to the server and inform the user that the object has not been changed;

Problem 1:

I tried to use the class TDataSnapJsonSerializer (Aurelius.Json.DataSnap), but I did not succeed
for more complex entities (with associations), since fields are returned out of order at each serialization.

Ex.:

  loEntidadeComplexa1 := FClient.Get<TEntidadeComplexa>(10);
  loJson1 := loEntidadeComplexa1.ToJsonString;  
  
  loEntidadeComplexa2 := FClient.Get<TEntidadeComplexa>(10);
  loJson2 := loEntidadeComplexa2.ToJsonString;
  
  if loJson1 <> loJson2 then
    FClient.Put(loEntidadeComplexa2)
  else
    Showmessage('No data was changed');

...

Serialization result loJson1:
{
  "$type": "App.Modelo.Entidade.TEntidadeComplexa",
  "$id": 1,
  "FNome": "FAZENDA TR\u00CAS PONTAS ALTERADO",
  "FAreaCultura": 38000,
  "FQuantidadeNascente": 3,
  "FCooperado": {
    "$proxy": "single",
    "key": null,
    "class": "",
    "member": ""
  },
  "FCulturas": {
    "$proxy": "list",
    "key": null,
    "class": "",
    "member": ""
  }, ...

Serialization result loJson2:
{
  "$type": "App.Modelo.Entidade.TEntidadeComplexa",
  "$id": 1,
  "FValidade": "2020-02-10",
  "FObservacao": null,
  "FImagemMapa": {
    "$lazyblob": true,
    "key": null,
    "class": "",
    "member": ""
  },
  "FCooperado": {
    "$proxy": "single",
    "key": null,
    "class": "",
    "member": ""
  }, ...  

Problem 2:

Accessing a property that references another entity also changes the final json.

  [Entity]
  [Table('MUNICIPIO')]
  [Sequence('SEQ_MUNICIPIO')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TMunicipio = class
  strict private
    [Column('ID')]
    FID: Integer;

    [Column('NOME', [TColumnProp.Unique, TColumnProp.Required], 40)]
    FNome: string;
    
    [Association([TAssociationProp.Lazy], CascadeTypeAllButRemove)]
    [ForeignKey('FK_MUNICIPIO_UF')]
    [JoinColumn('ID_UNIDADE_FEDERATIVA', [TColumnProp.Required])]
    FUnidadeFederativa: Proxy<TUnidadeFederativa>;
  protected ...
  
  loMunicipio1 := FClient.Get<TMunicipio>(12);
  loJson1 := loMunicipio1.ToJsonString;
  
  loMunicipio2 := FClient.Get<TMunicipio>(12);
  UF := loMunicipio2.UnidadeFederativa.Nome;
  loJson2 := loMunicipio2.ToJsonString;  

...
  
Serialization result loJson1:
{
  "$type": "App.Modelo.Entidade.TMunicipio",
  "$id": 1,
  "FID": 12,
  "FNome": "TR\u00CAS PONTAS",
  "FUnidadeFederativa": {
    "$proxy": "single",
    "key": null,
    "class": "",
    "member": ""
  }
}  

Serialization result loJson2:
{
  "$type": "App.Modelo.Entidade.TMunicipio",
  "$id": 1,
  "FID": 12,
  "FNome": "TR\u00CAS PONTAS",
  "FUnidadeFederativa": {
    "$type": "App.Modelo.Entidade.TUnidadeFederativa",
    "$id": 2,
    "FID": 11,
    "FNome": "MINAS GERAIS",
    "FSigla": "MG",
    "FCodigoIBGE": 31,
    "FPais": {
      "$type": "App.Modelo.Entidade.TPais",
      "$id": 3,
      "FID": 27,
      "FCodigo": 55,
      "FNomePtBr": "BRASIL",
      "FNomeEn": "BRAZIL",
      "FSigla2": "BR",
      "FSigla3": "BRA"
    }
  }
}

I think using Json is not the best option, you agree?

What is the best way to do this comparison? Does Aurelius have any features that make this easy?

Thank you


Hi,


You can use some methods in TMappingExplorer, for example ObjectChanged, UpdateChangedColumns or GetColumnValues. Those are methods that could help you out with that.

But, as a piece of advice, why are you doing that at client side? Shouldn't you be treating that at server-side. And in any case, when using Merge as PutMode (the default), XData will internally perform a Merge operation using Aurelius object manager at server-side, and will only update the modified columns anyway.
But, as a piece of advice, why are you doing that at client side?

Yes, I'm doing this from the client side, through json comparison,
because I would like to avoid bandwidth consumption and send a message to the user.

Shouldn't you be treating that at server-side...

What is the best way to treat this server side and send a message to the user that the object has not changed data.

I tested the "ObjectChanged" method of class "TMappingExplorer" but did not work as expected.

for example:

  loMunicipio1 := FClient.Get<TMunicipio>(12);  
  
  loMunicipio2 := FClient.Get<TMunicipio>(12);

  // here -> Object not changed
  if TMappingExplorer.Default.ObjectChanged(loMunicipio1,loMunicipio2) then
    ShowMessage('Object changed')
  else
    ShowMessage('Object not changed');   

  // here accesses an association
  loJson2 := loMunicipio2.UnidadeFederativa.Nome;      


  // here -> Object changed
  if TMappingExplorer.Default.ObjectChanged(loMunicipio1,loMunicipio2) then
    ShowMessage('Object changed')
  else
    ShowMessage('Object not changed'); 

Why are they different?

Invert the order, use ObjectChanged(loMunicipio2, loMunicipio1)

Ok, it worked now (Invert the order). Thanks....

What is the best way to treat this server side and send a message to the user that the object has not changed data?

You can use XData server-side events (OnEntityModifying for example): http://www.tmssoftware.biz/business/xdata/doc/web/onentitymodifying_event.html

Is what I imagined. 


But how to get the original entity? Will I have to retrieve it again through a get, and then compare them to the "ObjectChanged" method of class "TMappingExplorer"?
If you take a look at the method TObjectManager.InternalFlush, you will see this code:



  Old := FObjects.GetOldState(Entity);
  if Old <> nil then
  begin
    ChangedColumns := TList<string>.Create;
    try
      Explorer.UpdateChangedColumns(Entity, Old.Values, ChangedColumns);
      if ChangedColumns.Count > 0 then
        PerformUpdate(Entity, ChangedColumns);
    finally
      ChangedColumns.Free;
    end;
  end else
    PerformUpdate(Entity, nil);


You can use a similar approach. The only trick is that FObjects is private. You can try to use a hack to access a protected property:


type THackManager = class(TObjectManager);
{...}
Old := THackManager(TXDataOperationContext.Current.GetManager).ObjectMap.GetOldState(Entity);

Wagner

I tried to use your approach, but I'm getting error "Exception class EListError with message 'Item not found'", in the line below:

Old: = THackManager (TXDataOperationContext.Current.GetManager) .ObjectMap.GetOldState (Args.Entity);

The "FMap" field of the "TObjectMap" object does not contain any reference to the entity being changed.

Are you trying that in your real application or a test application? If you have a test application that can be compiled and run by us, could you send it to us via e-mail so we can implement the solution there directly and send back to you?

In an actual application, but I can prepare a test application.Could send it to which email?

support @ tmssoftware.com

Thank you. Indeed it was an issue with OnEntityModifying event in a very specific situation (on merging objects). We have fixed this and sent you a patch through e-mail. The fix will be included in the next XData version release.

Thanks,Wagner