2013-11-15 5 views
7

Ciao appassionati di codice!TClientDataSet: come mantenere i dati locali salvati e disponibili quando una struttura di database è stata modificata

Ho una domanda, che è senza dubbio dovuta all'inesperienza della mia conoscenza Delphi XE2. Proverò a spiegarlo qui.

Introduzione:

Ho una base di dati Interbase con i dati. Questo database si trova su una macchina remota. L'applicazione client che sto sviluppando utilizza questo database. A causa del fatto che l'applicazione deve essere utilizzata quando non è disponibile alcuna connessione di rete, devo utilizzare il modello di valigetta. Ecco perché utilizzo un ClientDataSet che recupera i dati e li memorizza localmente in un formato XML. Secondo me sarebbe più semplice usare un database locale invece dei file XML, ma non ho ancora il permesso di cambiarlo. Quindi sono ancora legato a XML :(

A causa dell'importanza dei dati, vorrei mantenerlo il più sicuro possibile, anche quando altri sviluppatori stanno cambiando la struttura interna del database (ad esempio, i campi nelle tabelle sono aggiunti, rinominati o addirittura rimossi) i dati archiviati localmente devono essere ancora disponibili

Quello che faccio ora è che utilizzo un ClientDataSet per recuperare i metadati dal database, che viene memorizzato separatamente su disco. sto pianificando di fare, è di confrontare i metadati dal database con i metadati memorizzati nel dataset locale. Quando trovo le differenze nei campi, creo un nuovo set di dati nel codice in cui creo le definizioni dei campi e aggiungo i dati in seguito. In altre parole, creo solo un nuovo set di dati locale che sia conforme la struttura di una tabella dal database remoto.

Questo è facile quando trovo le rimozioni o le aggiunte di colonne (campi), tuttavia diventa un po 'più difficile quando c'è un cambiamento nel nome o nel tipo di dati di un campo.

Non ho ancora preso in considerazione chiavi primarie, straniere e univoche, tuttavia ritengo che sia necessario farlo anche in questo caso.

Domanda:

mia domanda in primo luogo è, è che mi chiedo se questo è il modo corretto. È un bel po 'di lavoro per raggiungere questo obiettivo, e prima di iniziare a implementare tutto questo, vorrei sapere se ci sono altri modi (più convenienti e più facili) per ottenere le cose che ho descritto sopra.

Nel modo in cui lo vedo, i dati disponibili localmente hanno una priorità più alta rispetto ai dati memorizzati nel database remoto. Solo perché l'utente sta lavorando con i dati locali e non direttamente sui dati remoti.

Qualche idea su questo? Spero di poter chiarire abbastanza la mia domanda, in caso contrario, per favore chiedi e fornirò maggiori dettagli. Sto lavorando con Interbase XE (SP5) e Delphi XE 2.

+2

Solo con i nuovi metadati della struttura del database non è possibile decidere se una colonna viene rinominata o se una colonna precedente viene eliminata e ne viene aggiunta una nuova. –

+0

Sono d'accordo, in tal caso continuo a scorrere i fielddefs di entrambi i set di dati per trovare le differenze. Questo è ancora un bel lavoro per il quale dubito che sia efficiente ... – RvdV79

+0

Il database locale è di sola lettura? –

risposta

5

Beh, mi ci è voluto un po 'di tempo, ma ora funziona. Anche se sono ancora un po 'scettico riguardo alla mia soluzione fino ad ora (lo sto testando per il secondo giorno ora, finora non ho ancora avuto problemi). Sono anche contento di averlo funzionato per ora.

Devo scusarmi per la lunghezza di questa risposta, per la quale penso che non sia di beneficio per l'intera leggibilità del mio post, ma non vedo un'altra possibilità di fornire dettagli sufficienti su questo argomento.

Nel caso in cui altre persone stiano lavorando su cose simili, ho deciso di pubblicare la mia soluzione come risposta. Spero che sia d'aiuto, e ovviamente sono ansioso di sapere se mi sarebbe sfuggito qualcosa.

Ho scritto una funzione che tenta di aggiornare i metadati quando le differenze sono state trovate. Poiché i set di dati locali sono archiviati in formato XML (quindi tutto ciò che è memorizzato localmente può essere considerato una stringa), posso considerarli come varianti. Che in effetti è un enorme vantaggio quando si tratta di aggiungere dati:

procedure TdmDatabase.UpdateMetaDataFor(cds : TCustomClientDataSet; folder : String); 

Ora seguono le procedure e la funzione nidificate. Questo potrebbe cambiare in seguito, dal momento che non sono ancora troppo sicuro di utilizzare questo approccio ...

procedure AddInLocalData(local, newCds : TCustomClientDataSet); 
    var i : Integer; 
    begin 
     try 
     (* Assume that the new dataset is still closed... *) 
     newCds.CreateDataSet; 
     newCds.Insert; 
     for i := 0 to Pred(local.Fields.Count) do 
     begin 
      if (i < newCds.FieldCount) then 
      newCds.Fields[i].AsVariant := local.Fields[i].AsVariant; 
     end; 

     newCds.Post; 

     newCds.SaveToFile(folder + newCds.TableName + '_updated.xml', dfXMLUTF8); 
    except on E: Exception do 
     raise Exception.Create(_Translate(RS_ERROR_UNABLE_TO_SYNC_LOCAL_AND_REMOTE)); 
    end; 
    end; 

campi Aggiunta è la parte facile, soprattutto quando ci sono vincoli. I campi aggiuntivi vengono trovati dal set di dati remoto (in realtà proveniente dal database stesso) e i dati non sono importanti lì. Quindi, possiamo inserire un nuovo campo senza preoccuparci dei dati che devono essere inseriti lì. Se questo sarebbe il caso (per il nostro progetto non è necessario), questa funzione ha certo bisogno di aggiornamento:

function AddFieldsLocally(remote, local, newCds : TCustomClientDataSet) : TCustomClientDataSet; 
    var i  : Integer; 
     fieldDef : TFieldDef; 
    begin 
    try 
     (* Local provider is leading... *) 
     newCds.SetProvider(local); 
     newCds.FieldDefs.Update; 
     (* Find the already existing fields and add them *) 
     for i := 0 to Pred(newCds.FieldDefs.Count) do 
     begin 
     with newCds.FieldDefs[i].CreateField(cds) do 
     begin 
      FieldName := local.Fields[i].FieldName; 
      Calculated := local.Fields[i].Calculated; 
      Required := local.Fields[i].Required; 
      Size := local.Fields[i].Size; //TODO: Add checking here! 
     end; 
     end; 
     (* Check for additional fields that exist remotely and add them *) 
     for i := newCds.fieldDefs.Count to Pred(remote.FieldDefs.Count) do 
     begin 
     fieldDef := remote.FieldDefs.Items[i]; 
     if (fieldDef <> nil) then 
     begin 
      newCds.FieldDefs.Add(fieldDef.Name, fieldDef.DataType, fieldDef.Size, fieldDef.Required); 
      newCds.FieldDefs[ Pred(newCds.FieldDefs.Count)].CreateField(newCds); 
     end; 
     end; 

    (* Finally, add the existing local data to the newly created dataset *) 
    AddInLocalData(local, newCds); 
    result := newCds; 
    except on E:Exception 
     raise E; 
    end; 
    end; 

rimozione di campi è un po 'più specifico. Per prima cosa, deve ancora essere verificato se un campo che deve essere rimosso, ha dei vincoli. In tal caso, il metodo non dovrebbe continuare e l'intero set di dati locale con tutte le tabelle dovrebbe essere rimosso e ricostruito dal database, solo per garantire la corretta funzionalità. Attualmente, questi cambiamenti sono considerati importanti cambiamenti. Eseguo un controllo se sono state applicate importanti modifiche, se lo fa, molto probabilmente sarà necessaria anche una nuova versione dell'applicazione.

function RemoveFieldsLocally(remote, local, newCds : TCustomClientDataSet) : TCustomClientDataSet; 
    var i  : Integer; 
     fieldDef : TFieldDef; 
     field : TField; 
    begin 
    try 
     (* Remote provider has lead here! *) 
     newCds.SetProvider(remote); 
     newCds.FieldDefs.Update; 

     (* Find the already existing fields and add them *) 
     for i := 0 to Pred(newCds.FieldDefs.Count) do 
     begin 
     field := newCds.FieldDefs[i].CreateField(cds); 

     if assigned(field) then 
     begin 
      field.FieldName := local.Fields[i].FieldName; 
      field.Calculated := local.Fields[i].Calculated; 
      field.Required := local.Fields[i].Required; 
      (* Necessary for compatibility with for example StringFields, BlobFields, etc *) 
      if (HasProperty(field, 'Size')) then 
      Field.Size := local.FIelds[i].Size; 
     end; 
     end; 

    (* Now add in the existing data from the local dataset. 
     Warning: since fields have been removed in the remote dataset, these 
     will not be added as well. If constraints were put up, these become 
     lost *) 
    AddInLocalData(local, newCds); 
    result := newCds; 
    except on E:Exception do 
    raise E; 
    end; 
end; 

La funzione seguente controlla l'uguaglianza tra i campi. Se vengono rilevate differenze per quanto riguarda DataType (FieldType), FieldName e così via, tenterà di aggiornarlo in base ai metadati del set di dati remoto, che è in testa.

function VerifyInternalStructuresAndFields(remote, local, newCds : TCustomClientDataSet) : boolean; 
var i, equalityCounter : Integer; 
    equal : boolean; 
begin 
    try 
    (* We know that both datasets (local and remote) are equal for when it comes to 
     the fieldcount. In this case, the structure of the dataset from the remote  dataset is leading. *) 
    newCds.SetProvider(remote); 
    newCds.FieldDefs.Update; 

    equal := false; 
    equalityCounter := 0; 
    for i := 0 to Pred(newCds.FieldDefs.Count) do 
    begin 
     (* 1. Fielddefinitions which are exactly equal, can be copied *) 
     equal := (remote.Fields[i].FieldName = local.Fields[i].FieldName) and 
       (remote.Fields[i].Required = local.Fields[i].Required) and 
       (remote.Fields[i].Calculated = local.Fields[i].Calculated) and 
       (remote.Fields[i].DataType = local.Fields[i].DataType) and 
      (remote.Fields[i].Size = local.Fields[i].Size); 

     if (equal) then 
     begin 
     inc(equalityCounter); 
     with newCds.FieldDefs[i].CreateField(cds) do 
     begin 
      FieldName := local.Fields[i].FieldName; 
      Calculated := local.Fields[i].Calculated; 
      Required := local.Fields[i].Required; 
      Size := local.FIelds[i].Size; 
     end; 
     end 
     else (* fields differ, try to update it, here the remote fields are leading! *) 
     begin 
     if (MessageDlg(_Translate(RS_WARNING_DIFFERENCES_IN_FIELDS), mtWarning, mbYesNo, 0) = IDYES) then 
     begin 
      with newCds.FieldDefs[i].CreateField(cds) do 
      begin 
      FieldName := remote.Fields[i].FieldName; 
      Calculated := remote.Fields[i].Calculated; 
      Required := remote.Fields[i].Required; 
      if (HasProperty(remote, 'Size')) then 
       Size := remote.Fields[i].Size; 
      SetFieldType(remote.Fields[i].DataType); //TODO: If this turns out to be unnecessary, remove it. 
      end; 
     end 
     else 
     begin 
      result := false; 
      exit; 
     end; 
     end; 
    end; 

    if (equalityCounter = local.FieldCount) then 
    begin 
     result := false; 
    end else 
    begin 
     AddInLocalData(local, newCds); 
     result := true; 
    end; 
    except on E:Exception do 
    raise E; 
    end; 
end; 

Questa è la funzione principale che cercherà di rilevare le differenze fra campi e campo definizioni dei set di dati locali e remoti.

function FindDifferencesInFields(remote, local: TCustomClientDataSet) : TCustomClientDataSet; 
var i, k  : Integer; 
    fieldDef : TFieldDef; 
    newCds : TKLAClientDataSet; 
begin 
    try 
    newCds := TCustomClientDataSet.Create(nil); 
    newCds.FileName := local.FileName; 
    newCds.Name := local.Name; 
    newCds.TableName := local.TableName; 

    (* First check if the remote dataset has added fields. *) 
    if (remote.FieldDefs.Count > local.FieldDefs.Count) then 
    begin 
     result := AddFieldsLocally(remote, local, newCds); 
    end 
    (* If no added fields could be found, check for removed fields *) 
    else if (remote.FieldDefs.Count < local.FieldDefs.Count) then 
    begin 
     result := RemoveFieldsLocally(remote, local, newCds); 
    end 
    (* Finally, check if the fieldcounts are equal and if renames have taken place *) 
    else if (remote.FieldDefs.Count = local.FieldDefs.Count) then 
    begin 
     if (VerifyInternalStructuresAndFields(remote, local, newCds)) then 
     result := newCds 
     else result := local; 
    end; 
    except on E:Exception do 
    raise Exception.Create('Could not verify remote and local dataset: ' + E.Message); 
    end; 
end; 

Poiché tutte le funzioni e le procedure utilizzate sopra sono piuttosto critico, ho deciso di farli annidati all'interno della procedura principale chiamata UpdateMetaDataFor. Potrei cambiarlo più tardi, ma per ora è abbastanza buono.

var fieldDefs : TFieldDefs; 
remotecds  : TCustomClientDataSet; 
constraints : TCheckConstraints; 
fileName  : String; 
k    : integer; 
begin 
    try 
    try 
     ConnectDB(false); 
     fileName := folder + cds.TableName + '_metadata_update.xml'; 

     (* Retrieve the latest metadata if applicable *) 
     remotecds := CreateDataset; 
     remotecds.SQLConnection := SQLConnection; 
     remotecds.TableName := cds.TableName; 
     remotecds.SQL.Text := Format('SELECT * from %s where id=-1', [cds.TableName]); 
     remotecds.Open; 

     remotecds.SaveToFile(fileName , dfXMLUTF8); 

     (* Load the local dataset with data for comparison *) 
     cds.LoadFromFile(folder + cds.FileName); 

     SyncProgress(_Translate(RS_SYNC_INTEGRITY_CHECK) + ' ' + cds.TableName); 

     cds := FindDifferencesInFields(remotecds, cds); 
     cds.SaveToFile(folder + cds.FileName, dfXMLUTF8); 
    except on E: Exception do 
     ShowMessage(E.Message); 
    end; 
finally 
    if assigned(remotecds) then 
    remotecds.Free; 
    if FileExists(fileName) then 
    SysUtils.DeleteFile(fileName); 
    end; 
end; 

Questo conclude la mia risposta molto esaustiva, che lascia ancora alcune cose a disposizione. Ad esempio, cosa si deve fare con i vincoli (questi non sono direttamente visibili su un set di dati locale).

Un altro approccio potrebbe essere quello di aggiungere una tabella al database stesso, che conterrà tutte le modifiche a seconda del numero di versione. Se questi cambiamenti sono considerati minori (cambio di nome per un campo che non ha vincoli o altro), allora questo approccio semi-automatico può ancora essere usato.

Come sempre, sono ancora molto curioso di altri approcci per garantire l'integrità di un database quando si applicano modelli di cartelle per database.

Problemi correlati