DbUpdateConcurrencyException
От: merge  
Дата: 15.06.22 11:09
Оценка:
Столкнулся тут с ошибкой в таком коде на шарпе

для простоты написал псевдокодом. но идея в том что табла меняется из 2 мест по сути в рамках энтити контекста и в скл самом и кидается такой экспешн DbUpdateConcurrencyException

begin tran
    efContext.ExecuteSqlRaw("update users set time = getdate() where id = 5")
        var user = efContext.Single(a => a.id == 5);
        user.time = Now
        efContex.Users.Update(user);
        efContext.SaveChanges()
commit tran


решение я нашел тут
но мне показалось странным выставлять значение в обработке эксепшна.
Есть решение получше или всё же это нормально?

 catch (DbUpdateConcurrencyException ex)
    {
        foreach (var entry in ex.Entries)
        {
            if (entry.Entity is Person)
            {
                var proposedValues = entry.CurrentValues;
                var databaseValues = entry.GetDatabaseValues();

                foreach (var property in proposedValues.Properties)
                {
                    var proposedValue = proposedValues[property];
                    var databaseValue = databaseValues[property];

                    // TODO: decide which value should be written to database
                    // proposedValues[property] = <value to be saved>;
                }

                // Refresh original values to bypass next concurrency check
                entry.OriginalValues.SetValues(databaseValues);
            }
            else
            {
                throw new NotSupportedException(
                    "Don't know how to handle concurrency conflicts for "
                    + entry.Metadata.Name);
            }
        }
    }
Re: DbUpdateConcurrencyException
От: RushDevion Россия  
Дата: 15.06.22 11:44
Оценка:
M>Есть решение получше или всё же это нормально?

Имхо, этот вариант борьбы с concurrency изначально порочен.
Откуда тебе знать, что такая подмена значений не поломает бизнес-логику в целом?
Например, если на основе "старых" значений из конфликтующей сущности были сделаны изменения в других таблицах или вообще в сторонних системах.
Как быть уверенным, что поменяв значения в cath() {...}, ты снова не отхватишь concurrency exception, который уже не перехватить?

А касательно правильного подхода, it depends.
1. Если это фоновый обработчик. Типа раз в минуту взять данные из базы, пошуршать, положить обновленные данные в базу. Тогда проще сделать ретрай всей бизнес-транзакции: прочитать-изменить-сохранить.
2. Если это сохраняется из UI, то можно
2.1 Действовать как в п.1, т.е. считаем что last write wins и делаем цикл прочитать-применить изменения-обновить до тех пор, пока не пройдет без ConcurrencyException.
2.2 Показать юзеру ошибку: "Версия данных в БД отличается от текущей. Перезагрузите страницу." Реализация сведется к простому перехвату exception и показу сообщения.
2.3 Показать юзеру ошибку: "Версия данных в БД отличается от текущей, обновить из БД?" Это сведется к перехвату exception, вытаскиванию актуальных БД-значений и запихиванию их в поля на форме редактирования, если пользователь выбрал "Обновить из БД".
Отредактировано 15.06.2022 11:54 RushDevion . Предыдущая версия .
Re[2]: DbUpdateConcurrencyException
От: merge  
Дата: 15.06.22 12:01
Оценка:
Здравствуйте, RushDevion, Вы писали:

M>>Есть решение получше или всё же это нормально?


RD>Имхо, этот вариант борьбы с concurrency изначально порочен.

RD>Откуда тебе знать, что такая подмена значений не поломает бизнес-логику в целом?
RD>Например, если на основе "старых" значений из конфликтующей сущности были сделаны изменения в других таблицах или вообще в сторонних системах.
RD>Как быть уверенным, что поменяв значения в cath() {...}, ты снова не отхватишь concurrency exception, который уже не перехватить?

RD>А касательно правильного подхода, it depends.

RD>1. Если это фоновый обработчик. Типа раз в минуту взять данные из базы, пошуршать, положить обновленные данные в базу. Тогда проще сделать ретрай всей бизнес-транзакции: прочитать-изменить-сохранить.
RD>2. Если это сохраняется из UI, то можно
RD>2.1 Показать юзеру ошибку: "Версия данных в БД отличается от текущей. Перезагрузите страницу." Реализация сведется к простому перехвату exception и показу сообщения.
RD>2.2 Показать юзеру ошибку: "Версия данных в БД отличается от текущей, обновить из БД?" Это сведется к перехвату exception, вытаскиванию актуальных БД-значений и запихиванию их в поля на форме редактирования, если пользователь выбрал "Обновить из БД".


