c# - practices - transactionscope rollback
TransactionScope y transacciones (6)
Debes usar una captura de prueba
BEGIN TRANSACTION --SqlTransaction
BEGIN TRY
SELECT 1/0
COMMIT TRANSACTION --SqlTransaction
RETURN 0
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION --SqlTransaction
RETURN -1
END CATCH
Y esta pregunta debe responder su pregunta sobre TransactionScope y Rollbacks ¿Cómo hace TransactionScope para revertir las transacciones?
En mi código C # estoy usando TransactionScope porque me dijeron que no confiara en que mis programadores de SQL siempre usarán las transacciones y somos responsables y yada yada.
Una vez dicho esto
Parece que el objeto TransactionScope retrocede antes de la Transacción Sql? ¿Es eso posible y, de ser así, cuál es la metodología correcta para envolver un TransactionScope en una transacción?
Aquí está la prueba de SQL
CREATE PROC ThrowError
AS
BEGIN TRANSACTION --SqlTransaction
SELECT 1/0
IF @@ERROR<> 0
BEGIN
ROLLBACK TRANSACTION --SqlTransaction
RETURN -1
END
ELSE
BEGIN
COMMIT TRANSACTION --SqlTransaction
RETURN 0
END
go
DECLARE @RESULT INT
EXEC @RESULT = ThrowError
SELECT @RESULT
Y si ejecuto esto obtengo la división por 0 y devuelvo -1
Llamar desde el código C # me aparece un mensaje de error adicional
Ocurrió un error de división por cero.
El recuento de transacciones después de EJECUTAR indica que falta una instrucción de COMPROMISO o TRANSACCIÓN DE RODILLO. Recuento anterior = 1, recuento actual = 0.
Si le doy un nombre a la transacción sql
No se puede revertir SqlTransaction. No se encontró ninguna transacción o punto de salvaguarda de ese nombre. El recuento de transacciones después de EXECUTE indica que falta una instrucción COMMIT o ROLLBACK TRANSACTION. Recuento anterior = 1, recuento actual = 2.
Algunas veces parece que el conteo sube, hasta que la aplicación sale por completo.
El c # es solo
using (TransactionScope scope = new TransactionScope())
{
... Execute Sql
scope.Commit()
}
EDITAR:
El código sql tiene que funcionar para 2000 y 2005.
Hubo una actualización masiva del manejo de errores en SQL Server 2005. Estos artículos son bastante extensos: manejo de errores en SQL 2005 y versiones posteriores por Erland Sommarskog y manejo de errores en SQL 2000 - un fondo por Erland Sommarskog
La mejor manera es algo como esto:
Crea tu procedimiento almacenado como:
CREATE PROCEDURE YourProcedure
AS
BEGIN TRY
BEGIN TRANSACTION --SqlTransaction
DECLARE @ReturnValue int
SET @ReturnValue=NULL
IF (DAY(GETDATE())=1 --logical error
BEGIN
SET @ReturnValue=5
RAISERROR(''Error, first day of the month!'',16,1) --send control to the BEGIN CATCH block
END
SELECT 1/0 --actual hard error
COMMIT TRANSACTION --SqlTransaction
RETURN 0
END TRY
BEGIN CATCH
IF XACT_STATE()!=0
BEGIN
ROLLBACK TRANSACTION --only rollback if a transaction is in progress
END
--will echo back the complete original error message to the caller
--comment out if not needed
DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
SELECT @ErrorMessage = N''Error %d, Line %d, Message: ''+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
RETURN ISNULL(@ReturnValue,1)
END CATCH
GO
Sin embargo, eso es solo para SQL Server 2005 y superiores. Sin usar los bloques TRY-CATCH en SQL Server 2005, le resulta muy difícil eliminar todos los mensajes que envía SQL Server. Los extra messages
se refiere están causados por la forma en que se manejan las reversiones usando @@ trancount:
de http://www.sommarskog.se/error-handling-I.html#trancount
@@ trancount es una variable global que refleja el nivel de transacciones anidadas. Cada COMIENZO DE TRANSACCIÓN aumenta @@ trancount en 1, y cada COMPROMISO DE TRANSACCIÓN disminuye @@ trancount en 1. En realidad, nada se compromete hasta que @@ trancount llega a 0. ROLLBACK TRANSACTION hace retroceder todo a lo más externo. COMIENZE A TRANSACCIÓN (a menos que haya usado el bastante exótico GUARDAR TRANSACCIÓN), y obliga a @@ trancount a 0, respecto del valor anterior.
Cuando sale de un procedimiento almacenado, si @@ trancount no tiene el mismo valor que tenía cuando se inició el procedimiento, SQL Server genera el error 266. Sin embargo, este error no se genera si el procedimiento se llama directamente desde un disparador. o indirectamente. Tampoco se genera si está ejecutando SET SET IMPLICIT TRANSACTIONS ON
Si no desea que la advertencia sobre el recuento de transacciones no coincida, solo necesita tener una transacción abierta a la vez. Haces esto creando todo tu procedimiento como este:
CREATE PROC YourProcedure
AS
DECLARE @SelfTransaction char(1)
SET @SelfTransaction=''N''
IF @@trancount=0
BEGIN
SET @SelfTransaction=''Y''
BEGIN TRANSACTION --SqlTransaction
END
SELECT 1/0
IF @@ERROR<> 0
BEGIN
IF @SelfTransaction=''Y''
BEGIN
ROLLBACK TRANSACTION --SqlTransaction
END
RETURN -1
END
ELSE
BEGIN
IF @SelfTransaction=''Y''
BEGIN
COMMIT TRANSACTION --SqlTransaction
END
RETURN 0
END
GO
Al hacer esto, solo emite los comandos de transacción si aún no está en una transacción. Si codifica todos sus procedimientos de esta manera, solo el procedimiento o el código C # que emite BEGIN TRANSACTION emitirá realmente COMMIT / ROLLBACK y los recuentos de transacciones siempre coincidirán (no obtendrá un error).
en C # de la documentación de la clase TransactionScope :
static public int CreateTransactionScope(
string connectString1, string connectString2,
string commandText1, string commandText2)
{
// Initialize the return value to zero and create a StringWriter to display results.
int returnValue = 0;
System.IO.StringWriter writer = new System.IO.StringWriter();
try
{
// Create the TransactionScope to execute the commands, guaranteeing
// that both commands can commit or roll back as a single unit of work.
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection1 = new SqlConnection(connectString1))
{
// Opening the connection automatically enlists it in the
// TransactionScope as a lightweight transaction.
connection1.Open();
// Create the SqlCommand object and execute the first command.
SqlCommand command1 = new SqlCommand(commandText1, connection1);
returnValue = command1.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command1: {0}", returnValue);
// If you get here, this means that command1 succeeded. By nesting
// the using block for connection2 inside that of connection1, you
// conserve server and network resources as connection2 is opened
// only when there is a chance that the transaction can commit.
using (SqlConnection connection2 = new SqlConnection(connectString2))
{
// The transaction is escalated to a full distributed
// transaction when connection2 is opened.
connection2.Open();
// Execute the second command in the second database.
returnValue = 0;
SqlCommand command2 = new SqlCommand(commandText2, connection2);
returnValue = command2.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
}
}
// The Complete method commits the transaction. If an exception has been thrown,
// Complete is not called and the transaction is rolled back.
scope.Complete();
}
}
catch (TransactionAbortedException ex)
{
writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message);
}
catch (ApplicationException ex)
{
writer.WriteLine("ApplicationException Message: {0}", ex.Message);
}
// Display messages.
Console.WriteLine(writer.ToString());
return returnValue;
}
Solo un pensamiento, pero es posible que pueda usar la captura TransactionAbortedException
para obtener el error real e ignorar la advertencia de falta de coincidencia en el recuento de transacciones.
No utilice transacciones tanto en su código C # como en los datos. Uno es suficiente. Que casi siempre debería ser su código C #, solo él sabe qué conjunto de actualizaciones de la base de datos debe rechazarse o confirmarse en su totalidad.
Sé que esta es una sugerencia increíblemente mundana, pero ¿no sería una buena solución prevenir la división por cero en primer lugar? Casi todas las operaciones DML (insertar, seleccionar, actualizar) se pueden reescribir para evitar dividir por ceros mediante el uso de las declaraciones CASE.
Si tiene que admitir SQL Server 2000, use TransactionScope para hacer su vida más fácil. Sin embargo, vea en la parte inferior por qué tiene limitaciones.
El manejo de errores de SQL antes de TRY / CATCH es errático. El artículo de Erland publicado por KM explica los errores de aborto de declaración / alcance / lote que lo hacen así. Básicamente, el código puede dejar de ejecutarse y te quedan bloqueos en las filas, etc.
Esto es lo que sucede anteriormente, por lo que su reversión no se ejecuta, por lo que recibe el error 226 sobre los recuentos de transacciones.
Si solo es compatible con SQL Server 2005+, use TRY / CATCH para detectar todos los errores y también use SET XACT_ABORT ON. TRY / CATCH hace que SQL Server sea mucho más resistente y atrapa todos los errores de tiempo de ejecución. SET XACT_ABORT ON también suprime el error 226 porque emite la reversión automáticamente y garantiza que se liberen todos los bloqueos.
Por cierto:
SELECT 1/0 es un excelente ejemplo de por qué debería usar el manejo de errores de SQL.
Use un DataAdapter para llenar
- un Datatable de un proceso almacenado con SELECT 1/0 -> sin error atrapado
- un DataSet de un proceso almacenado con SELECT 1/0 -> error atrapado
SQL TRY / CATCH tratará con esto ...
public string ExecuteReader(string SqlText)
{
SqlCommand cmd;
string retrunValue = "";
try
{
c.Open();
cmd = new SqlCommand();
cmd.CommandType = CommandType.Text;
cmd.Connection = c;
cmd.CommandText = SqlText;
retrunValue = Convert.ToString(cmd.ExecuteScalar());
c.Close();
}
catch (Exception SqlExc)
{
c.Close();
throw SqlExc;
}
return (retrunValue);
}