tipos - transacciones en procedimientos almacenados sql server
¿Es una mejor práctica llamar explícitamente la reversión de transacción o permitir que una excepción active una reversión implícita? (3)
Ex. Si busca ejemplos de MSND en temas similares, como TransactionScope
, todos están a favor de la reversión implícita. Hay varias razones para eso, pero solo le daré una muy simple: cuando capte la excepción, es posible que la transacción ya haya retrocedido. Muchos errores retrotraen la transacción pendiente y luego devuelven el control al cliente, donde ADO.Net genera CLR SqlException después de que la transacción ya se haya retrotraído al servidor (1205 DEADLOCK es el ejemplo típico de dicho error), por lo que el Rollback()
llamada a Rollback()
es, en el mejor de los casos, no operativa y, como mucho, un error. El proveedor de la DbTransaction
(por ejemplo, SqlTransaction
) debe saber cómo manejar este caso, por ej. porque hay un chat explícito entre el servidor y el cliente notificando el hecho de que la transacción ya se ha revertido, y el método Dispose()
hace lo correcto.
Una segunda razón es que las transacciones pueden anidarse, pero la semántica de ROLLBACK es que una reversión revierte todas las transacciones, por lo que solo debe llamarla una vez (a diferencia de Commit()
que solo compromete la transacción interna y debe llamarse emparejado para cada comienzo). De nuevo, Dispose()
hace lo correcto.
Actualizar
El ejemplo de MSDN para SqlConnection.BeginTransaction()
realmente favorece el segundo formulario y realiza un Rollback()
explícito en el bloque catch
. Sospecho que el escritor técnico simplemente tenía la intención de mostrar en una sola muestra tanto Rollback()
como Commit()
, notar cómo necesitaba agregar un segundo bloque try / catch alrededor del Rollback
para eludir exactamente algunos de los problemas que mencioné originalmente.
En el siguiente código, si se produce una excepción al ejecutar las sentencias SQL, debería esperarse una reversión implícita en la transacción, ya que la transacción no se confirmó, queda fuera del alcance y se descarta:
using (DbTransaction tran = conn.BeginTransaction())
{
//
// Execute SQL statements here...
//
tran.Commit();
}
¿Es lo anterior una práctica aceptable, o debería captar la excepción y hacer una llamada explícita a tran.Rollback () como se muestra a continuación?
using (DbTransaction tran = conn.BeginTransaction())
{
try
{
//
// Execute SQL statements here...
//
tran.Commit();
}
catch
{
tran.Rollback();
throw;
}
}
Tiendo a estar de acuerdo con la reversión "implícita" basada en vías de excepción. Pero, obviamente, eso depende de dónde se encuentre en la pila y de lo que intenta hacer (es decir, si la clase DBTranscation capta la excepción y realiza la limpieza, o ¿no está "cometiendo" pasivamente?).
Aquí hay un caso donde el manejo implícito tiene sentido (tal vez):
static T WithTranaction<T>(this SqlConnection con, Func<T> do) {
using (var txn = con.BeginTransaction()) {
return do();
}
}
Pero, si la API es diferente, el manejo de la confirmación podría ser también (concedido, esto:
static T WithTranaction<T>(this SqlConnection con, Func<T> do,
Action<SqlTransaction> success = null, Action<SqlTransaction> failure = null)
{
using (var txn = con.BeginTransaction()) {
try {
T t = do();
success(txn); // does it matter if the callback commits?
return t;
} catch (Exception e) {
failure(txn); // does it matter if the callback rolls-back or commits?
// throw new Exception("Doh!", e); // kills the transaction for certain
// return default(T); // if not throwing, we need to do something (bogus)
}
}
}
No puedo pensar en demasiados casos en los que el repliegue explícito sea el enfoque correcto, excepto cuando exista una política estricta de control de cambios que se aplique. Pero, de nuevo, soy un poco lento en la adopción.
Puedes ir de cualquier manera, siendo el primero más conciso, el último siendo más revelador.
Una advertencia con el primer enfoque sería que la invocación de RollBack
en la eliminación de la transacción depende de la implementación específica del controlador. Con suerte, casi todos los conectores .NET lo hacen. SqlTransaction
hace:
private void Dispose(bool disposing)
{
Bid.PoolerTrace("<sc.SqlInteralTransaction.Dispose|RES|CPOOL> %d#, Disposing/n", this.ObjectID);
if (disposing && (this._innerConnection != null))
{
this._disposing = true;
this.Rollback();
}
}
MySQL:
protected override void Dispose(bool disposing)
{
if ((conn != null && conn.State == ConnectionState.Open || conn.SoftClosed) && open)
Rollback();
base.Dispose(disposing);
}
Una advertencia con el segundo enfoque es que no es seguro llamar a RollBack
sin otro try-catch
. Esto se establece explícitamente en la documentación .
En resumen, ¿qué es mejor? Depende del controlador, pero por lo general es mejor ir por la primera, por las razones mencionadas por Remus.
Además, consulte ¿Qué ocurre con una transacción no confirmada cuando la conexión está cerrada? para saber cómo se compromete la eliminación de la conexión y se revierten.