在Entity Framework中,在2个语句期间锁定一个表

4

我有以下陈述:

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

InvoiceNumber列应该是唯一的,但这种方法很危险,因为在我获取newId值的同时,另一条记录可能会被添加到表中。我读到锁定表可以解决这个问题,但我不确定如何使用Entity Framework实现。

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

更新

假设这是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
}

显然,Id 是该表的主键。我可以通过以下方式将 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,但当我尝试为其分配一个值时,出现了错误,指示我无法为标识列分配值。这就是我试图手动完成的原因。


请纠正我,但标识符的实际值是无关紧要的,只要您有正确的标识符即可,对吗?您不必担心OnlinePayments表中使用的ID,因为数据库将确保它是唯一的(您会得到下一个排队的ID)。从那时起,您可以确信该标识符是唯一的,并且任何其他对象(例如opi.Invoicenumber)都可以使用它来引用它。 - Flater
@Flater 是的,我只需要一个唯一的值。实际的值并不重要。我的表已经有一个标记为“Identity”的“Id”,我知道我也可以将“InvoceNumber”设置为“Identity”,但问题是,我无法编辑一个“Identity”列。然而,每当我修改记录时,我需要为这个列获取一个新的唯一值。 - Alireza Noori
让我来回答。我认为你误解了ID分配的方式。 - Flater
发票号码必须是int类型吗?例如,它可以是Guid吗?此外,如果您解释了为什么每次发票更改时都必须更改InvoiceNumber,那可能会有所帮助-不明白为什么发票号码会更改(实际上,在许多情况下,出于审计原因,一旦发票已经发布并且第三方正在使用该InvoiceNumber作为参考,就不允许这样做)。您是否将InvoiceNumber用作某种版本号或其他内容? - Stephen Byrne
在SQL Server 2012及以上版本中,请使用“SEQUENCE”,在早期版本中,请使用带有“IDENTITY”的专用表进行模拟。啊,我看到@Giorgi Nakeuri已经提供了这个答案。主要思想是每当您需要一个新的唯一发票号码时,请向数据库请求。 - Vladimir Baranov
显示剩余4条评论
6个回答

3

如果您的任务仅是保持发票号码唯一,那么您可以使用rowversion或uniqueidentifier类型来实现,但我同意,这不够易读。一种方法是创建一个单独的表格,例如:

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

您可以创建存储过程,向该表插入一行数据,并返回插入的标识符,如下:

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代替创建新表以生成标识值。


2
“它将自动生成一个唯一的发票号码(InvoiceNumber)。但是,每次记录更改时,我需要该编号也随之更改。”
SQL Server提供了一个名为rowversion (timestamp) column的功能来实现此功能。
只需将表列的类型更改为rowversion/timestamp即可。rowversion列当然不能用作主键。
使用rowversion列,您无需担心表锁定或事务,其唯一值由SQL Server自动维护。
如果您有不使用时间戳的理由,可以结合SQL Server序列触发器使用。
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数据库中处理身份或时间戳的插入/更新。

请参见SQL Fiddle


我想要这样做,但我需要一个整数。 - Alireza Noori
有没有办法根据“rowversion”获取一个数字,或者告诉SQL服务器使用数字而不是时间戳? - Alireza Noori
不是这样想,但你可以计算它。为什么你需要以那种方式,我并不完全理解你的目标。 - Vojtěch Dohnal
如果您需要自动增加的数字但不使用标识列,请使用序列。https://msdn.microsoft.com/zh-cn/library/ff878091.aspx?f=255&MSPPError=-2147217396 - Vojtěch Dohnal

2
您可以通过创建关系来实现这一点。
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创建一个新的身份,这将是唯一的。

注:对于Code First的语法不是完全确定。


这个方法可以行,但是之前数据库里的记录怎么办?另外,我应该如何删除冗余记录? - Alireza Noori
您的意思是指不再使用的发票号码等冗余记录?它们真的很重要吗?如果您从上下文中获取PaymentInfo而不是创建新的PaymentInfo,那么您的OnlinePaymentInfo将不会有任何冗余。 - Janne Matikainen
它们并不是“那么”重要。但我宁愿在我的数据库中没有任何冗余数据。 - Alireza Noori
你可以在分配新的发票号码之前添加一行代码 db.InvoiceNumbers.Remove(paymentInfo.InvoiceNumber);,这样在保存更改时就会删除旧的发票号码。 - Janne Matikainen
谢谢。这似乎是实现我想要的最好和最合理的方法。 - Alireza Noori

