Gestire uno storico di modifiche ai record del database con Foundation

Ho realizzato un progetto di esempio per Instant Developer Foundation che implementa sul mitico database Northwind una gestione generalizzata di salvataggio delle modifiche della versione precedente del record o della catena di record in caso di master-detail.

L’esempio lo potete scaricare qui: Log Modifiche Documenti.zip (1,4 MB)

Naturalmente questa cosa è facilmente :joy: realizzabile utilizzando i documenti di Instant Developer Foundation, gli eventi globali e alcune caratteristiche del framework.

Vi racconto un po’ come funziona.

Per prima cosa occorre avere un posto dove memorizzare le versioni precedenti un aggiornamento di record di una tabella e nel database Northwind ho creato la tabella Logs con i seguenti campi:

  • ID - la chiave primaria
  • Document - Nome del documento
  • Date Mod - Data e ora fino alla quale il documento aveva la forma qui ssalvata
  • Doc DNA - La chiave univoca del documento
  • Record - qui salviamo in formato JSON il documento prima della modifica

Al salvataggio dei documenti andremo a scrivere in questa tabella la versione prima della modifica e lo facciamo utilizzando l’vento GlobalBeforeSave dei documenti.
Il codice nell’evento utilizza la reflection di Instant Developer Foundation per analizzare la struttura del documento ed andare a prendersi i valori precedenti dei campi e richiamare il metodo CreaLog().

Le strutture padre-figlio scrivono un unico record sulla tabella Logs con tutto il documento comprensivo dei due livelli come si vede nel codice qui sotto (occorre modificarlo per renderlo multilivello).

// Se sono in modifica e non sto salvado un documento di classe Log procedo
if (Phase == 2 && Doc.updated && Doc.typeName() != "Log")
{
  // Disabilito il log singolo dei documenti figli di una struttura padre figlio (viene fatto nel documento principale)
  IDDocumentStructure ds = Doc.getStructure()
   
  for (int i = 1; i <= ds.getCollectionCount(); i = i + 1)
  {
    IDCollection coll of IDDocument = (IDCollection)Doc.getCollection(i)
     
    for each IDDocument docColl in coll
    {
      docColl.setTag(NOLOGCHILD, true)
    }
  }
   
  boolean noLog = Doc.getTag(NOLOGCHILD)
   
  if (noLog == false)
  {
    if (!(Log.CreaLog(Doc)))
    {
      // Se non riesco a creare il log potrei annullare il salvataggio impostando Cancel = true in questo blocco di codice
    }
  }
}

Il metodo statico CreaLog del documento Log recupera i valori originali e salva il tutto nella tabella Logs.

boolean esito = false
IDDocument docOriginal = Log.RecuperaValoriOriginali(Doc)
string docXml = docOriginal.saveToXML(false, "", 9999, JSON, ...)
 
Log l = new()
l.init()
l.Document = docOriginal.typeName()
l.DateMod = now()
l.DocDNA = docOriginal.getDNA()
l.Record = docXml
 
esito = l.saveToDB(...)
return esito

Non sto a descrivere tutti metodi che sono presenti nel progetto ma era giusto per farvi vedere al volo che si possono fare cose simpatiche con la reflection di Instant Developer Foundation.

Comunque in questo modo abbiamo che quando un documento viene salvato causa una modifica viene scritto un record sulla tabella Logs.
Per il momento non è stata gestita la cancellazione di un record.

Vediamo ora come possiamo far vedere queste modifiche.

Per prima cosa ho creato una videata che visualizza i record del documento Log (LogsDoc):

Il contenuto del record prima della modifica è nel riquadro rosso ed è in formato JSON che è di poco aiuto per gli nostri dell’applicativo, ma in esso ci sono tutti i dati che ci occorrono per visualizzare un record in una videata.
Come facciamo a visualizzare correttamente questi dati?

Per ogni documento ho creato una videata apposita con il solo dettaglio e in sola visualizzazione così da avere un posto specifico dove visualizzare ogni singolo documento.
Nella videata LogsDoc è stato gestito l’evento l’evento OnActivatingRow che riporta questo codice:

Log l = Logs.document
 
if (l != null)
{
  IDDocument doc = IDDocument.getFromDNA(l.DocDNA, ...)
  doc.loadFromXML(l.Record, false, JSON, ...)
  doc.show(Popup)
  if (this.RichiamataDaMenu == false)
  {
    this.close(...)
  }
}

