web-dev-qa-db-fra.com

Vérification de la concurrence sur une entité sans mettre à jour la version de ligne

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

  1. 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é ).

  2. 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 ).

17
Michael Randall

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:

  • Créez une vue modifiable sur la table enfant contenant une colonne ParentRowVersion
  • Mapper l'entité enfant à cette vue

Permettez-moi de montrer comment cela fonctionne. C'est assez simple.

Modèle de base de données:

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 .

Les données

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

Modèle de classe

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; }
}

Le contexte

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();
    }
}

Le rassembler

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.

6
Gert Arnold

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.

3
sjb-sjb

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

2
Dewfy