Una singola SqlException
(può) esegue il wrapping di più errori di SQL Server. Puoi scorrere tra di loro con Errors
proprietà. Ogni errore è SqlError
:
foreach (SqlError error in exception.Errors)
Ogni SqlError
ha una Class
proprietà che puoi utilizzare per determinare approssimativamente se puoi riprovare o meno (e nel caso in cui riprovi se devi ricreare anche la connessione). Da MSDN:
Class
<10 è per errori nelle informazioni che hai passato, quindi (probabilmente) non puoi riprovare se prima non correggi gli input.Class
da 11 a 16 sono "generati dall'utente", quindi probabilmente non puoi fare nulla se l'utente prima non corregge i suoi input. Tieni presente che la classe 16 include molti temporanei errori e la classe 13 è per i deadlock (grazie a EvZ), quindi puoi escludere queste classi se le gestisci una per una.Class
da 17 a 24 sono errori hardware/software generici e puoi riprovare. QuandoClass
è 20 o superiore devi ricreare la connessione anche. 22 e 23 possono essere gravi errori hardware/software, 24 indica un errore del supporto (qualcosa che l'utente dovrebbe essere avvisato ma puoi riprovare nel caso fosse solo un errore "temporaneo").
Puoi trovare una descrizione più dettagliata di ogni classe qui.
In generale se gestisci gli errori con la loro classe non avrai bisogno di conoscere esattamente ogni errore (usando error.Number
proprietà o exception.Number
che è solo una scorciatoia per il primo SqlError
in quella lista). Questo ha lo svantaggio che puoi riprovare quando non è utile (o l'errore non può essere recuperato). Suggerirei un approccio in due passaggi :
- Controlla i codici di errore noti (elenca i codici di errore con
SELECT * FROM master.sys.messages
) per vedere cosa vuoi gestire (sapere come). Quella vista contiene messaggi in tutte le lingue supportate, quindi potrebbe essere necessario filtrarli permsglangid
colonna (ad esempio 1033 per l'inglese). - Per tutto il resto affidati alla classe di errore, riprova quando
Class
è 13 o superiore a 16 (e riconnessione se 20 o superiore). - Gli errori con gravità superiore a 21 (22, 23 e 24) sono errori gravi e poche attese non risolveranno i problemi (anche il database stesso potrebbe essere danneggiato).
Una parola sulle classi superiori. Come gestire questi errori non è semplice e dipende da molti fattori (inclusa la gestione del rischio per la tua candidatura). Come primo semplice passaggio, non riproverei per 22, 23 e 24 quando tento di eseguire un'operazione di scrittura:se il database, il file system o il supporto sono gravemente danneggiati, la scrittura di nuovi dati può deteriorare ulteriormente l'integrità dei dati (SQL Server è estremamente attento a non compromettere DB per una query anche in circostanze critiche). Un server danneggiato, a seconda dell'architettura di rete del database, potrebbe anche essere sostituito a caldo (automaticamente, dopo un determinato periodo di tempo o quando viene attivato un trigger specifico). Consulta e lavora sempre vicino al tuo DBA.
La strategia per riprovare dipende dall'errore che stai gestendo:risorse libere, attendere il completamento di un'operazione in sospeso, intraprendere un'azione alternativa, ecc. In generale dovresti riprovare solo se tutte gli errori sono "riprovabili":
bool rebuildConnection = true; // First try connection must be open
for (int i=0; i < MaximumNumberOfRetries; ++i) {
try {
// (Re)Create connection to SQL Server
if (rebuildConnection) {
if (connection != null)
connection.Dispose();
// Create connection and open it...
}
// Perform your task
// No exceptions, task has been completed
break;
}
catch (SqlException e) {
if (e.Errors.Cast<SqlError>().All(x => CanRetry(x))) {
// What to do? Handle that here, also checking Number property.
// For Class < 20 you may simply Thread.Sleep(DelayOnError);
rebuildConnection = e.Errors
.Cast<SqlError>()
.Any(x => x.Class >= 20);
continue;
}
throw;
}
}
Avvolgi tutto in try
/finally
per smaltire correttamente la connessione. Con questo CanRetry()
semplice, falso e ingenuo funzione:
private static readonly int[] RetriableClasses = { 13, 16, 17, 18, 19, 20, 21, 22, 24 };
private static bool CanRetry(SqlError error) {
// Use this switch if you want to handle only well-known errors,
// remove it if you want to always retry. A "blacklist" approach may
// also work: return false when you're sure you can't recover from one
// error and rely on Class for anything else.
switch (error.Number) {
// Handle well-known error codes,
}
// Handle unknown errors with severity 21 or less. 22 or more
// indicates a serious error that need to be manually fixed.
// 24 indicates media errors. They're serious errors (that should
// be also notified) but we may retry...
return RetriableClasses.Contains(error.Class); // LINQ...
}
Alcuni modi piuttosto complicati per trovare l'elenco degli errori non critici qui.
Di solito incorporo tutto questo codice (boilerplate) in un metodo (dove posso nascondere tutte le cose sporche fatto per creare/eliminare/ricreare la connessione) con questa firma:
public static void Try(
Func<SqlConnection> connectionFactory,
Action<SqlCommand> performer);
Da usare in questo modo:
Try(
() => new SqlConnection(connectionString),
cmd => {
cmd.CommandText = "SELECT * FROM master.sys.messages";
using (var reader = cmd.ExecuteReader()) {
// Do stuff
}
});
Tieni presente che lo scheletro (riprova in caso di errore) può essere utilizzato anche quando non stai lavorando con SQL Server (in realtà può essere utilizzato per molte altre operazioni come I/O e cose relative alla rete, quindi suggerirei di scrivere una funzione generale e di riutilizzarlo ampiamente).