тут работа из юая идет, просто 2 шага и первый из-за большого кол-ва скл кода сделали в виде процедуры которая в одном месте апдет таблы делает, которую потом же апдейтит энтити обновленным значением.
то есть юзер тут ничего не изменит.
тут вариант видимо перенос логики из энтити в даппер какой-то который не имеет оптимистичных блокировок либо как прочитал тут
Автор: scale_tone
Дата: 03.12.13
про паттерн какой-то
Re[3]: DbUpdateConcurrencyException
От: Sinclair Россия http://corp.ingrammicro.com/Solutions/Cloud.aspx
Дата: 16.06.22 03:41
Оценка:
Здравствуйте, merge, Вы писали:
M>тут вариант видимо перенос логики из энтити в даппер какой-то который не имеет оптимистичных блокировок либо как прочитал тут
Автор: scale_tone
Дата: 03.12.13
про паттерн какой-то

Вот правильный паттерн: https://rsdn.org/forum/dotnet/5380663.1
Автор: IT
Дата: 02.12.13
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
http://rsdn.org/File/5743/rsdnaddict.GIF
Re[4]: DbUpdateConcurrencyException
От: merge  
Дата: 16.06.22 05:25
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, merge, Вы писали:

M>>тут вариант видимо перенос логики из энтити в даппер какой-то который не имеет оптимистичных блокировок либо как прочитал тут
Автор: scale_tone
Дата: 03.12.13
про паттерн какой-то

S>Вот правильный паттерн: https://rsdn.org/forum/dotnet/5380663.1
Автор: IT
Дата: 02.12.13


а он жив и его поддерживают?

ну перенос это дело небыстрое в любом случае, а в одном месте использовать это плохо
Re: DbUpdateConcurrencyException
От: Qulac Россия  
Дата: 16.06.22 06:28
Оценка:
Здравствуйте, merge, Вы писали:

M>Столкнулся тут с ошибкой в таком коде на шарпе


M>для простоты написал псевдокодом. но идея в том что табла меняется из 2 мест по сути в рамках энтити контекста и в скл самом и кидается такой экспешн DbUpdateConcurrencyException


M>
M>begin tran
M>    efContext.ExecuteSqlRaw("update users set time = getdate() where id = 5")
M>        var user = efContext.Single(a => a.id == 5);
M>        user.time = Now
M>        efContex.Users.Update(user);
M>        efContext.SaveChanges()
M>commit tran
M>


M>решение я нашел тут

M>но мне показалось странным выставлять значение в обработке эксепшна.
M>Есть решение получше или всё же это нормально?

M>
M> catch (DbUpdateConcurrencyException ex)
M>    {
M>        foreach (var entry in ex.Entries)
M>        {
M>            if (entry.Entity is Person)
M>            {
M>                var proposedValues = entry.CurrentValues;
M>                var databaseValues = entry.GetDatabaseValues();

M>                foreach (var property in proposedValues.Properties)
M>                {
M>                    var proposedValue = proposedValues[property];
M>                    var databaseValue = databaseValues[property];

M>                    // TODO: decide which value should be written to database
M>                    // proposedValues[property] = <value to be saved>;
M>                }

M>                // Refresh original values to bypass next concurrency check
M>                entry.OriginalValues.SetValues(databaseValues);
M>            }
M>            else
M>            {
M>                throw new NotSupportedException(
M>                    "Don't know how to handle concurrency conflicts for "
M>                    + entry.Metadata.Name);
M>            }
M>        }
M>    }
M>



Сущность можно обновлять двумя способами. Первый:
 using (var db=new Db())
            {
                var m = db.Managers.Find(1);
                m.Name = "MyName_1";
                db.SaveChanges();
            }


EF генерит такие запросы:

