Проверка параллелизма в объекте без обновления версии строки

У меня есть материнская организация, который мне нужно сделать проверка параллелизма (как указано ниже)

[Timestamp]
public byte[] RowVersion { get; set; }

У меня есть куча клиентские процессы, которые получают доступ к значениям только для чтения из этого материнская организация и в первую очередь Обновить к его дочерние сущности.

Ограничение

  1. Клиенты не должны мешать работе друг друга (например, обновление дочерних записей не должно вызывать исключение параллелизма в материнская организация).

  2. У меня есть серверный процесс, который делает, Обновить, этот материнская организация, и в этом случае клиентский процесс нужно выкинуть, если материнская организация был изменен.

Note : The client's concurrency check is sacrificial, the server's workflow is mission critical.

Эта проблема

Мне нужно проверить (из клиентский процесс), изменился ли материнская организациябез обновления версии строки родительского объекта.

Достаточно просто выполнить проверку параллелизма для материнская организация в ЭФ:

// Update the row version's original value
_db.Entry(dbManifest)
      .Property(b => b.RowVersion)
      .OriginalValue = dbManifest.RowVersion; // the row version the client originally read

// Mark the row version as modified
_db.Entry(dbManifest)
       .Property(x => x.RowVersion)
       .IsModified = true;

IsModified = true — это нарушитель сделки, потому что он заставляет изменить версию строки. Или, говоря в контексте, эта проверка из клиентского процесса вызовет изменение версии строки в материнская организация, что излишне мешает другим рабочим процессам клиентские процессы.

A work around : I could potentially wrap the SaveChanges from the client process in a Transaction and then a subsequent read of the parent entity's row version, in-turn, rolling back if the row version has changed.

Резюме

Есть ли способ нестандартный с Структура сущности, где я могу SaveChangesклиентский процесс для дочерние объекты), а также проверить, изменился ли версия строки родительского объекта (без обновления версия строки родительских сущностей).

Можно ли использовать функцию sqlserver rowversion? docs.microsoft.com/en-us/sql/t-sql/data-types/…

ilkerkaran 19.07.2019 08:29

@ilkerkaran да, я его использую, однако это скорее случай того, как проверить изменение параллелизма в родительской таблице без изменения rowversion в этой таблице, поэтому SaveChanges не работает в структуре сущностей.

TheGeneral 19.07.2019 08:32

Эта проблема похожа на вашу? social.msdn.microsoft.com/Forums/en-US/….

Andrei Odegov 19.07.2019 11:52

Есть много методов, которые изменяют родителей и детей, или только несколько? Другими словами, не приведет ли обход вашей транзакции к повторяющемуся коду?

Gert Arnold 22.07.2019 22:47

@GertArnold привет, да, клиентам нужно будет защитить места, где они обновляют дочерние записи, их всего несколько, может быть, до 10, хотя это не является нарушением условий сделки. Я мог бы легко инкапсулировать логику транзакций в метод, но было бы неплохо, если бы был другой атомарный способ.

TheGeneral 23.07.2019 01:10

Я думаю, что обход вашей транзакции не так уж и плох. особ. при инкапсулировании преимущество заключается в том, что все это находится в одном месте, поэтому понятно, что происходит, и маловероятно, что это будет иметь какие-либо побочные эффекты. Любое другое решение, например, использующее перехватчики дерева команд EF, будет состоять из отдельных частей кода, что позволит легко сломать что-то по обе стороны спектра. Ф.э. все это терпит неудачу, если родительские идентификаторы строк не извлекаются из базы данных, о чем легко забыть, когда код не показывает, почему и где они необходимы. Кроме того, что, возможно, более важно, это привязывает вас к этой версии EF.

Gert Arnold 23.07.2019 09:23

Вы можете использовать атрибут [ConcurrencyCheck] над одним из свойств, который, исходя из моего опыта, обеспечивает хороший контроль параллелизма для всей таблицы.

Amirhossein Mehrvarzi 25.07.2019 16:35
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
18
7
4 447
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ну, что вам нужно сделать, так это проверить токен параллелизма (Timestamp) родительского объекта, когда вы пишете в дочерний объект. Единственная проблема заключается в том, что родительская метка времени отсутствует в дочерних объектах.

Вы не указали явно, но я предполагаю, что вы используете EF Core.

Глядя на https://docs.microsoft.com/en-us/ef/core/saving/concurrency, кажется, что EF Core выдаст исключение параллелизма, если UPDATE или DELETE затронет нулевые строки. Чтобы реализовать тестирование параллелизма, EF добавляет предложение WHERE, проверяющее токен параллелизма, а затем проверяет, повлияло ли обновление или удаление на правильное количество строк.

