У меня есть материнская организация, который мне нужно сделать проверка параллелизма (как указано ниже)
[Timestamp]
public byte[] RowVersion { get; set; }
У меня есть куча клиентские процессы, которые получают доступ к значениям только для чтения из этого материнская организация и в первую очередь Обновить к его дочерние сущности.
Ограничение
Клиенты не должны мешать работе друг друга (например, обновление дочерних записей не должно вызывать исключение параллелизма в материнская организация).
У меня есть серверный процесс, который делает, Обновить, этот материнская организация, и в этом случае клиентский процесс нужно выкинуть, если материнская организация был изменен.
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 (в клиентский процесс для дочерние объекты), а также проверить, изменился ли версия строки родительского объекта (без обновления версия строки родительских сущностей).
@ilkerkaran да, я его использую, однако это скорее случай того, как проверить изменение параллелизма в родительской таблице без изменения rowversion в этой таблице, поэтому SaveChanges не работает в структуре сущностей.
Эта проблема похожа на вашу? social.msdn.microsoft.com/Forums/en-US/….
Есть много методов, которые изменяют родителей и детей, или только несколько? Другими словами, не приведет ли обход вашей транзакции к повторяющемуся коду?
@GertArnold привет, да, клиентам нужно будет защитить места, где они обновляют дочерние записи, их всего несколько, может быть, до 10, хотя это не является нарушением условий сделки. Я мог бы легко инкапсулировать логику транзакций в метод, но было бы неплохо, если бы был другой атомарный способ.
Я думаю, что обход вашей транзакции не так уж и плох. особ. при инкапсулировании преимущество заключается в том, что все это находится в одном месте, поэтому понятно, что происходит, и маловероятно, что это будет иметь какие-либо побочные эффекты. Любое другое решение, например, использующее перехватчики дерева команд EF, будет состоять из отдельных частей кода, что позволит легко сломать что-то по обе стороны спектра. Ф.э. все это терпит неудачу, если родительские идентификаторы строк не извлекаются из базы данных, о чем легко забыть, когда код не показывает, почему и где они необходимы. Кроме того, что, возможно, более важно, это привязывает вас к этой версии EF.
Вы можете использовать атрибут [ConcurrencyCheck] над одним из свойств, который, исходя из моего опыта, обеспечивает хороший контроль параллелизма для всей таблицы.





Ну, что вам нужно сделать, так это проверить токен параллелизма (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.
Да, это определенно инфраструктура сущностей, а не ядро, сегодня я еще раз взгляну на этот подход и посмотрю, применим ли он.
Последняя ссылка, приведенная в моем ответе выше, повторяет проблему и называет классы, которые имеют отношение к решению проблемы до ядра.
Существует удивительно простое решение «из двух коробок», но оно требует двух модификаций, которые я не уверен, что вы можете или хотите сделать:
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 не будет проблемой.
На самом деле это очень аккуратное и творческое решение, а также новый способ достижения желаемого результата с видом. сегодня на работе поиграю, спасибо!
"из двух коробок" - лол, мне это нравится! Было бы неплохо, если бы EF позволял нам создавать такое «представление» с помощью кода (столбец типа выражения?), но это не так, так что это кажется лучшей комбинацией «двух миров» :)
@Иван, спасибо! Да, EF мог бы быть немного лучше осведомлен о графах/агрегатах, хотя я думаю, что EF-core — это «небольшой шаг» в этом направлении. Я считаю, что вы способны реализовать эту идею столбца типа выражения в пул реквест, сделав «гигантский скачок» в этой области. Только занимает немного времени...
Это лучше всего отвечает на вопрос в духе того, о чем я просил
Отличный ответ! Просто любопытно, как создать представление при использовании EF Code First для определения модели.
@sjb-sjb «как создать представление» включает в себя множество вещей. Лучше задать новый вопрос, если это проблема для вас.
От проекта к проекту я встречаю эту проблему на широких платформах (не только .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
Можно ли использовать функцию sqlserver rowversion? docs.microsoft.com/en-us/sql/t-sql/data-types/…