17
votes

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

J'ai une entité parente dont j'ai besoin pour faire une vérification de la concurrence (comme annotée ci-dessous)

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

J'ai un tas de processus clients qui accèdent aux valeurs en lecture seule de cette entité parente et mettent principalement à jour ses entités enfants .

La contrainte

  1. Les clients ne doivent pas interférer avec le travail de chacun (par exemple, la mise à jour des enregistrements enfants ne doit pas lever d'exception de concurrence sur l' entité parente ).

  2. J'ai un processus serveur qui fait mettre à jour cette entité mère, et dans ce cas le processus client a besoin de jeter si l'entité mère a été modifiée.

Remarque : la vérification de la concurrence du client est sacrificielle, le flux de travail du serveur est essentiel à la mission.

Le problème

Je dois vérifier (à partir du processus client ) si l' entité parente a changé sans mettre à jour la version de ligne de l'entité parente .

Il est assez facile de faire une vérification de la concurrence sur l' entité parente dans EF :

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

Le IsModified = true est le facteur de rupture , car il force la version de la ligne de changement. Ou, dit en contexte, cette vérification du processus client provoquera un changement de version de ligne dans l' entité parente , ce qui interfère inutilement avec les flux de travail des autres processus client .

Une SaveChanges : je pourrais potentiellement SaveChanges les SaveChanges du processus client dans une transaction, puis une lecture ultérieure de la version de ligne de l'entité parente, à son tour, en revenant si la version de ligne a changé.

Sommaire

Y at - il un hors-the-box chemin avec Entity Framework où je peux SaveChanges (dans le processus client pour les entités enfants) mais aussi vérifier si la version de ligne de l'entité mère a changé (sans mettre à jour les entités mères version de ligne).


7 commentaires

Est-il possible d'utiliser la fonction de rowversion de sqlserver? docs.microsoft.com/en-us/sql/t-sql/data-types/...


@ilkerkaran oui je l'utilise, mais c'est plus un cas de savoir comment vérifier un changement de concurrence sur une table parent sans changer la rowversion sur cette table afin que SaveChanges échoue dans le cadre d'entité.


Ce problème est-il similaire au vôtre? social.msdn.microsoft.com/Forums/en-US/… .


Existe-t-il de nombreuses méthodes qui modifient les parents et les enfants, ou seulement quelques-unes? En d'autres termes, le contournement de votre transaction conduirait-il à un code répétitif?


@GertArnold salut, Ouais, les clients auraient besoin de protéger les endroits où ils mettent à jour les enregistrements enfants, il n'y a qu'une poignée d'endroits, peut-être jusqu'à 10, ce n'est pas un facteur décisif. Je pourrais facilement encapsuler une logique de transaction dans une méthode, ce serait bien s'il y avait une autre manière atomique.


Je pense que le contournement de votre transaction n'est pas si mal. Esp. une fois encapsulé, l'avantage est que tout est au même endroit, donc ce qui se passe est clair et il est peu probable qu'il y ait des effets secondaires. Toute autre solution, par exemple en utilisant les intercepteurs d'arbre de commande d'EF, consistera en des parties de code séparées, ce qui facilitera la rupture de quelque chose de chaque côté du spectre. Fe tout échoue si les rowids parents ne sont pas extraits de la base de données, ce qui est facile à oublier lorsque le code ne montre pas pourquoi / où ils sont nécessaires. De plus, peut-être plus important encore, cela vous lie à cette version d'EF.


Vous pouvez utiliser l'attribut [ConcurrencyCheck] au-dessus de l'une des propriétés, ce qui provoque un bon contrôle de concurrence pour l'ensemble de la table en fonction de mon expérience.


3 Réponses :


3
votes

Eh bien, ce que vous devez faire est de vérifier le jeton d'accès concurrentiel (horodatage) de l'entité parente lorsque vous écrivez dans l'entité enfant. Le seul défi est que l'horodatage parent ne se trouve 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 qu'EF Core lèvera l'exception de concurrence si une UPDATE ou DELETE affecte zéro ligne. Pour implémenter le test de concurrence, EF ajoute une clause WHERE testant le jeton de concurrence, puis teste si le nombre correct de lignes a été affecté 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 pourrez peut-être 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 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 commentaires