Что вы могли бы попробовать, так это добавить дополнительное предложение WHERE в UPDATE или DELETE, которое проверяет значение RowVersion родителя. Я думаю, вы могли бы сделать это, используя класс System.Diagnostics.DiagnosticListener для перехвата EF Core 2. Об этом есть статья на https://weblogs.asp.net/ricardoperes/interception-in-entity-framework-core и обсуждение на Могу ли я настроить перехватчик в EntityFramework Core?. Очевидно, что EF Core 3 (думаю, он появится в сентябре/октябре) будет включать механизм перехвата, аналогичный тому, что был в EF pre-Core, см. https://github.com/aspnet/EntityFrameworkCore/issues/15066.

Надеюсь, это будет полезно для вас.

Ну, во-первых, это нет EF Core, но EF6 - я специально спросил у ОП, и они добавили правильный тег, который должен был быть там на момент ответа. Во-вторых, что касается только вызов - для меня это довольно большая проблема. Хотя в целом идея кажется правильной, я хотел бы, чтобы она была реализована в общем виде, особенно с довольно нелогичной моделью метаданных EF6.

Ivan Stoev 22.07.2019 10:35

Да, это определенно инфраструктура сущностей, а не ядро, сегодня я еще раз взгляну на этот подход и посмотрю, применим ли он.

TheGeneral 23.07.2019 01:14

Последняя ссылка, приведенная в моем ответе выше, повторяет проблему и называет классы, которые имеют отношение к решению проблемы до ядра.

sjb-sjb 25.07.2019 04:43
Ответ принят как подходящий

Существует удивительно простое решение «из двух коробок», но оно требует двух модификаций, которые я не уверен, что вы можете или хотите сделать:

  • Создайте представление обновляемый для дочерней таблицы, содержащей столбец ParentRowVersion
  • Сопоставьте дочернюю сущность с этим представлением

Позвольте мне показать, как это работает. Все довольно просто.

Модель базы данных:

CREATE TABLE [dbo].[Parent]
(
[ID] [int] NOT NULL IDENTITY(1, 1),
[Name] [nvarchar] (50) NOT NULL,
[RowVersion] [timestamp] NOT NULL
) ON [PRIMARY]
ALTER TABLE [dbo].[Parent] ADD CONSTRAINT [PK_Parent] PRIMARY KEY CLUSTERED  ([ID]) ON [PRIMARY]

CREATE TABLE [dbo].[Child]
(
[ID] [int] NOT NULL IDENTITY(1, 1),
[Name] [nvarchar] (50) NOT NULL,
[RowVersion] [timestamp] NOT NULL,
[ParentID] [int] NOT NULL
) ON [PRIMARY]
ALTER TABLE [dbo].[Child] ADD CONSTRAINT [PK_Child] PRIMARY KEY CLUSTERED  ([ID]) ON [PRIMARY]
GO
CREATE VIEW [dbo].[ChildView]
WITH SCHEMABINDING
AS
SELECT Child.ID
, Child.Name
, Child.ParentID
, Child.RowVersion
, p.RowVersion AS ParentRowVersion
FROM dbo.Child
INNER JOIN dbo.Parent p ON p.ID = Child.ParentID

Представление можно обновлять, поскольку оно соответствует условия для обновляемых представлений Sql Server.

Данные

SET IDENTITY_INSERT [dbo].[Parent] ON
INSERT INTO [dbo].[Parent] ([ID], [Name]) VALUES (1, N'Parent1')
SET IDENTITY_INSERT [dbo].[Parent] OFF

SET IDENTITY_INSERT [dbo].[Child] ON
INSERT INTO [dbo].[Child] ([ID], [Name], [ParentID]) VALUES (1, N'Child1.1', 1)
INSERT INTO [dbo].[Child] ([ID], [Name], [ParentID]) VALUES (2, N'Child1.2', 1)
SET IDENTITY_INSERT [dbo].[Child] OFF

Модель класса

public class Parent
{
    public Parent()
    {
        Children = new HashSet<Child>();
    }
    public int ID { get; set; }
    public string Name { get; set; }
    public byte[] RowVersion { get; set; }
    public ICollection<Child> Children { get; set; }
}

public class Child
{
    public int ID { get; set; }
    public string Name { get; set; }
    public byte[] RowVersion { get; set; }

    public int ParentID { get; set; }
    public Parent Parent { get; set; }
    public byte[] ParentRowVersion { get; set; }
}

