Dovrei controllare se qualcosa esiste nel db e fallire velocemente o aspettare l'eccezione db

26

Con due classi:

public class Parent 
{
    public int Id { get; set; }
    public int ChildId { get; set; }
}

public class Child { ... }

Quando assegni ChildId a Parent dovrei controllare prima se esiste nel DB o aspettare che il DB lanci un'eccezione?

Ad esempio (utilizzando Entity Framework Core):

NOTA questi tipi di controlli sono ALL OVER THE INTERNET anche sui documenti ufficiali di Microsoft: link ma esiste un'ulteriore gestione delle eccezioni per SaveChanges

Inoltre, si noti che l'intento principale di questo controllo era di restituire il messaggio amichevole e lo stato HTTP noto all'utente dell'API e di non ignorare completamente le eccezioni del database. E l'unica eccezione del luogo che viene generata è all'interno di SaveChanges o SaveChangesAsync call ... quindi non ci saranno eccezioni quando chiami FindAsync o Any . Quindi, se il figlio esiste ma è stato eliminato prima di SaveChangesAsync , verrà lanciata un'eccezione di concorrenza.

L'ho fatto a causa del fatto che l'eccezione foreign key violation sarà molto più difficile da formattare per visualizzare "Child with id {parent.ChildId} non è stato trovato."

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    // is this code redundant?
   // NOTE: its probably better to use Any isntead of FindAsync because FindAsync selects *, and Any selects 1
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null)
       return NotFound($"Child with id {parent.ChildId} could not be found.");

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        

    return parent;
}

vs

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    _db.Parents.Add(parent);
    await _db.SaveChangesAsync();  // handle exception somewhere globally when child with the specified id doesn't exist...  

    return parent;
}

Il secondo esempio di Postgres genererà l'errore 23503 foreign_key_violation : link

Lo svantaggio di gestire le eccezioni in questo modo in ORM come EF è che funzionerà solo con un database specifico back-end. Se hai mai desiderato passare al server SQL o qualcos'altro, questo non funzionerà più perché il codice di errore cambierà.

Non formattare correttamente l'eccezione per l'utente finale potresti esporre alcune cose che non vuoi che siano gli sviluppatori a vedere.

Related:

link

link

link

    
posta Konrad 19.09.2018 - 12:08
fonte

5 risposte

3

Piuttosto una domanda confusa, ma dovresti controllare prima e non solo gestire un'eccezione DB.

Prima di tutto, nel tuo esempio sei al livello dati, usando EF direttamente sul database per eseguire SQL. Il tuo codice equivale all'esecuzione di

select * from children where id = x
//if no results, perform logic
insert into parents (blah)

L'alternativa che stai suggerendo è:

insert into parents (blah)
//if exception, perform logic

L'uso dell'eccezione per eseguire la logica condizionale è lento e universalmente disapprovato.

Hai una condizione di gara e dovresti usare una transazione. Ma questo può essere fatto completamente nel codice.

using (var transaction = new TransactionScope())
{
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null) 
    {
       return NotFound($"Child with id {parent.ChildId} could not be found.");
    }

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        
    transaction.Complete();

    return parent;
}

La cosa fondamentale è chiedersi:

"Do you expect this situation to occur?"

In caso contrario, quindi sicuro, inserire via e lanciare un'eccezione. Ma gestisci l'eccezione come qualsiasi altro errore che potrebbe verificarsi.

Se ti aspetti che si verifichi, allora NON è eccezionale e dovresti controllare se il bambino esiste per primo, rispondendo con il messaggio amichevole appropriato se non lo fa.

Modifica - Sembra che ci siano molte polemiche su questo. Prima di considerare downvote:

A. Cosa accadrebbe se ci fossero due vincoli FK. Vorresti sostenere l'analisi del messaggio di eccezione per capire quale oggetto mancava?

B. Se si dispone di una mancanza, viene eseguita una sola istruzione SQL. Sono solo i risultati che comportano la spesa aggiuntiva di una seconda query.

C. Di solito Id sarebbe una chiave surrogata, È difficile immaginare una situazione in cui si sa che e non si è abbastanza sicuri che sia sul DB. Il controllo sarebbe strano. Ma cosa succede se è una chiave naturale che l'utente ha digitato? Ciò potrebbe avere un'alta probabilità di non essere presente

    
risposta data 20.09.2018 - 18:08
fonte
105