Qui prendo il documento attivo dal pannello e poi instanzio un IDDocument (il documento base da cui ereditano tutti i documenti) e con il metodo getFromDNA() ne creo la struttura in memoria nella variabile doc.
Poi carico i suoi dati dal campo del record di Log preso dal video e a questo punto posso usare il metodo show() del documento che lo porterà a video nella videata di default del documento stesso.

La videata di default di un documento è quella che è stata creata per prima con il drag & drop per quel documento ed è possibile cambiare questa impostazione come spiegato più sotto.

Nel progetto ho creato le videate per visualizzare i dati prima della modifica dei miei documenti e sono quelle che Vis alla fine del nome e sono impostate solo per visualizzare un record in dettaglio.

Cliccando con il tasto destro del mouse su di una videata si vede a quale classe è collegata perché sono presenti i comandi Vai alla classe xxx e Stacca dalla classe xxx.
Per cambiare la videata predefinita si deve trascinare la nuova videata (che vuoi diventi quella predefinita) sul documento tenendo premuto il tasto il tasto SHIFT.

Questo un esempio di videata per visualizzare il record di Log.

Nell’immagine si vede che il campo Shipped Date ha il testo di colore rosso ad indicare che quello è il valore modificato rispetto al documento attuale.
Questa finezza è realizzata dal metodo confrontaDocs della classe DocHelper.

if (doc != null && docPrec != null)
{
  IDDocumentStructure ds = doc.getStructure() // 
  // 
  for (int i = 1; i <= ds.getPropertyCount(); i = i + 1)
  {
    string value = doc.getProperty(i)
    string valuePrec = docPrec.getProperty(i)
    if (value != valuePrec)
    {
      IDPropertyDefinition pd = ds.getPropertyDefinition(i)
      // 
      for (int k = 0; k < pan.fieldsCount(); k = k + 1)
      {
         if (pan.getFieldDBCode(k) == pd.DBCode)
         {
           pan.setFieldTextColor(k, RGBColor(255, 0, 0, ...))
           break 
         }
      }
    }
  }
}

Questo metodo riceve tre parametri:

  • doc - la versione attuale del documento
  • docPrev - la versione precedente del documento
  • pan - il pannello dove viene visualizzato il docPrev

Per ora il metodo confrontaDocs non va a verificare i documenti figli ma lo sistemo quanto prima.

La descrizione di questo esempio non è completa ma almeno con quello che ho scritto vi saprete orientare al suo interno e poi con vostri consigli posso migliorarlo.

6 Mi Piace

Molto bello! Grazie Paolo.
Due domande:
Nal metodo confrontaDocs della classe DocHelper, pan come viene ricavato?
Se cambia la struttura delle tabelle/campi db che succede?

1 Mi Piace

Bello, molto interessante, soprattutto il fatto di salvare a livello di padre tutta la gerarchia e il confronto.

Potrebbe avere qualche problema se si gestisce indipendentemente un figlio, cosa che di solito si fa lato codice su operazioni automatizzate, però per le modifiche da interfaccia dovrebbe essere sufficiente.

Oppure se un documento figlio ha 2 padri (es righe ordine per ordine e righe ordine per articolo) verrà registrato il log della gerarchia solo per il padre che aveva quando è stato modificato, quindi se lo confronto per il secondo padre non troverò modifiche oppure ne troverò più di quante dovrei a seconda di cosa è stato modificato (o ripristinato come valore).

1 Mi Piace

L’oggetto pan è passato come parametro al metodo ed è un IDPanel.
Il metodo ConfrontaDocs viene richiamato nell’evento Activate della videata che visualizza il documento letto dalla tabella Logs.

Non ho provato a cambiare la struttura di una tabella aggiungendo un campo ma dovrebbe funzionare ugualmente, magari occorre mettere una protezione al codice.

Ribadisco che è un esempio da migliorare sicuramente ma ci mostra le potenzialità dei documenti e degli eventi globali di Instant Developer Foundation.

2 Mi Piace

Mi sono accorto adesso che avevo già scritto un post con questo argomento e questo esempio un po’ più grezzo…

Questo il link:

Magari vedo se si riesce a unificare i post, sicuramente aggiorno l’esempio allegato all’ultima versione e cito questo post anche nell’altro.

L’età comincia a farsi sentire :thinking:

1 Mi Piace

Tranquillo, non sei l’unico… e non mi è neanche venuto quel senso di deja vu :rofl:

1 Mi Piace

Il post è di gennaio 2024 e l’ho anche cercato prima di scrivere. :joy:

2 Mi Piace

Ho cambiato categoria a questo articolo che è più corretto sia un esempio e non un Tips & Tricks.

1 Mi Piace