Disegni e pratiche per difendersi da voci nulle errate dal database

9

Una parte del mio programma recupera i dati da molte tabelle e colonne nel mio database per l'elaborazione. Alcune colonne potrebbero essere null , ma nel contesto di elaborazione corrente è un errore.

Questo dovrebbe "teoricamente" non accadere, quindi se lo fa punta a dati errati oa un bug nel codice. Gli errori hanno diverse gravità, a seconda del campo che è null ; Ad esempio, per alcuni campi l'elaborazione deve essere interrotta e qualcuno deve essere informato, per altri l'elaborazione deve essere autorizzata a continuare e basta avvisare qualcuno.

Esistono validi principi di architettura o di progettazione per gestire le voci di null rare ma possibili?

Le soluzioni dovrebbero essere implementabili con Java ma non ho usato il tag perché ritengo che il problema sia in qualche modo indipendente dal linguaggio.

Alcuni pensieri che ho avuto io stesso:

Uso di NOT NULL

Il modo più semplice sarebbe utilizzare un vincolo NOT NULL nel database.

Ma cosa succede se l'inserimento originale dei dati è più importante di questa fase di elaborazione successiva? Quindi, nel caso in cui l'inserto inserisse un null nella tabella (o a causa di bug o forse anche qualche ragione valida), non vorrei che l'inserimento fallisse. Diciamo che molte altre parti del programma dipendono dai dati inseriti, ma non da questa particolare colonna. Quindi preferirei rischiare l'errore nella fase di elaborazione corrente invece della fase di inserimento. Ecco perché non voglio usare un vincolo NOT NULL.

Naively in base a NullPointerException

Potrei semplicemente usare i dati come se mi aspettassi che fosse sempre lì (e dovrebbe essere proprio così) e catturare gli NPE risultanti a un livello appropriato (ad esempio, in modo che l'elaborazione della voce corrente si fermi ma non il l'intero processo di elaborazione). Questo è il principio del "fail fast" e spesso lo preferisco. Se si tratta di un bug, almeno ricevo un NPE registrato.

Ma poi perdo la capacità di distinguere tra vari tipi di dati mancanti. Per esempio. per alcuni dati mancanti potrei lasciarlo fuori, ma per altri l'elaborazione dovrebbe essere interrotta e un amministratore notificato.

Verifica null prima di ogni accesso e generazione di eccezioni personalizzate

Le eccezioni personalizzate mi consentono di decidere l'azione corretta in base all'eccezione, quindi questa sembra la strada da percorrere.

Ma cosa succede se dimentico di controllarlo da qualche parte? Inoltre ho ingombrato il mio codice con controlli null che non sono mai o raramente previsti (e quindi sicuramente non fanno parte del flusso della logica di business).

Se scelgo di andare in questo modo, quali sono i modelli più adatti per l'approccio?

Qualsiasi pensiero e commento sui miei approcci sono i benvenuti. Anche migliori soluzioni di qualsiasi tipo (schemi, principi, migliore architettura del mio codice o modelli ecc.).

Modifica

C'è un altro vincolo, in quanto sto usando un ORM per fare il mapping dal DB all'oggetto di persistenza, quindi fare controlli nulli su quel livello non funzionerebbe (dato che gli stessi oggetti sono usati in parti dove il null non fa alcun danno). Ho aggiunto questo perché le risposte fornite finora hanno menzionato questa opzione.

    
posta jhyot 05.01.2016 - 14:39
fonte

8 risposte

9

Inserirò i controlli null nel codice di mappatura, in cui costruisci il tuo oggetto dal set di risultati. Questo mette il controllo in un posto, e non permetterà al tuo codice di ottenere a metà strada l'elaborazione di un record prima di colpire un errore. A seconda di come funziona il flusso dell'applicazione, potresti voler eseguire la mappatura di tutti i risultati come una fase di pre-elaborazione invece di mappare ed elaborare ogni record uno alla volta.

