EF-эквивалент для строк, затронутых SqlCommand.ExecuteNonQuery - PullRequest
5 голосов
/ 12 сентября 2010

В рабочем процессе утверждения я хочу убедиться, что письма с напоминанием отправляются ровно один раз.

С SqlCommand.ExecuteNonQuery я могу убедиться в этом, протестировав возвращаемое значение.Какое решение рекомендуется использовать EF?Согласно документации ObjectContext.SaveChanges не возвращает эквивалентное значение.

Пример SqlCommand: (TransactionScope используется для отката обновления БД в случае сбоя SendMail.)


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

Включениемоптимистичный параллелизм (с использованием ConcurrencyMode = Fixed для свойства RowVersion) и перехват исключительной ситуации OptimisticConcurrencyException. Я могу определить, действительно ли объект был обновлен в магазине.Теперь TransactionScope (используется для отката обновления БД в случае сбоя SendMail) выдает ошибку взаимоблокировки.Почему?


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

Ответы [ 2 ]

2 голосов
/ 13 сентября 2010

Ну, на самом деле точное число затронутых строк может быть выведено из вызова ObjectContext.SaveChanges ().Если вы посмотрите документацию ObjectContext.SaveChanges , вы увидите:

public int SaveChanges()

  1. Возвращаемое значение:число объектов в состоянии «Добавлено», «Изменено» или «Удалено» при вызове SaveChanges.
  2. «SaveChanges работает в транзакции. SaveChanges откатит эту транзакцию и выдаст исключение, если любой изГрязные объекты ObjectStateEntry не могут быть сохранены. "

(1) и (2) в основном означает, что , если ваш вызов SaveChanges () был успешно завершен, и вы неНе получайте никаких исключений, тогда EF гарантирует, что возвращаемое значение точно отражает количество объектов, которые были изменены.

Поэтому все, что вам нужно сделать, это: <pre>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(); } Когда Refresh withВызывается ClientWins, он выполняет запрос для получения текущих значений этого объекта в базе данных, включая новую временную метку.Поэтому все исходные значения полей были обновлены, чтобы отразить последние значения базы данных, поэтому мы можем безопасно попробовать SaveChanges () еще раз.

Обновлено с вашим вопросом:
Задача: отправлять электронную почту только в том случае, если я могу изменить состояние с созданного на напоминание.Таким образом, не имеет смысла форсировать SaveChanges при обработке исключения OptimisticConcurrencyException.Обработчик ошибок должен завершиться, если изменение состояния вызвало исключение, и в противном случае повторить всю задачу (чтение и сохранение).Как это сделать, если оптимистичный параллелизм включен через столбец RowVersion, а не только по состоянию?

Ну, каждый раз, когда строка изменяется, поле rowversion автоматически обновляетсятак что в вашем случае было бы лучше, если бы вы выключили режим параллелизма на rowversion и включили его для свойства State , чтобы ваш код был таким простым: <pre>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; }
Но, если вы хотите установить Режим параллелизма = Фиксированный только для свойства rowversion (как вы упомянули), то это означает, что вы можете потенциальнополучите OptimisticConcurrencyException для изменения любого поля, включая State , так что работа будет немного больше: <pre>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 голосов
/ 17 сентября 2010

В результате беседы с Мортезой я отвечаю на мой вопрос следующим образом.

SaveChanges возвращает количество объектов, которые он намеревается обновить, а не количество, которое он обновил в хранилище. Таким образом, он должен использоваться вместе с OptimisticConcurrencyException, чтобы определить, было ли изменение успешным. Следует учитывать, что другие свойства, отличные от предназначенных для изменения, могут вызвать исключение OptimisticConcurrencyException.

Чтение сущности и обновление ее в том же TransactionScope вызывает тупик.

Для моей задачи «Отправлять электронную почту только в том случае, если я могу изменить состояние с созданного на напоминание», я использую следующее решение:

Разделите сущность ApprovalRequest на две части со связью 1: 1, выйдите на OptimisticConcurrencyException, отправьте почту в TransactionScope с помощью 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

Осторожно! Обновление дочернего объекта при обращении к нему через его родителя также приводит к обновлению родительского объекта в БД - в этом случае возникает нежелательное исключение OptimisticConcurrencyException. При этом я не использовал: ApprovalRequests.ApprovalRequestStates.State = "Reminded"

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...