Контекст

public class TestContext : DbContext
{
    public TestContext(string connectionString) : base(connectionString){ }

    public DbSet<Parent> Parents { get; set; }
    public DbSet<Child> Children { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Parent>().Property(e => e.RowVersion).IsRowVersion();
        modelBuilder.Entity<Child>().ToTable("ChildView");
        modelBuilder.Entity<Child>().Property(e => e.ParentRowVersion).IsRowVersion();
    }
}

Объединяем

Этот фрагмент кода обновляет Child, в то время как поддельный параллельный пользователь обновляет свой Parent:

using (var db = new TestContext(connString))
{
    var child = db.Children.Find(1);

    // Fake concurrent update of parent.
    db.Database.ExecuteSqlCommand("UPDATE dbo.Parent SET Name = Name + 'x' WHERE ID = 1");
    
    child.Name = child.Name + "y";
    db.SaveChanges();
}

Теперь SaveChanges бросает нужный DbUpdateConcurrencyException. Когда обновление родителя закомментировано, дочернее обновление выполняется успешно.

Я думаю, что преимущество этого метода в том, что он практически не зависит от библиотеки доступа к данным. Все, что вам нужно, — это ORM, поддерживающий оптимистичный параллелизм. Будущий переход на EF-core не будет проблемой.

На самом деле это очень аккуратное и творческое решение, а также новый способ достижения желаемого результата с видом. сегодня на работе поиграю, спасибо!

TheGeneral 23.07.2019 23:22

"из двух коробок" - лол, мне это нравится! Было бы неплохо, если бы EF позволял нам создавать такое «представление» с помощью кода (столбец типа выражения?), но это не так, так что это кажется лучшей комбинацией «двух миров» :)

Ivan Stoev 24.07.2019 19:50

@Иван, спасибо! Да, EF мог бы быть немного лучше осведомлен о графах/агрегатах, хотя я думаю, что EF-core — это «небольшой шаг» в этом направлении. Я считаю, что вы способны реализовать эту идею столбца типа выражения в пул реквест, сделав «гигантский скачок» в этой области. Только занимает немного времени...

Gert Arnold 24.07.2019 20:55

Это лучше всего отвечает на вопрос в духе того, о чем я просил

TheGeneral 26.07.2019 08:33

Отличный ответ! Просто любопытно, как создать представление при использовании EF Code First для определения модели.

sjb-sjb 30.08.2019 14:12

@sjb-sjb «как создать представление» включает в себя множество вещей. Лучше задать новый вопрос, если это проблема для вас.

Gert Arnold 30.08.2019 14:26

От проекта к проекту я встречаю эту проблему на широких платформах (не только .Net). С точки зрения архитектуры могу предложить несколько решений, не свойственных EntityFramework. (как по мне №2 лучше)

ОПЦИЯ 1 для реализации оптимистического подхода к блокировке. В целом идея звучит так: «Давайте обновим Клиент, а потом проверим состояние родителя». Вы уже упомянули идею «Использовать транзакцию», но оптимистичная блокировка может просто сократить время, необходимое для сохранения родительской сущности. Что-то типа:

var expectedVersion = _db.Parent...First().RowVersion;
using (var transactionScope = new TransactionScope(TransactionScopeOption.Required))
{
    //modify Client entity there
    ...
    //now make second check of Parent version
    if ( expectedVersion != _db.Parent...First().RowVersion )
        throw new Exception(...);
    _db.SaveChanges();
}

Примечание! В зависимости от настроек SQL-сервера (уровней изоляции) вам может потребоваться применить к родительскому объекту выбор для обновления, пожалуйста, посмотрите, как это сделать. Как реализовать функцию «Выбор для обновления» в EF Core

ВАРИАНТ 2 Как по мне, лучше вместо EF использовать явный SQL что-то вроде:

UPDATE 
    SET Client.BusinessValue = :someValue -- changes of client
    FROM Client, Parent
         WHERE Client.Id = :clientToChanges -- restrict updates by criteria
         AND Client.ParentId = Parent.Id -- join with Parent entity
         AND Parent.RowVersion = :expectedParent

После этого запроса в коде .Net вам нужно проверить, что была затронута ровно 1 строка (0 означает, что Parent.Rowversion был изменен)

if (_db.ExecuteSqlCommand(sql) != 1 )
    throw new Exception();

Также попробуйте проанализировать паттерн проектирования "Глобальная блокировка" с помощью дополнительной таблицы БД. Вы можете прочитать об этом подходе здесь http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

Другие вопросы по теме