2010-09-12 17 views
5

En un flujo de trabajo de aprobación, quiero asegurarme de que los correos electrónicos de recordatorio se envíen exactamente una vez.EF eqivalent para filas afectadas de SqlCommand.ExecuteNonQuery

Con SqlCommand.ExecuteNonQuery puedo asegurar esto probando el valor de retorno. ¿Cuál es la solución recomendada con EF? De acuerdo con la documentación ObjectContext.SaveChanges no devuelve un valor equivalente.

SqlCommand ejemplo: (El TransactionScope se utiliza para deshacer la actualización de base de datos en caso de SendMail falla.)

 

Dim sql = "UPDATE LeaveApprovalRequests SET State = 'Reminded'" & 
      " WHERE ID=3 AND State <>'Reminded'" 
Using scope As New TransactionScope 
    Using cnx As New SqlConnection(My.Settings.connectionString) 
     cnx.Open() 
     Dim cmd As New SqlCommand(sql, cnx) 
     If 1 = cmd.ExecuteNonQuery Then 
      SendMail() 
     End If 
     scope.Complete() 
    End Using 
End Using 
 

Al permitir la concurrencia optimista (usando ConcurrencyMode = fija en una propiedad rowversion) y agarrar la OptimisticConcurrencyException I Puedo identificar si el objeto realmente se actualizó en la tienda. Ahora, el TransactionScope (utilizado para deshacer la actualización de la base de datos si SendMail falla) genera un error de interbloqueo. ¿Por qué?

 

Using scope As New TransactionScope 
    Using ctx As New ApprovalEntities 
    Try 
     Dim approval = ctx.LeaveApprovalRequests. 
     Where(Function(r) r.ID = 3 And r.State = "Created" 
     ).FirstOrDefault 
     If approval Is Nothing Then 
     Console.WriteLine("not found") 
     Exit Sub 
     End If 
     Threading.Thread.Sleep(4000) 
     approval.State = "Reminded" 
     ctx.SaveChanges() 
     SendMail() 
    Catch ex As OptimisticConcurrencyException 
     Exit Try 
    End Try 
    End Using 
    scope.Complete() 
End Using 
 

Respuesta

0

Como resultado de la discusión con Morteza contesto mi pregunta sigue

SaveChanges devuelve la cantidad de objetos que intenta actualizar, no el número que actualizó en la tienda. Por lo tanto, se debe usar junto con OptimisticConcurrencyException para determinar si el cambio tuvo éxito. Se debe considerar que otras propiedades además de la que se pretende cambiar pueden causar una OptimisticConcurrencyException.

Leer una entidad y actualizarla en el mismo TransactionScope provoca un interbloqueo.

Por mi tarea "Sólo envíe un correo electrónico si soy capaz de cambiar el estado de creado para recordar" Puedo utilizar la siguiente solución:

Dividir la entidad ApprovalRequest en dos con un 1: asociación 1, salida de OptimisticConcurrencyException, envíe correo en TransactionScope con SaveChanges.

 

ApprovalRequests 
    ID (PK) 
    RequestedBy 
    ... 
    RowVersion (ConcurrencyMode=Fixed) 

ApprovalRequestStates 
    ApprovalRequest_ID (PK, FK) 
    State (ConcurrencyMode=Fixed) 
 
 

Using ctx As New ApprovalEntities 
    Dim approval = cxt.ApprovalRequests.Where ... 
    Dim state = ctx.ApprovalRequestStates. 
     Where(Function(r) r.ApprovalRequest_ID = approval.ID And r.State = "Created" 
     ).FirstOrDefault() 
    If state Is Nothing Then Exit Sub 
    state.State = "Reminded" 
    Threading.Thread.Sleep(3000) 
    Using scope As New TransactionScope 
     Try 
      ctx.SaveChanges() 
      SendMail() 
      scope.Complete() 
     Catch ex As OptimisticConcurrencyException 
      Exit Try 
     End Try 
    End Using 
End Using 
 

¡Cuidado! La actualización de una entidad hija al hacer referencia a ella a través de su padre también provoca una actualización de la base de datos del padre, en este caso lanzando una OptimisticConcurrencyException no deseada. Así que no usé: ApprovalRequests.ApprovalRequestStates.State = "Reminded"

2

Bueno, como cuestión de hecho, el número exacto de filas afectadas se puede deducir de una llamada a ObjectContext.SaveChanges().

Si se echa un vistazo en el ObjectContext.SaveChanges documentation verá:

public int SaveChanges()

  1. Valor de retorno: El número de objetos en un añadido, modificado o estatal eliminados cuando SaveChanges fue llamado.
  2. "SaveChanges opera dentro de una transacción. SaveChanges revertirá esa transacción e incluirá una excepción si no se puede conservar ninguno de los objetos ObjectStateEntry sucios".

(1) y (2) básicamente significa que si su llamada a SaveChanges() se ha completado con éxito y no obtener ninguna excepción a continuación EF garantiza que el valor de retorno refleja con exactitud el número de objetos eso ha sido modificado

Por lo tanto, todo lo que tiene que hacer es:

try { 
    // Try to save changes, which may cause a conflict. 
    int num = context.SaveChanges(); 
    SendMail(); 
} 
catch (OptimisticConcurrencyException) { 
    //Refresh the entity, using ClientWins 
    context.Refresh(RefreshMode.ClientWins, yourEntityObject); 
    //SaveChanges again; 
    context.SaveChanges(); 
} 
Cuando Refresh con ClientWins se llama, se ejecuta una consulta para recuperar los valores actuales de esta entidad en la base de datos, incluyendo el nuevo orden de llegada. Por lo tanto, todos los valores de campo originales se han actualizado para reflejar los últimos valores de base de datos, por lo que podemos probar SaveChanges() una vez más.

actualizados con su pregunta:
La tarea es: sólo se envía un correo electrónico si soy capaz de cambiar el estado de creado para recordar. Por lo tanto, no tiene sentido forzar a través de los SaveChanges al manejar la OptimisticConcurrencyException. El controlador de errores debería salir si cambiar el estado provocó la excepción y volver a intentar toda la tarea (leer y guardar). ¿Cómo puedo hacer esto si la concurrencia optimista está habilitada a través de una columna RowVersion y no solo por estado?

Bueno, cada vez que se modifica una fila, el campo rowversion se actualiza automáticamente, por lo que en su caso sería el mejor si se apaga el modo de concurrencia en rowversion y lo enciende, para el Estado propiedad, por lo que su código será tan simple como:
try { 
    context.SaveChanges(); 
    SendMail(); 
} 
catch (OptimisticConcurrencyException) { 
    // RowVersion Concurrency Mode = Fixed 
    // State Concurrency Mode = None 
    // If it reaches here it means that the State has been changed; 
    // so you do nothing except than throwing the exception 
    throw; 
} 

Pero, si le gustaría establecer modalidad de simultaneidad = fijo sólo para la propiedad rowversion (como usted ha mencionado), entonces significa s que potencialmente puede obtener OptimisticConcurrencyException para el cambio en cualquier campo, incluyendo Estado, por lo que será un poco más trabajo hacer el trabajo:
try { 
    ctx.SaveChanges(); 
    SendMail; 
} 
catch (OptimisticConcurrencyException) { 
    // RowVersion Concurrency Mode = Fixed 
    // State Concurrency Mode = None 
    // If it reches here it means that ANY/All field(s) has changed 
    // So we need to see if it was State: 
    ctx.Refresh(RefreshMode.ClientWins, approval); 
    ObjectStateEntry ose = ctx.ObjectStateManager.GetObjectStateEntry(approval); 
    string stateValue = ose.OriginalValues["State"].ToString(); 
    // If the value is still "Created" then something else should have changed, 
    // And caused the exception, so we can proceed with the Save: 
    if (stateValue == "Created") { 
     ctx.SaveChanges(); 
     SendMail; 
    } 
    else { 
     // Nope, it was state, so we throw the exception to the caller: 
     throw; 
    } 

+0

Para mí, la documentación dice que SaveChanges devuelve el número de objetos que intenta actualizar, no el número que actualizó en la tienda. Lo verifiqué estableciendo un punto de interrupción en SaveChanges(), cambiando el valor en SQL Server y luego reanudando. –

+0

Exactamente. Y si puede llamar a SaveChanges() * sin * ninguna excepción, entonces EF garantiza que el número de objetos tiene la intención de actualizarse, de hecho se actualiza en la tienda o obtendrá una excepción. –

+0

Disculpe, no leí correctamente su primer comentario (aunque destacó la parte importante). Mi caso de prueba inicial no usó concurrencia optimista, pero en mis escenarios del mundo real lo habilitaré en la mayoría de los casos. Eso deja la pregunta por qué el TransactionScope arroja un error de interbloqueo. –

Cuestiones relacionadas