Eh bien, d'abord, ce n'est pas EF Core, mais EF6 - j'ai spécifiquement demandé à OP et ils ont ajouté la balise correcte, qui aurait dû être là au moment de la réponse. Deuxièmement, en ce qui concerne le seul défi - pour moi, c'est un défi de taille. Bien que dans l'ensemble l'idée semble correcte, j'aimerais voir cela implémenté de manière générique, en particulier avec le modèle de métadonnées EF6 assez contre-intuitif.


Oui, il s'agit certainement d'un cadre d'entité et non de base, je vais jeter un coup d'œil à cette approche aujourd'hui et voir si cela s'applique.


Le dernier lien cité dans ma réponse ci-dessus rappelle le problème et nomme les classes pertinentes pour résoudre le problème pré-Core.



7
votes

Il existe une solution étonnamment simple, "out-of-2-box", mais elle nécessite deux modifications que je ne suis pas sûr que vous puissiez ou voulez faire:

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

Laissez-moi vous montrer comment cela fonctionne. Tout est assez simple.

Modèle de base de données:

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

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

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

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

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

Le rassembler

Ce morceau de code met à jour un Child tandis qu'un faux utilisateur simultané met à jour son Parent :

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

Maintenant SaveChanges lève l' DbUpdateConcurrencyException requise. Lorsque la mise à jour du parent est commentée, la mise à jour de l'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 d'un ORM qui prend en charge la concurrence optimiste. Un futur passage à EF-core ne sera pas un problème.


6 commentaires

C'est en fait une solution très soignée et une solution créative, également une nouvelle façon d'obtenir le résultat souhaité avec la vue. mal jouer avec au travail aujourd'hui, merci!


"out-of-2-boxes" - lol, j'aime ça! Cela aurait été bien si EF nous permettait de créer une telle "vue" avec du code (colonne de type d'expression?), Mais ce n'est pas le cas, donc cela semble être la meilleure combinaison des "deux mondes" :)


@Ivan tnx! Ouais, EF pourrait faire avec un peu plus de conscience graphique / agrégé, même si je pense que EF-core est un "petit pas" dans cette direction. Je vous considère capable de concrétiser cette idée de colonne de type expression en une pull request faisant un "pas de géant" dans ce domaine. Cela ne prend qu'un peu de temps ...


Cela répond le mieux à la question dans l'esprit de ce que je demandais


Très bonne réponse! Je suis simplement curieux de savoir comment créer la vue lorsque vous utilisez EF Code First pour la définition du modèle.


@ sjb-sjb "comment créer la vue" englobe beaucoup de choses. Il est préférable de poser une nouvelle question, c'est un problème pour vous.



3
votes

De projet en projet je rencontre ce problème sur une large plateforme (pas seulement .Net). Du point de vue de l'architecture, je peux proposer plusieurs décisions qui ne sont pas propres à EntityFramework. (Quant à moi, c'est mieux)

OPTION 1 pour mettre en œuvre 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 «Pour utiliser la transaction», mais le verrouillage optimiste peut simplement réduire le temps nécessaire pour conserver l'entité Parent. Quelque chose comme:

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

Remarque ! En fonction des paramètres du serveur SQL (niveaux d'isolation), vous devrez peut-être appliquer à l'entité parente de sélection pour la mise à jour. Veuillez y voir comment le faire. Comment implémenter Select For Update dans EF Core

OPTION 2 Quant à moi, il vaut mieux approcher au lieu d'EF d'utiliser du 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 dans le code .Net, vous devez vérifier qu'exactement 1 ligne a été affectée (0 signifie que Parent.Rowversion a été modifié)

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

Essayez également d'analyser le modèle de conception "Global Lock" à l'aide d'une table DB supplémentaire. Vous pouvez lire cette approche ici http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html


0 commentaires