Sunday, June 28, 2009

Erro no DataSnap: "LinkFields to detail must be unique"

Estive brigando com um erro "LinkFields to detail must be unique" do DataSnap, numa tela que continha 3 DataSets numa relação Mater-Detail típica. O mais interessante é que este tipo de relação é bem comum e eu já desenvolvi com DataSnap este tipo de construção incontáveis vezes, mas este erro resolveu aparecer para ficar.

Considere as seguintes tabelas numa relação Master-Detail:Os campos id_master (PK), id_detail (PK) e id_detail2 (PK) são inteiros e os desc_master, desc_detail e desc_detail2 são Varchar. O campo id_master é FK na tabela detalhe de primeiro nível, table_detail. O campo id_detail é FK na tabela detalhe de segundo nível, table_detail2. As PKs são todas auto-incremento com valor gerado no servidor (de aplicação).

Tenho um DataModule com 3 DataSets (no meu caso, usei TADOQuery) contendo os seguintes comandos SQL:

  • qryMaster:
SELECT * FROM table_master

  • qryDetail:
SELECT * FROM table_detail WHERE id_master = :id_master

  • qryDetail2:
SELECT * FROM table_detail2 WHERE id_detail = :id_detail


Existem ainda 2 componentes TDataSource ligando o qryDetail ao qryMaster (dsMaster) pela propriedade DataSource, e também um ligando o qryDetail2 ao qryDetail (dsDetail). Desta forma, tenho um DataSetProvider ligado ao DataSource dsMaster que irá prover os dados aos meus ClientDataSet. Eis meu DataModule:


O cdsMaster está ligado ao DataSetProvider, prvMaster, o cdsDetail ligado ao DataSetField cdsMasterqryDetail, e o cdsDetail2 está ligado ao DataSetField cdsDetailqryDetail2. Uma construção comum quando se trata de relação master-detail usando DataSnap.

O fato de estar tudo junto (apenas 2 camadas) facilita o entendimento e o debug. Mas poderia estar distribuído, com as queries e o provider num servidor de aplicação e os ClientDataSets na aplicação cliente.

ProviderFlags dos campos configurados corretamente (incluído pfInKey para id_master, id_detail e id_detail2 nas 3 queries), um form simples com 3 DBNavigators e 3 DBGrids ligados aos 3 ClientDataSets. Pois bem, era de se esperar que tudo funcionasse as mil maravilhas na primeira tentativa, certo? Errado!

Ao rodar o programa, inseri dados na tabela master e salvei, chamando então cdsMaster.ApplyUpdates. Tudo Ok. Inseri também registros no cdsDetail, tudo Ok. Ao rodar a aplicação e inserir registros no cdsDetail2 (o detalhe de segundo nível) e chamar o cdsMaster.ApplyUpdates, kabooommmm! Lá vem o erro "Link Fields to detail must be unique". Não fazia o menor sentido uma vez que os FK's são corretamente preenchidos automaticamente pelo DataSnap e os ID's são preenchidos com valores únicos negativos. Não havia nada errado!

O erro acontece ao aplicar os updates. Mas nem chega ao Provider. O erro acontece mesmo no ClientDataSet ANTES de enviar o Update ao Provider.
Aí fui debugar usando o DBClient.pas. O erro ocorre no método TCustomClientDataSet.InternalPost, na linha:


Check(FDSCursor.InsertRecord(ActiveBuffer));


Ou seja... Como isto ocorre dentro do Midas.dll (ou MidasLib.dcu), não há como debugar além deste ponto.

Usei o Google atrás de uma solução, fiz todas as alterações possíveis que pude imaginar para ver se o erro cessava, mas nada funcionava.

Depois de um tempo pensando, eu me lembrei que esta construção onde a PK da tabela master vira FK da tabela detalhe não me é usual. Eu sempre propago as chaves do mestre para compor a chave do detalhe, da forma:

table_detail:
  • id_master (PK)
  • id_detail (PK)
  • desc_detail

table_detail2:
  • id_master (PK)
  • id_detail (PK)
  • id_detail2 (PK)
  • desc_detail
Então resolvi mudar a ordem dos campos no ClientDataSet. No cdsDetail coloquei os campos na ordem (1) id_master, (2) id_detail e (3) desc_detail. No cdsDetail2 fiz o mesmo e coloquei na ordem (1) id_detail, (2) id_detail2 e (3) desc_detail2. Ou seja, o LinkField no detalhe deve ser o primeiro campo no DataSets!

Após isto o Update ocorre normalmente, sem erros. Ainda esta semana vou anexar o código fonte completo do projeto de exemplo.