1

(此答案与上面的评论线有关)

向表中添加对象时,它将被分配一个身份标识。

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;

请注意,直到数据库返回ID给您之前,您不知道ID是什么。数据库在创建时决定了ID。
如果您预先检查表中当前最高的ID,然后执行类似于int myNewId = tableMaxId + 1;的操作,那么如果其他人在您的代码仍在进行预测时已经在表中保存对象,则会遇到问题。
永远不要猜测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可能会导致失败,在不同的应用程序或线程将对象保存在同一数据库表中的情况下。

谢谢您的回答。这将有效,直到我需要更改ID。请阅读上面的更新。 - Alireza Noori
为什么你想要改变ID呢?这似乎违背了ID的主要目的,即它是一个身份标识,其值是无关紧要的,只有在使用中保持一致性才是重要的。你能解释一下为什么需要更改ID吗? - Flater
不,我不想改变 Id。我想改变的是 InvoiceNumber。我将此号码发送到银行,银行需要为每次付款提供唯一的编号。 - Alireza Noori
你不应该将你的身份信息用于除PK / FK之外的任何事情。银行不知道你的数据库,他们也不应该知道。我也不确定这个在银行中需要多独特(是你的发票独特还是所有银行交易独特)。不要使用你的内部主键作为发送到外部的数据。如果你重新创建数据库,身份生成将从1开始重新开始。这不是一个好主意,因此应完全防止。 - Flater
简而言之,您试图以其从未预期的方式使用特定于数据库的功能。外键(InvoiceNumber)“根据定义”必须引用相关表中现有的主键。将其更改为您喜欢的值是不可能的,并且违反了外键的主要约束和固有性质。 - Flater
我不需要使用特定于数据库的功能(即Identity),我只需要生成一个唯一的“发票号码”。我不建议使用“Identity”,因为我认为它不会起作用。 - Alireza Noori

1
我建议您不要试图重新发明轮子。数据库具有EF暴露的良好功能。 SQL rowversion 属性 EF代码注释时间戳
 [Timestamp]  
 public virtual byte[] RowVersion { get; set; }

你可以采用乐观锁定方法。
如果您想拥有自己的锁定控制,请使用数据库功能并进行调用。 SQL服务器应用程序级锁定 另一个选项是使用第二个表格。
  Public class IdPool{
    int Id {get;set}
    string bla {get; set;}
  }

只需从表格中获取下一行即可,这样只提供一次 Id 并且容易构建。


然后将字节数组转换为长整型。在SQL Server中,rowversion始终为8个字节。请参阅https://msdn.microsoft.com/en-us/library/system.bitconverter.toint64.aspx了解如何操作。 - phil soady
这个可以工作,但是有几个问题需要澄清:1. 这个数字在所有记录中是唯一的吗?我不想有多个相同的数字。2. 当我将数字转换为 long 时,它是一个非常大的数字。有没有办法让它连续?(意思是从1或其他开始)。3. 我要查找的数字是32位整数。有没有办法将这个数字转换为更小的32位int?目前我得到的值非常大,无法通过强制转换为int来转换。 - Alireza Noori
是的,rowversion在数据库中是唯一的。它是一个8字节长整型,而不是4字节整型。如果需要Int32 4字节,则只需创建一个简单的表,带有int键并将其设置为DB生成,并在每次需要新的int时添加一行即可。 - phil soady
在这种情况下,@JanneMatikainen的答案似乎是正确的选择。 - Alireza Noori
我的意思是他们的解决方案很好。所以你选择得很好。一切都好,阿里扎。 - phil soady
显示剩余3条评论

1
你可以使用存储过程来解决这个问题:
首先创建一个命名序列的表:
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

由于您在更新语句中设置了返回值,因此它是原子的 -- 您永远不会得到重复值。如果您有其他情况需要序列号(Sequence),只需将具有新键和起始值的行添加到表中即可。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接