J'ai une entité parent dont j'ai besoin pour effectuer un contrôle d'accès simultané ( comme annoté comme ci-dessous)
[Timestamp]
public byte[] RowVersion { get; set; }
J'ai un tas de processus clients qui accèdent en lecture seule aux valeurs de ce entité parent et principalement mettre à jour son entités enfants .
La contrainte
Les clients ne doivent pas interférer les uns avec les autres, (par exemple, la mise à jour des enregistrements enfants ne doit pas lancer d'exception de concurrence sur le parent entité ).
J'ai un processus serveur qui fait mise à jour cette entité parent , et dans ce cas, le processus client doit lancer si l'entité parent a été modifiée.
Remarque : La vérification d'accès concurrentiel du client est sacrificielle, le flux de travail du serveur est essentiel à la mission .
Le problème
J'ai besoin de vérifier (à partir du processus client ) si l'entité parent a changé sans mettre à jour la version de ligne de l'entité parent .
Il est assez facile de vérifier la concurrence sur l'entité parent dans [~ # ~] ef [~ # ~ ] :
// 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;
Le IsModified = true
est le séparateur de transactions car il force la version de la ligne à changer. Ou, dit en contexte, cette vérification du processus client entraînera un changement de version de ligne dans l'entité parent , ce qui interfère inutilement avec l'autre workflows des processus clients .
Contournement : je pourrais potentiellement envelopper le SaveChanges
de le processus client dans une transaction, puis une lecture ultérieure de la version de ligne de l'entité parent, à son tour, annule si la version de ligne a changé.
Résumé
Existe-t-il un moyen prêt à l'emploi avec Entity Framework où je peux SaveChanges
(dans le processus client pour les entités enfants ) mais vérifiez également si la version de ligne de l'entité parent a changé (sans mettre à jour le version de la ligne des entités parentes ).
Il existe une solution étonnamment simple, "out-of-2-boxes", mais elle nécessite deux modifications, je ne suis pas sûr que vous puissiez, ou êtes disposé à, apporter:
ParentRowVersion
Permettez-moi de montrer comment cela fonctionne. C'est assez simple.
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
La vue peut être mise à jour car elle remplit les conditions pour que les vues du serveur SQL puissent être mises à jour .
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();
}
}
Ce morceau de code met à jour un Child
tandis qu'un faux utilisateur simultané met à jour son 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();
}
Maintenant, SaveChanges
lance le DbUpdateConcurrencyException
requis. Lorsque la mise à jour du parent est commentée, la mise à jour enfant réussit.
Je pense que l'avantage de cette méthode est qu'elle est assez indépendante d'une bibliothèque d'accès aux données. Tout ce dont vous avez besoin est un ORM qui prend en charge la concurrence optimiste. Un futur passage à EF-core ne sera pas un problème.
Eh bien, ce que vous devez faire est de vérifier le jeton d'accès simultané (horodatage) de l'entité parent lorsque vous écrivez dans l'entité enfant. Le seul défi est que l'horodatage parent n'est pas dans les entités enfants.
Vous n'avez pas indiqué explicitement, mais je suppose que vous utilisez EF Core.
En regardant https://docs.Microsoft.com/en-us/ef/core/saving/concurrency , il semble que EF Core lèvera l'exception de concurrence si une mise à jour ou une suppression affecte zéro ligne. Pour implémenter les tests de concurrence, EF ajoute une clause WHERE testant le jeton de concurrence, puis teste si le nombre correct de lignes a été impacté par UPDATE ou DELETE.
Ce que vous pourriez essayer serait d'ajouter une clause WHERE supplémentaire à UPDATE ou DELETE qui teste la valeur de RowVersion du parent. Je pense que vous pourriez être en mesure de le faire en utilisant la classe System.Diagnostics.DiagnosticListener pour intercepter l'EF Core 2. Il y a un article à ce sujet à https://weblogs.asp.net/ricardoperes/interception-in -entity-framework-core et une discussion sur Puis-je encore configurer un intercepteur dans EntityFramework Core? . Évidemment, EF Core 3 (je pense qu'il arrivera en septembre/octobre) inclura un mécanisme d'interception similaire à celui qui était dans EF pré-Core, voir https://github.com/aspnet/EntityFrameworkCore/issues/ 15066
J'espère que cela vous sera utile.
De projet en projet je rencontre ce problème sur de larges plateformes (pas seulement .Net). Du point de vue de l'architecture, je peux proposer plusieurs décisions qui ne sont pas propres à EntityFramework. (Quant à moi # 2, c'est mieux)
OPTION 1 pour implémenter une approche de verrouillage optimiste. En général, l'idée ressemble à: "Mettons à jour le client puis vérifions l'état du parent". Vous avez déjà mentionné l'idée "Utiliser la transaction", mais un verrouillage optimiste peut simplement réduire le temps nécessaire pour conserver l'entité parent. Quelque chose comme:
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();
}
Remarque! Selon les paramètres du serveur SQL (niveaux d'isolement), vous devrez peut-être appliquer à l'entité parent sélectionner pour la mise à jour les pls y voient comment le faire. Comment implémenter Select For Update dans EF Core
OPTION 2 Quant à moi, une meilleure approche au lieu d'EF pour utiliser SQL explicite quelque chose comme:
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
Après cette requête en code .Net, vous devez vérifier qu'exactement 1 ligne a été affectée (0 signifie que Parent.Rowversion
a été modifié)
if(_db.SaveChanges() != 1 )
throw new Exception();
Essayez également d'analyser le modèle de conception "Global Lock" à l'aide d'une table DB supplémentaire. Vous pouvez lire sur cette approche http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html