Controllare l'unicità e quindi impostare è un antipattern; può sempre accadere che l'ID venga inserito contemporaneamente tra il tempo di controllo e il tempo di scrittura. I database sono attrezzati per affrontare questo problema attraverso meccanismi come vincoli e transazioni; la maggior parte dei linguaggi di programmazione non lo sono. Pertanto, se apprezzi la consistenza dei dati, lasciali all'esperto (il database), cioè fai l'inserimento e cattura un'eccezione se si verifica.

    
risposta data 19.09.2018 - 12:13
fonte
36

Penso che quello che chiami "fail fast" e quello che io chiamo non sia lo stesso.

Dicendo al database di apportare una modifica e gestire l'errore, che è veloce. La tua strada è complicata, lenta e non particolarmente affidabile.

Questa tua tecnica non fallisce in fretta, è "preflight". A volte ci sono buone ragioni, ma non quando si utilizza un database.

    
risposta data 19.09.2018 - 12:59
fonte
14

Questo è iniziato come un commento ma è diventato troppo grande.

No, come le altre risposte hanno dichiarato, questo modello non dovrebbe essere usato. *

Quando si ha a che fare con sistemi che usano componenti asincroni, ci sarà sempre una condizione di competizione in cui il database (o il file system o altro sistema asincrono) potrebbe cambiare tra il controllo e la modifica. Un controllo di questo tipo non è semplicemente un modo affidabile per prevenire il tipo di errore che non si vuole gestire.
Peggio del non essere sufficiente, a colpo d'occhio dà l'impressione che dovrebbe impedire che l'errore di record duplicato dia un falso senso di sicurezza.

Hai comunque bisogno della gestione degli errori.

Nei commenti hai chiesto cosa succede se hai bisogno di dati da più fonti.
Ancora No.

Il problema fondamentale non scompare se ciò che si desidera controllare diventa più complesso.

Hai comunque bisogno della gestione degli errori comunque.

Anche se questo controllo fosse un modo affidabile per prevenire il particolare errore che stai cercando di evitare, altri errori possono ancora verificarsi. Cosa succede se si perde la connessione al database o si esaurisce lo spazio o?

Molto probabilmente hai ancora bisogno di gestire gli errori altri del database in ogni caso. La gestione di questo particolare errore dovrebbe probabilmente essere una piccola parte di esso.

Se hai bisogno di dati per determinare cosa cambiare, ovviamente dovrai raccoglierlo da qualche parte. (a seconda degli strumenti che stai usando ci sono probabilmente modi migliori rispetto a query separate per raccoglierlo) Se, esaminando i dati raccolti, si determina che non è necessario apportare la modifica dopo tutto, ottimo, non fare il modificare. Questa determinazione è completamente separata dai problemi di gestione degli errori.

Hai comunque bisogno della gestione degli errori.

So di essere ripetitivo, ma ritengo sia importante chiarirlo. Ho ripulito questo casino prima.

Alla fine fallirà. Quando fallisce sarà difficile e richiede tempo per arrivare fino in fondo. Risolvere i problemi che sorgono dalle condizioni di gara è difficile. Non accadono in modo coerente, quindi sarà difficile o addirittura impossibile da riprodurre in isolamento. Per prima cosa non hai inserito la corretta gestione degli errori, quindi probabilmente non avrai molto da fare: forse la relazione di un utente finale di un testo criptico (che in effetti cercavi di evitare di vedere in primo luogo.) Forse una traccia di stack che rimanda a quella funzione che quando la guardi nega apertamente che l'errore dovrebbe essere possibile.

* Potrebbero esserci validi motivi commerciali per eseguire questi controlli, in modo da evitare che l'applicazione possa duplicare lavori costosi, ma non è un sostituto adatto per la corretta gestione degli errori.

    
risposta data 20.09.2018 - 00:08
fonte
2

Penso che una cosa secondaria da notare qui: uno dei motivi per cui lo vuoi è che tu possa formattare un messaggio di errore che l'utente possa vedere.

Ti raccomando vivamente di:

a) mostra all'utente finale lo stesso messaggio di errore generico per l'errore ogni che si verifica.

b) registra l'eccezione effettiva da qualche parte a cui solo gli sviluppatori possono accedere (se su un server) o da qualche parte che può essere inviata dagli strumenti di segnalazione degli errori (se il client è distribuito)

c) non tentare di formattare i dettagli delle eccezioni di errore registrati, a meno che non sia possibile aggiungere ulteriori informazioni utili. Non vuoi aver "formattato" accidentalmente l'unica informazione utile che avresti potuto utilizzare per rintracciare un problema.

In breve - le eccezioni sono piene di informazioni tecniche molto utili. Nulla di ciò dovrebbe essere per l'utente finale e tu perdi queste informazioni a tuo rischio e pericolo.

    
risposta data 20.09.2018 - 17:37
fonte

Leggi altre domande sui tag