info: 16.06.2022 09:20:43.803 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (51ms) [Parameters=[@__p_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT TOP(1) [m].[Id], [m].[Family], [m].[Name]
FROM [Managers] AS [m]
WHERE [m].[Id] = @__p_0
info: 16.06.2022 09:20:44.006 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (24ms) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Managers] SET [Name] = @p0
WHERE [Id] = @p1;
SELECT @@ROWCOUNT;

Ясное дело что после select сущность в бд могла быть изменена другим запросом.

Второй способ:
  using (var db = new Db())
            {
                var m = new Manager() { Id = 1, Name = "MyName_2", Family = "Pupkin" };

                db.Attach<Manager>(m).State = EntityState.Modified;
                db.SaveChanges();
            }


EF генерит один запрос:

info: 16.06.2022 09:15:29.601 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (52ms) [Parameters=[@p2='?' (DbType = Int32), @p0='?' (Size = 4000), @p1='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Managers] SET [Family] = @p0, [Name] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

Не проверял. но наверно в этом случае лучше использовать второй способ.
Программа – это мысли спрессованные в код
Re[5]: DbUpdateConcurrencyException
От: Sinclair Россия http://corp.ingrammicro.com/Solutions/Cloud.aspx
Дата: 16.06.22 10:19
Оценка:
Здравствуйте, merge, Вы писали:
M>а он жив и его поддерживают?
А то!
M>ну перенос это дело небыстрое в любом случае, а в одном месте использовать это плохо
Верно.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
http://rsdn.org/File/5743/rsdnaddict.GIF
Re: DbUpdateConcurrencyException
От: Ночной Смотрящий Россия  
Дата: 16.06.22 14:14
Оценка:
Здравствуйте, merge, Вы писали:

M>решение я нашел тут

M>но мне показалось странным выставлять значение в обработке эксепшна.
M>Есть решение получше или всё же это нормально?

Это т.н. optimistic concurrency. Только ты зря цикл не процитировал в примере, он важен (кому интересно — вот статья откуда пример).

Algorithms built around CAS typically read some key memory location and remember the old value. Based on that old value, they compute some new value. Then they try to swap in the new value using CAS, where the comparison checks for the location still being equal to the old value. If CAS indicates that the attempt has failed, it has to be repeated from the beginning: the location is re-read, a new value is re-computed and the CAS is tried again.

https://en.wikipedia.org/wiki/Compare-and-swap
... << RSDN@Home 1.3.17 alpha 5 rev. 62>>
Re[4]: DbUpdateConcurrencyException
От: Ночной Смотрящий Россия  
Дата: 16.06.22 14:17
Оценка:
Здравствуйте, Sinclair, Вы писали:

M>>тут вариант видимо перенос логики из энтити в даппер какой-то который не имеет оптимистичных блокировок либо как прочитал тут
Автор: scale_tone
Дата: 03.12.13
про паттерн какой-то

S>Вот правильный паттерн: https://rsdn.org/forum/dotnet/5380663.1
Автор: IT
Дата: 02.12.13


Optimistic locking можно и в EF отключить. Ценой перфа на неконкурентном обновлении, разумеется.
... << RSDN@Home 1.3.17 alpha 5 rev. 62>>
Re[2]: DbUpdateConcurrencyException
От: merge  
Дата: 16.06.22 16:00
Оценка:
Здравствуйте, Ночной Смотрящий, Вы писали:

НС>Здравствуйте, merge, Вы писали:


M>>решение я нашел тут

M>>но мне показалось странным выставлять значение в обработке эксепшна.
M>>Есть решение получше или всё же это нормально?

НС>Это т.н. optimistic concurrency. Только ты зря цикл не процитировал в примере, он важен (кому интересно — вот статья откуда пример).

НС>

НС>Algorithms built around CAS typically read some key memory location and remember the old value. Based on that old value, they compute some new value. Then they try to swap in the new value using CAS, where the comparison checks for the location still being equal to the old value. If CAS indicates that the attempt has failed, it has to be repeated from the beginning: the location is re-read, a new value is re-computed and the CAS is tried again.

НС>https://en.wikipedia.org/wiki/Compare-and-swap

самое интересное что у меня этот код свалился в такую же ошибку.
так вот сделал


if (entry.Entity is Promo)
                    {
                        var proposedValues = entry.CurrentValues;
                        var databaseValues = entry.GetDatabaseValues();
                        
                        foreach (var property in proposedValues.Properties)
                        {
                            var proposedValue = proposedValues[property];
                            var databaseValue = databaseValues[property];

                            // TODO: decide which value should be written to database
                            proposedValues[property] = proposedValue;
                        }

                        // Refresh original values to bypass next concurrency check
                        entry.OriginalValues.SetValues(proposedValues);
                    }
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.