Se stai utilizzando un ORM, dovrai eseguire tutti i controlli null prima di elaborare ogni record. Consiglierei un metodo recordIsValid(recordData) -type, in questo modo è possibile (di nuovo) mantenere tutta la logica di verifica nulla e di convalida in un unico posto. Definitivamente non mescolerei i controlli null con il resto della tua logica di elaborazione.

    
risposta data 05.01.2016 - 16:03
fonte
6

Sembra che inserire un valore null sia un errore ma hai paura di applicare questo errore all'inserimento perché non vuoi perdere i dati. Tuttavia, se un campo non deve essere nullo ma è, stai perdendo dati . Pertanto la soluzione migliore è garantire che i campi nulli non vengano erroneamente salvati in primo luogo.

A tal fine, fai in modo che i dati siano corretti nell'unico deposito permanente autorevole di quei dati, il database. Fatelo aggiungendo vincoli non nulli. Quindi il tuo codice potrebbe fallire, ma questi guasti ti avvisano immediatamente dei bug, permettendoti di correggere i problemi che già ti causano la perdita di dati. Ora che sei in grado di identificare facilmente i bug, prova il tuo codice e testalo due volte. Sarai in grado di correggere i bug che portano alla perdita di dati e, nel processo, semplificare notevolmente l'elaborazione a valle dei dati perché non dovrai preoccuparti dei valori nulli.

    
risposta data 05.01.2016 - 20:09
fonte
5

Riguardo a questa frase nella domanda:

This should "theoretically" not happen, so if it does it points to bad data or a bug in the code.

Ho sempre apprezzato questa citazione (per gentile concessione di questo articolo ):

I find it amusing when novice programmers believe their main job is preventing programs from crashing. I imagine this spectacular failure argument wouldn't be so appealing to such a programmer. More experienced programmers realize that correct code is great, code that crashes could use improvement, but incorrect code that doesn't crash is a horrible nightmare.

