Verrouillage d'une table pendant 2 instructions dans Entity Framework

J'ai les déclarations suivantes:

int newId = db.OnlinePayments.Max(op => op.InvoiceNumber) + 1; opi.InvoiceNumber = newId; await db.SaveChangesAsync(); 

La colonne InvoiceNumber doit être unique, mais cette approche est dangereuse car à partir du moment où j'obtiens la valeur de newId , un autre logging peut être ajouté à la table. J'ai lu que le locking de la table permettrait de résoudre ce problème, mais je ne suis pas sûr de la façon dont je devrais y parvenir avec Entity Framework.

Aussi, je pensais que faire cela dans une Transaction serait suffisant mais quelqu'un a dit que ce n'était pas le cas.

Mettre à jour

Supposons que ce soit la définition de la table dans Entity Framework.

 public class OnlinePaymentInfo { public OnlinePaymentInfo() { Orders = new List<Order>(); } [Key] public int Id { get; set; } public int InvoiceNumber { get; set; } public int Test { get; set; } //..rest of the table } 

Maintenant, évidemment, Id est la key primaire de la table. Et je peux marquer InvoiceNumber comme Idenity avec ceci:

  [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int InvoiceNumber { get; set; } 

Maintenant, cela fonctionnera si je fais quelque chose comme ça:

 var op = new OnlinePayment { Test = 1234 }; db.OnlinePayments.Add(op); await db.SaveChangesAsync(); 

Et il générera automatiquement un numéro unique pour InvoiceNumber . Mais j'ai besoin de ce numéro pour changer chaque fois que je change de record. Par exemple:

 var op = await db.OnlinePayments.FindAsync(2); op.Test = 234243; //Do something to invalidate/change InvoideNumber here db.SaveChangesAsync(); 

J'ai essayé de le mettre à 0 mais quand j'essaye d'assigner une valeur, j'obtiens une erreur indiquant que je ne peux pas assigner à une colonne d'identité. C'est la raison pour laquelle j'ai essayé de le faire manuellement.

Une option que vous pourriez faire est de créer une relation avec

 public class OnlinePaymentInfo { public OnlinePaymentInfo() { Orders = new List<Order>(); } [Key] public int Id { get; set; } [Key, ForeignKey("InvoiceNumber")] public InvoiceNumber InvoiceNumber { get; set; } public int Test { get; set; } //..rest of the table } public class InvoiceNumber { public InvoiceNumber() { } [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } } 

Et quand vous enregistrez le OnlinePaymentInfo, vous feriez

 paymentInfo.InvoiceNumber = new InvoiceNumber(); 

Et puis EF créerait une nouvelle identité pour le InvoiceNumber et ce serait unique.

ps. Pas tout à fait sûr de la syntaxe sur le code d'abord.

Si votre mission est simplement de garder les numéros de facture uniques, vous pouvez utiliser les types rowversion ou uniqueidentifier pour cela, mais je suis d'accord, ce n'est pas lisible. Une approche serait d'avoir saparate table comme:

 Create Table InvoiceNumbers(ID bigint identity(1, 1)) 

Ensuite, vous pouvez créer une procédure stockée qui va insert une ligne dans cette table et returnner l'identité insérée comme:

 Create Procedure spGenerateInvoiceNumber @ID bigint output As Bebin Insert into InvoiceNumbers default values Set @ID = scope_identity() End 

Ensuite, partout où vous devez générer un nouveau numéro, appelez ce proc et obtenez la valeur unique suivante pour le numéro de facture. Notez que vous pouvez get des lacunes avec cette approche.

Voici un exemple d'appel de proc mémorisé avec le code d'abord et d'get le paramètre de sortie EF4.1 Code First: procédure stockée avec paramètre de sortie

Si vous avez une version de Sql Server 2012+ vous pouvez utiliser de nouveaux Sequence objects fonctions au lieu de créer une nouvelle table pour générer des valeurs d'identité.

Et il générera automatiquement un numéro unique pour InvoiceNumber. Mais j'ai besoin de ce numéro pour changer chaque fois que je change de record.

Cette fonctionnalité est dans SQL Server fournie par une colonne rowversion (timestamp) .

Changez juste le type de votre colonne de table en rowversion / timestamp. La colonne rowversion ne peut certainement pas être utilisée comme key primaire.

Avec la colonne rowversion, vous n'avez pas besoin de vous soucier du locking de table ou des transactions, sa valeur unique est automatiquement gérée par SQL Server.

Si vous avez une raison de ne pas utiliser d'horodatage, vous pouvez utiliser des séquences SQL Server combinées avec des sortingggers :

 CREATE SEQUENCE dbo.InvoiceNumberSeq AS int START WITH 1 INCREMENT BY 1 ; GO CREATE TRIGGER dbo.SetInvoiceNumber ON dbo.OnlinePayments AFTER INSERT, UPDATE AS BEGIN UPDATE t SET t.InvoiceNumber = NEXT VALUE FOR dbo.InvoiceNumberSeq FROM dbo.OnlinePayments AS t INNER JOIN inserted AS i ON t.ID = i.ID; END GO 

Si possible, il est préférable de gérer autant que possible les insertions / mises à jour d'identité ou d'horodatage dans la database SQL en utilisant des fonctionnalités embeddedes.

Voir SQL Fiddle .

(Cette réponse est en relation avec le fil de commentaires ci-dessus)

Lorsque vous ajoutez un object à une table, une identité lui est affectée.

 Person myPerson = new Person() { Name = "Flater" }; //at this point, myPerson.Id is 0 because it was not yet assigned a different value db.People.Add(myPerson); db.SaveChanges(); //at this point, myPerson.Id is assigned a value because it was saved! int assignedIdentity = myPerson.Id; 

Notez que vous ne connaissez pas l'ID avant que la database ne vous le renvoie. La database a déterminé ce qu'est l'ID au moment de la création .

Si vous vérifiez de manière préventive l'identifiant le plus élevé actuellement dans la table, puis faites quelque chose comme int myNewId = tableMaxId + 1; vous rencontrerez un problème si quelqu'un d'autre sauvegardait déjà des objects dans la table pendant que votre code fait ses suppositions préemptives.

Jamais, ne devinez jamais ce que l'ID sera . Enregistrez toujours l'object dans la database afin que la database puisse s'assurer que l'ID atsortingbué ne sera jamais atsortingbué à quelqu'un d'autre.

Remarque: Si à ce moment-là, vous avez besoin de l'ID, mais il peut toujours être possible que vous deviez annuler la transation, ayez un scénario de secours où vous supprimez l'object proviously créé.

Quelque chose comme ça:

  int the_id_that_was_assigned = 0; //currently not known yet! try { Person myPerson = new Person() { Name = "Flater" }; db.People.Add(myPerson); db.SaveChanges(); the_id_that_was_assigned = myPerson.Id; //now it is known because the object was saved. //do whatever processing you need to do based on that ID //If you want to cancel, you can throw an exception or delete the object manually //If you do not want to cancel, then the Person you just created is excatly what you want. } catch(Exception ex) { if(the_id_that_was_assigned > 0) { db.People.Delete(the_id_that_was_assigned); } } 

Ceci est juste un exemple rapide et sale. J'espère que vous comprenez l'essentiel, vous devez créer l'object pour savoir quelle identité il a été assigné . Deviner l'ID entraînera un échec possible dans les cas où différentes applications ou threads enregistrent un object dans la même table de database.

Je vous recommand de ne pas essayer de réinventer la roue. La database possède de bonnes fonctionnalités que EF expose. Rowversion SQL

properties Horodatage des annotations du code EF

  1514634494 public virtual byte[] RowVersion { get; set; } 

vous pouvez alors utiliser une approche de locking optimiste.

Si vous tenez à avoir votre propre contrôle de locking. Utilisez une fonctionnalité DB et appelez-le. Verrous de niveau d'application SQL Server

Une autre option consiste simplement à utiliser une seconde table

  Public class IdPool{ int Id {get;set} ssortingng bla {get; set;} } 

Et juste get la prochaine ligne de la table Cela fournit l'Id qu'une seule fois et est facilement construit.

Vous pouvez résoudre ceci avec un proc stocké:

Commencez par créer une table de séquences nommées:

 CREATE TABLE [dbo].[NamedSequence]( [SequenceName] [nchar](10) NOT NULL, [NextValue] [int] NOT NULL, CONSTRAINT [PK_NamedSequence] PRIMARY KEY CLUSTERED ( [SequenceName] ASC ) 

Puis graine datatables (vous le faites une fois)

 insert into NamedSequence values ('INVOICE_NO', 1) 

Alors ce proc:

 create procedure GetNextValue @SequenceName varchar(10), @SequenceValue int out as update NamedSequence set @SequenceValue = NextValue, NextValue = NextValue + 1 where SequenceName = @SequenceName go 

Parce que vous définissez la valeur de return dans la même instruction que dans la mise à jour, elle sera atomique – vous n'obtiendrez jamais de duplicates. Si vous avez d'autres cas en dehors du numéro de facture où vous avez besoin de séquences, vous ajoutez simplement une ligne avec une nouvelle key et commencez la valeur à la table.