Concurrencia optimista
En un entorno multiusuario hay dos modelos para actualizar datos en una base de datos: concurrencia optimista y concurrencia pesimista. El objeto DataSet está diseñado para fomentar el uso de la concurrencia optimista en actividades cuya ejecución tiene una larga duración, como cuando se trabaja con interacción remota y cuando los usuarios interactúan con datos.
La concurrencia pesimista implica bloquear filas en el origen de datos para impedir que los usuarios modifiquen los datos de tal forma que otros usuarios resulten afectados. En un modelo pesimista, cuando un usuario realiza una acción que hace que se aplique un bloqueo, otros usuarios no pueden realizar acciones que entrarían en conflicto con el bloqueo hasta que el propietario del bloqueo lo libere. Este modelo se utiliza principalmente en aquellos entornos en los que hay mucha contención de datos, donde el costo de proteger los datos con bloqueos es menor que el costo de deshacer transacciones si se producen conflictos de concurrencia.
Por tanto, en un modelo de concurrencia pesimista, un usuario que lee una fila con intención de modificarla establece un bloqueo. Hasta que el usuario no haya terminado la actualización y liberado el bloqueo, nadie más podrá modificar dicha fila. Por este motivo, la concurrencia pesimista resulta más adecuada cuando los tiempos de bloqueo son cortos, como ocurre en el procesamiento de registros mediante programación. La concurrencia pesimista no es una opción escalable cuando los usuarios interactúan con los datos, lo que hace que los registros queden bloqueados durante períodos de tiempo relativamente largos.
Por el contrario, los usuarios que utilizan la concurrencia optimista no bloquean una fila cuando la leen. Cuando un usuario desea actualizar una fila, la aplicación debe determinar si otro usuario la ha modificado o no desde que se leyó. La concurrencia optimista suele utilizarse en entornos con poca contención de datos. Esto mejora el rendimiento porque no es necesario bloquear registros, a la vez que el bloqueo de registros requiere recursos adicionales del servidor. Además, para mantener bloqueos de registros es necesaria una conexión persistente con el servidor de base de datos. Como éste no es el caso en un modelo de concurrencia optimista, las conexiones con el servidor pueden atender a un mayor número de clientes en menos tiempo.
En un modelo de concurrencia optimista, se considera que ha habido una infracción si, después de que un usuario recibe un valor de la base de datos, otro usuario modifica el valor antes de que el primer usuario haya intentado modificarlo.
Las siguientes tablas muestran un ejemplo de concurrencia optimista.
A la 1:00 p.m., el Usuario1 lee una fila de la base de datos con los valores siguientes:
IdCliente Apellido Nombre
101 Martínez Cris
Nombre de columna | Valor original | Valor actual | Valor en la base de datos |
---|---|---|---|
IdCliente | 101 | 101 | 101 |
Apellido | Martínez | Martínez | Martínez |
Nombre | Cris | Cris | Cris |
A la 1:01 p.m., el Usuario2 lee la misma fila.
A la 1:03 p.m., el Usuario2 cambia Nombre de "Cris" a "Cristina" y actualiza la base de datos.
Nombre de columna | Valor original | Valor actual | Valor en la base de datos |
---|---|---|---|
IdCliente | 101 | 101 | 101 |
Apellido | Martínez | Martínez | Martínez |
Nombre | Cris | Cristina | Cris |
La actualización se realiza correctamente porque los valores contenidos en la base de datos en el momento de la actualización coinciden con los valores originales que tiene el Usuario2.
A la 1:05 p.m., el Usuario1 cambia el nombre de Cris a "Jaime" e intenta actualizar la fila.
Nombre de columna | Valor original | Valor actual | Valor en la base de datos |
---|---|---|---|
IdCliente | 101 | 101 | 101 |
Apellido | Martínez | Martínez | Martínez |
Nombre | Cris | Jaime | Cristina |
En este momento, el Usuario1 encuentra una infracción de la concurrencia optimista porque los valores de la base de datos ya no coinciden con los valores originales que esperaba el Usuario1. Ahora hay que tomar la decisión de sobrescribir los cambios realizados por el Usuario2 con los efectuados por el Usuario1 o cancelar los cambios del Usuario1.
Probar si hay infracciones de la concurrencia optimista
Existen varias técnicas para probar si se ha producido una infracción de la concurrencia optimista. Una de ellas consiste en incluir una columna de marca de tiempo en la tabla. Las bases de datos suelen ofrecer funcionalidad de marca de tiempo que puede utilizarse para identificar la fecha y la hora en que se actualizó el registro por última vez. Mediante esta técnica se incluye una columna de marca de tiempo en la definición de la tabla. Siempre que se actualiza el registro se actualiza la marca de tiempo de manera que queden reflejadas la fecha y la hora actuales. Al hacer una prueba para ver si hay infracciones de la concurrencia optimista, la columna de marca de tiempo se devuelve con cualquier consulta del contenido de la tabla. Cuando se intenta realizar una actualización, se compara el valor de marca de tiempo de la base de datos con el valor de marca de tiempo contenido en la fila modificada. Si coinciden, se realiza la actualización y se actualiza la columna de marca de tiempo con la hora actual con el fin de reflejar la actualización. Si no coinciden, se ha producido una infracción de la concurrencia optimista.
Otra técnica para probar si hay alguna infracción relacionada con la concurrencia optimista consiste en comprobar que todos los valores de columna originales de una fila siguen coincidiendo con los existentes en la base de datos. Por ejemplo, observe la siguiente consulta:
SELECT Col1, Col2, Col3 FROM Table1
Para probar si hay alguna infracción relacionada con la concurrencia optimista al actualizar una fila de Tabla1, se utilizaría la siguiente instrucción UPDATE:
UPDATE Table1 Set Col1 = @NewCol1Value,
Set Col2 = @NewCol2Value,
Set Col3 = @NewCol3Value
WHERE Col1 = @OldCol1Value AND
Col2 = @OldCol2Value AND
Col3 = @OldCol3Value
La actualización se realizará siempre y cuando los valores originales coincidan con los valores de la base de datos. Si se ha modificado algún valor, la actualización no modificará la fila porque la cláusula WHERE no encontrará ninguna coincidencia.
Se recomienda devolver siempre un valor de clave principal único en la consulta. De lo contrario, la instrucción UPDATE anterior puede actualizar más de una fila, lo que quizás no sea su intención.
Si una columna del origen de datos admite valores nulos, quizás sea necesario extender la cláusula WHERE para comprobar si hay alguna referencia nula coincidente en la tabla local y en el origen de datos. Por ejemplo, la siguiente instrucción UPDATE comprueba que una referencia nula de la fila local sigue coincidiendo con una referencia nula del origen de datos o que el valor de la fila local sigue coincidiendo con el valor del origen de datos.
UPDATE Table1 Set Col1 = @NewVal1
WHERE (@OldVal1 IS NULL AND Col1 IS NULL) OR Col1 = @OldVal1
También se puede decidir la aplicación de criterios menos restrictivos al utilizar un modelo de concurrencia optimista. Por ejemplo, si sólo se utilizan las columnas de clave principal en la cláusula WHERE se sobrescribirán los datos, independientemente de que las otras columnas se hayan actualizado o no desde la última consulta. También se puede aplicar una cláusula WHERE sólo a determinadas columnas, lo que hará que se sobrescriban los datos a menos que se hayan actualizado ciertos campos desde que se consultaron por última vez.
Evento DataAdapter.RowUpdated
El evento DataAdapter.RowUpdated puede utilizarse junto con las técnicas descritas anteriormente para informar a la aplicación de las infracciones de la concurrencia optimista. RowUpdated se produce después de cada intento de actualizar una fila Modified de un DataSet. Esto permite agregar código especial de control, incluyendo el procesamiento cuando se produce una excepción, agregar información de error personalizada, agregar lógica de reintento, etc. El objeto RowUpdatedEventArgs devuelve una propiedad RecordsAffected con el número de filas afectadas por un determinado comando de actualización para una fila modificada de una tabla. Si se establece que el comando de actualización compruebe la concurrencia optimista, la propiedad RecordsAffected devolverá un valor 0 cuando se haya producido una infracción en la concurrencia optimista, ya que no se actualizó ningún registro. En tal caso se inicia una excepción. El evento RowUpdated permite controlar este caso y evitar la excepción al establecer un valor RowUpdatedEventArgs.Status apropiado, como UpdateStatus.SkipCurrentRow. Para obtener más información acerca del evento RowUpdated, vea Trabajar con eventos DataAdapter.
De forma opcional, es posible establecer DataAdapter.ContinueUpdateOnError como true antes de llamar a Update y responder a la información de error almacenada en la propiedad RowError de una fila determinada cuando Update termine. Para obtener más información, vea Agregar y leer información de error de fila.
Ejemplo de concurrencia optimista
A continuación se muestra un ejemplo sencillo que establece UpdateCommand de DataAdapter para probar la concurrencia optimista y, a continuación, utiliza el evento RowUpdated para probar si hay infracciones relacionadas con la concurrencia optimista. Cuando se encuentra una infracción de la concurrencia optimista, la aplicación establece el RowError de la fila para la que se emitió la actualización con el fin de reflejar la existencia de una infracción de la concurrencia optimista.
Hay que tener en cuenta que los valores de los parámetros pasados a la cláusula WHERE del comando UPDATE se asignan a los valores Original de sus respectivas columnas.
Dim nwindConn As SqlConnection = New SqlConnection("Data Source=localhost;Integrated Security=SSPI;Initial Catalog=northwind")
Dim custDA As SqlDataAdapter = New SqlDataAdapter("SELECT CustomerID, CompanyName FROM Customers ORDER BY CustomerID", nwindConn)
' The Update command checks for optimistic concurrency violations in the WHERE clause.
custDA.UpdateCommand = New SqlCommand("UPDATE Customers (CustomerID, CompanyName) VALUES(@CustomerID, @CompanyName) " & _
"WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName", nwindConn)
custDA.UpdateCommand.Parameters.Add("@CustomerID", SqlDbType.NChar, 5, "CustomerID")
custDA.UpdateCommand.Parameters.Add("@CompanyName", SqlDbType.NVarChar, 30, "CompanyName")
' Pass the original values to the WHERE clause parameters.
Dim myParm As SqlParameter
myParm = custDA.UpdateCommand.Parameters.Add("@oldCustomerID", SqlDbType.NChar, 5, "CustomerID")
myParm.SourceVersion = DataRowVersion.Original
myParm = custDA.UpdateCommand.Parameters.Add("@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName")
myParm.SourceVersion = DataRowVersion.Original
' Add the RowUpdated event handler.
AddHandler custDA.RowUpdated, New SqlRowUpdatedEventHandler(AddressOf OnRowUpdated)
Dim custDS As DataSet = New DataSet()
custDA.Fill(custDS, "Customers")
' Modify the DataSet contents.
custDA.Update(custDS, "Customers")
Dim myRow As DataRow
For Each myRow In custDS.Tables("Customers").Rows
If myRow.HasErrors Then Console.WriteLine(myRow(0) & vbCrLf & myRow.RowError)
Next
Private Shared Sub OnRowUpdated(sender As object, args As SqlRowUpdatedEventArgs)
If args.RecordsAffected = 0
args.Row.RowError = "Optimistic Concurrency Violation Encountered"
args.Status = UpdateStatus.SkipCurrentRow
End If
End Sub
[C#]
SqlConnection nwindConn = new SqlConnection("Data Source=localhost;Integrated Security=SSPI;Initial Catalog=northwind");
SqlDataAdapter custDA = new SqlDataAdapter("SELECT CustomerID, CompanyName FROM Customers ORDER BY CustomerID", nwindConn);
// The Update command checks for optimistic concurrency violations in the WHERE clause.
custDA.UpdateCommand = new SqlCommand("UPDATE Customers (CustomerID, CompanyName) VALUES(@CustomerID, @CompanyName) " +
"WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName", nwindConn);
custDA.UpdateCommand.Parameters.Add("@CustomerID", SqlDbType.NChar, 5, "CustomerID");
custDA.UpdateCommand.Parameters.Add("@CompanyName", SqlDbType.NVarChar, 30, "CompanyName");
// Pass the original values to the WHERE clause parameters.
SqlParameter myParm;
myParm = custDA.UpdateCommand.Parameters.Add("@oldCustomerID", SqlDbType.NChar, 5, "CustomerID");
myParm.SourceVersion = DataRowVersion.Original;
myParm = custDA.UpdateCommand.Parameters.Add("@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName");
myParm.SourceVersion = DataRowVersion.Original;
// Add the RowUpdated event handler.
custDA.RowUpdated += new SqlRowUpdatedEventHandler(OnRowUpdated);
DataSet custDS = new DataSet();
custDA.Fill(custDS, "Customers");
// Modify the DataSet contents.
custDA.Update(custDS, "Customers");
foreach (DataRow myRow in custDS.Tables["Customers"].Rows)
{
if (myRow.HasErrors)
Console.WriteLine(myRow[0] + "\n" + myRow.RowError);
}
protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs args)
{
if (args.RecordsAffected == 0)
{
args.Row.RowError = "Optimistic Concurrency Violation Encountered";
args.Status = UpdateStatus.SkipCurrentRow;
}
}
Vea también
Ejemplo de escenarios de ADO.NET | Actualizar la base de datos con un DataAdapter y el DataSet | Trabajar con eventos DataAdapter | Agregar y leer información de error de fila | Acceso a datos con ADO.NET | Utilizar proveedores de datos de .NET Framework para obtener acceso a datos