在 Entity Framework 的 2 个语句中锁定 table

Locking a table during 2 statements in Entity Framework

我有以下陈述:

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

InvoiceNumber 列应该是唯一的,但这种方法很危险,因为从我获得 newId 的值开始,另一条记录可能会添加到 table。我读到锁定 table 可以解决这个问题,但我不确定我应该如何使用 Entity Framework.

来实现这个

此外,我认为也许在事务中执行此操作就足够了,但有人说不行。

更新

假设这是 Entity Framework 中的 table 定义。

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
}

现在,显然Id是table的主键。我可以将 InvoiceNumber 标记为身份:

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

现在,如果我这样做就可以了:

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

并且它会自动为 InvoiceNumber 生成一个唯一的编号。但是每当我更改记录时,我都需要更改此数字。例如:

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

我试图将其设置为 0,但是当我尝试为其赋值时,我收到一条错误消息,指示我无法将其赋值给标识列。这就是我尝试手动执行此操作的原因。

(此答案与上面的评论主题相关)

将对象添加到 table 时,会为其分配一个标识。

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;

请注意,在数据库 returns 给您之前您不知道 ID 是什么。数据库在创建时决定了 ID 是什么

如果您先发制人地检查 table 中当前最高的 ID,然后执行类似 int myNewId = tableMaxId + 1; 的操作,如果其他人已经在其中保存对象,您将 运行 陷入困境table 而您的代码仍在进行先发制人的猜测。

永远不要猜 ID 是什么。始终将对象保存到数据库中,这样数据库可以确保分配的 ID 永远不会分配给其他人。

注意:如果此时您需要 ID,但您仍然可能需要取消交易,请采用后备方案,删除之前创建的对象。

像这样:

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

这只是一个简单粗暴的例子。我希望你明白了它的要点,你需要实际 创建对象才能知道它被分配了哪个身份 。在不同应用程序或线程在同一数据库中保存对象的情况下,猜测 ID 可能会导致失败 table。

And it will automatically generate a unique number for InvoiceNumber. But I need this number to change whenever I change the record.

此功能在 SQL 服务器中,由 rowversion (timestamp) column 提供。

只需将 table 列的类型更改为 rowversion / timestamp。 rowversion列肯定不能作为主键。

使用 rowversion 列,您无需担心 table 锁定或事务,其唯一值由 SQL 服务器自动维护。

如果你有理由不使用时间戳,你可以使用 SQL Server sequences combined with triggers:

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

如果可能,最好尽可能使用内置功能在 SQL 数据库中处理身份或时间戳 inserts/updates。

See SQL Fiddle.

您可以通过创建与

的关系来做到这一点
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; }
}

当您保存 OnlinePaymentInfo 时,您会这样做

paymentInfo.InvoiceNumber = new InvoiceNumber();

然后 EF 将为 InvoiceNumber 创建一个新的身份,这将是唯一的。

ps。不完全确定代码首先的语法。

如果您的任务只是保持发票编号的唯一性,您可以为此使用 rowversion 或 uniqueidentifier 类型,但我同意,这是不可读的。一种方法是使用单独的 table,例如:

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

然后您可以创建存储过程,将向此 table 和 return 插入一行插入标识,例如:

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

然后在任何需要生成新号码的地方只需调用该过程并获取发票号码的下一个唯一值。请注意,您可以使用这种方法获得差距。

下面是一个如何使用代码优先方法调用存储过程并获取输出参数的示例EF4.1 Code First: Stored Procedure with output parameter

如果您有 Sql Server 2012+ 版本,那么您可以使用新功能 Sequence objects 而不是创建新的 table 来生成标识值。

我建议您不要尝试重新发明轮子。 DB 具有 EF 公开的良好功能。 SQL rowversion

属性 EF Code annotation timestamp

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

然后您可以使用乐观锁定方法。

如果您渴望拥有自己的锁定控制。使用数据库功能并调用它。 SQL server application level locks

另一种选择是使用第二个 table

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

然后从 table 获取下一行 这只提供一次 Id,并且很容易构建。

你可以用存储过程解决这个问题:

首先创建一个 table 命名序列:

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

然后播种数据(你做过一次)

insert into NamedSequence values ('INVOICE_NO', 1)

然后这个过程:

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

因为您在与更新相同的语句中设置 return 值,所以它将是原子的——您永远不会得到重复项。如果您在需要序列的发票编号之外有其他情况,您只需将带有新键和起始值的行添加到 table.