In sostanza: sembra che tu stia approvando la Legge di Postel , "sii prudente in ciò che invii, sii liberale in cosa accetti". In teoria, in pratica questo "principio di robustezza" porta a un software che è non robusto , almeno a lungo termine - e talvolta anche a breve termine . (Confronta il documento di Eric Allman Il principio di robustezza riconsiderato , che è un trattamento molto approfondito del soggetto, anche se per lo più incentrato sui casi d'uso del protocollo di rete.)

Se hai programmi che inseriscono erroneamente dati nel tuo database, quei programmi sono interrotti e devono essere corretti . La carta sul problema consente solo di continuare a peggiorare; questo è l'equivalente di ingegneria software di che consente a un drogato di continuare la loro dipendenza.

In termini pragmatici, tuttavia, a volte è necessario abilitare il comportamento "interrotto" per continuare, almeno temporaneamente, specialmente come parte di una transizione senza interruzioni da stato lento, interrotto a stato rigoroso e corretto. In tal caso, vuoi trovare un modo per consentire il corretto inserimento degli inserimenti, ma consentire comunque che l'archivio dati "canonico" sia sempre in uno stato corretto . Ci sono vari modi per farlo:

  • Utilizzare un trigger del database per convertire inserti malformati in inserti corretti, ad es. sostituendo i valori mancanti / nulli con i valori predefiniti
  • I programmi errati vengono inseriti in una tabella di database separata che può essere "errata" e hanno un processo programmato separato o un altro meccanismo che sposta i dati corretti da quella tabella nell'archivio dati canonico
  • Utilizzare il filtro sul lato query (ad esempio una vista) per garantire che i dati recuperati dal database siano sempre in uno stato corretto, anche se i dati a riposo non sono

Un modo per eludere tutti questi problemi è inserire un livello API che controlli tra i programmi che generano scritture e il database effettivo.

Sembra che parte del tuo problema sia che non conosci nemmeno tutti i posti che stanno generando scritture non corrette o che ce ne sono semplicemente troppi da aggiornare. È uno stato spaventoso in cui stare, ma non avrebbe mai dovuto sorgere in primo luogo.

Non appena ottieni più di una manciata di sistemi a cui è consentito modificare i dati in un archivio dati di produzione canonico, sarai nei guai: non c'è modo di mantenere centralmente qualsiasi cosa su quel database. Sarebbe meglio consentire il minor numero possibile di processi per l'emissione di scritture e utilizzarli come "gatekeeper" in grado di pre-elaborare i dati prima di inserirli se necessario. Il meccanismo esatto dipende molto dalla tua architettura specifica.

    
risposta data 05.01.2016 - 21:18
fonte
2

" Esistono buoni principi di architettura o di progettazione per gestire le voci null ma possibili nulle? "

Risposta semplice - sì.

ETL

Esegui alcune operazioni preliminari per garantire che i dati siano di qualità sufficiente per accedere al database. Qualsiasi cosa nel file di rilascio deve essere segnalata e qualsiasi dato pulito può essere caricato nel database.

Come qualcuno che è stato poacher (dev) e game keeper (DBA), so per esperienza amara che terze parti semplicemente non risolveranno i loro problemi di dati a meno che non siano obbligati a farlo. Costantemente piegarsi all'indietro e massaggiare i dati attraverso set un pericoloso precedente.

Mart / Repository

In questo scenario, i dati grezzi vengono inseriti nel DB del repository e quindi una versione sterilizzata viene trasferita al database DB a cui le applicazioni possono accedere.

Valori predefiniti

Se puoi applicare valori predefiniti sensibili alle colonne, dovresti comunque farlo funzionare se questo è un database esistente.

Fail early

Si è tentati di risolvere semplicemente i problemi relativi ai dati al gateway per l'applicazione, la suite di report, l'interfaccia, ecc. Vi consiglio caldamente di non fare affidamento solo su questo. Se si collegano altri widget al DB, si avranno di nuovo gli stessi problemi. Affronta i problemi di qualità dei dati.

    
risposta data 06.01.2016 - 13:29
fonte
1

Ogni volta che il tuo caso d'uso consente di sostituire NULL in modo sicuro con un buon valore predefinito, puoi eseguire la conversione nelle istruzioni SELECT Sql utilizzando ISNULL o COALESCE . Quindi invece di

 SELECT MyColumn FROM MyTable

si può scrivere

 SELECT ISNULL(MyColumn,DefaultValueForMyColumn) FROM MyTable

Ovviamente, funzionerà solo quando l'ORM consente di manipolare direttamente le istruzioni select, o di fornire modelli modificabili per la generazione. Si dovrebbe fare in modo che nessun errore "reale" sia mascherato in questo modo, quindi applicarlo solo se la sostituzione con un valore predefinito è esattamente ciò che si desidera in caso di NULL.

Se sei in grado di modificare il database e lo schema e il tuo sistema db lo supporta, puoi prendere in considerazione l'aggiunta di una clausola del valore predefinito alle colonne specifiche, come suggerito da @RobbieDee. Tuttavia, questo richiederà anche di modificare i dati esistenti nel database per rimuovere eventuali valori NULL precedentemente inseriti e rimuoverà la capacità di distinguere tra i dati di importazione corretti e incompleti in seguito.

Dalla mia esperienza personale, so che usare ISNULL può funzionare sorprendentemente bene - in passato ho dovuto mantenere un'applicazione legacy in cui gli sviluppatori originali si erano dimenticati di aggiungere i vincoli NOT NULL a molte colonne, e non potevamo aggiungere facilmente quei vincoli più tardi per alcune ragioni. Ma nel 99% dei casi, 0 come default per le colonne numeriche e la stringa vuota come predefinita per le colonne di testo era pienamente accettabile.

    
risposta data 05.01.2016 - 17:33
fonte
1

L'OP presuppone una risposta che accoppi le regole aziendali con i dettagli tecnici del database.

This should "theoretically" not happen, so if it does it points to bad data or a bug in the code. The errors have different severities, depending which field is null; i.e. for some fields the processing should be stopped and somebody notified, for others the processing should be allowed to continue and just notify somebody.

Questa è tutte le regole aziendali. Le regole aziendali non si preoccupano di null per-se. Per tutti sa che il database potrebbe avere null, 9999, "BOO!" ... È solo un altro valore. Che, in un RDBMS, null ha proprietà interessanti e usi unici è moot.

L'unica cosa che conta è cosa significhi "nullità" per gli oggetti di business forniti ...

Are there any good architecture or design principles to handle the rare but possible null entries?

Sì.

  • Inserisci le regole aziendali nelle classi.
  • La traslitterazione dovrebbe essere in uno strato di codice appropriato che disaccoppia le classi aziendali e l'archivio dati. Se non riesci a inserirlo nel codice ORM, almeno non inserirlo nel database.
  • Rendi il più stupido possibile il database, senza regole aziendali. Anche cose innocue come l'impostazione predefinita di un valore ti mordono . Ci sono stato.
  • Convalida i dati in entrata e in arrivo dal database. E naturalmente questo viene fatto w / nel contesto degli oggetti di business.

Lanciare un'eccezione sul recupero dei dati non ha senso.

La domanda è "dovrei memorizzare" dati non validi "? Dipende:

  • Potrebbero essere usati dati errati - Non salvare mai oggetti non validi o compositi oggetto. Dati complessi / relazioni commerciali ovunque. Gli utenti possono svolgere qualsiasi funzione in qualsiasi momento, eventualmente utilizzando quell'entità aziendale in un certo numero di contesti. L'effetto (se presente) dei dati non validi, nel momento in cui viene salvato, non è noto perché dipende in gran parte dall'uso futuro. Non esiste un processo unificato / singolo di tali dati.
  • Impossibile progredire se ci sono dati errati - Permetti il salvataggio di dati non validi. Tuttavia il prossimo passo in un processo non può continuare fino a quando tutto è valido. Ad esempio facendo le tasse sul reddito di una persona. Quando viene recuperato dal database, il software indica gli errori e non può essere inviato all'IRS senza controllo di validità.
risposta data 05.01.2016 - 21:26
fonte
0

Ci sono molti modi per gestire i null, quindi passeremo dal livello del database fino al livello dell'applicazione.

Livello database

Puoi vietare i null ; anche se qui non è pratico.

Puoi configurare un valore predefinito su base per colonna:

  • richiede che la colonna sia assente da insert , quindi non copre l'inserimento nullo esplicito
  • impedisce il rilevamento da righe in cui insert ha erroneamente perso questa colonna

Puoi configurare un trigger , in modo che al momento dell'inserimento i valori mancanti vengano calcolati automaticamente:

  • richiede che siano presenti le informazioni necessarie per eseguire questo calcolo
  • rallenterà il insert

Livello query

Puoi saltare le righe dove è presente un null scomodo:

  • semplifica la logica principale
  • impedisce di rilevare le "righe danneggiate", quindi sarebbe necessario un altro processo per controllarle
  • richiede che ogni query sia strumentata

Puoi fornire un valore predefinito nella query:

  • semplifica la logica principale
  • impedisce di rilevare le "righe danneggiate", quindi sarebbe necessario un altro processo per controllarle
  • richiede che ogni query sia strumentata

Nota: la strumentazione di ogni query non è necessariamente un problema se hai un modo automatico di generarli.

Livello applicazione

Puoi precontrollare la tabella per vietato null :

  • semplifica la logica principale
  • migliora il time-to-failure
  • richiede di mantenere la pre-verifica e la logica dell'applicazione coerente

Puoi interrompere l'elaborazione quando incontri una percentuale proibita% co_de:

  • evita di duplicare la conoscenza di quali colonne possono essere null e quali non possono
  • è ancora relativamente semplice (solo un controllo + ritorno / lancio)
  • richiede che il tuo processo sia ripristinabile (se hai già inviato una e-mail, non vuoi inviarla due volte, o un centinaio di volte!)

Puoi saltare la riga quando incontri un null proibito:

  • evita di duplicare la conoscenza di quali colonne possono essere null e quali non possono
  • è ancora relativamente semplice (solo un controllo + ritorno / lancio)
  • non richiede che il tuo processo sia ripristinabile

Puoi inviare una notifica quando incontri un null proibito, uno alla volta o per partita, che è complementare agli altri modi presentati sopra. Ciò che conta di più, tuttavia, è "cosa allora?", In particolare se ti aspetti che la riga venga riparata e necessiti di essere rielaborata potresti dover garantire di avere un modo per distinguere le righe già elaborate dalle righe che necessitano di essere rielaborato.

Data la tua situazione, vorrei gestire la situazione dell'applicazione e combinare:

  • interrompi e notifica
  • salta e notifica

Tenderei solo a saltare se possibile per garantire in qualche modo un minimo di progresso, specialmente se l'elaborazione può richiedere del tempo.

Se non è necessario rielaborare le righe saltate, è sufficiente che la registrazione sia sufficiente e un'e-mail inviata alla fine del processo con il numero di righe saltate sarà una notifica apt.

Altrimenti, userei una tabella laterale per le file da correggere (e rielaborare). Questo side-table può essere un semplice riferimento (senza chiave esterna) o una copia completa: quest'ultimo, anche se più costoso, è necessario se non si ha il tempo di indirizzare il null prima di dover ripulire il dati principali.

    
risposta data 05.01.2016 - 19:48
fonte
-1

I valori Null possono essere gestiti nella traduzione o nella mappatura dei tipi di database in tipi di lingue. Ad esempio in C #, ecco un metodo generico che gestisce null per te per qualsiasi tipo:

public static T Convert<T>(object obj)
        {
            if (obj == DBNull.Value)
            {
                return default(T);
            }

            return (T) obj;
        }

public static T Convert<T>(object obj, T defaultValue)
        {
            if (obj == DBNull.Value)
            {
                T t = defaultValue;
                return t;
            }

            return (T) obj;
        }

Oppure, se vuoi eseguire un'azione ...

 public static T Convert<T>(object obj, T defaultValue)
        {
            if (obj == DBNull.Value)
            {
                //Send an Alert, we might want pass in the name
                //of column or other details as well
                SendNullAlert();
                //Set it to default so we can keep processing
                T t = defaultValue;
                return t;
            }

            return (T) obj;
        }

E poi nella mappatura, in questo caso su un oggetto di tipo "Sample", gestiremo null per ognuna delle colonne:

public class SampleMapper : MapperBase<Sample>
    {
        private const string Id = "Id";
        private const string Name = "Name";
        private const string DataValue = "DataValue";
        private const string Created = "Created";

        protected override Sample Map(IDataRecord record)
        {
            return new Sample(
                Utility.Convert<Int64>(record[Id]),
                Utility.Convert<String>(record[Name]),
                Utility.Convert<Int32>(record[DataValue]),
                Utility.Convert<DateTime>(record[Created])
                );
        }
    }

Infine, tutte le classi di mappatura possono essere generate automaticamente in base alla query SQL o alle tabelle coinvolte, esaminando i tipi di dati SQL e traducendoli in tipi di dati specifici della lingua. Questo è ciò che molti ORM fanno automaticamente per te. Tieni presente che alcuni tipi di database potrebbero non avere una mappatura diretta (colonne geo-spaziali, ecc.) E potrebbero richiedere una gestione speciale.

    
risposta data 05.01.2016 - 16:47
fonte