C# 正确生成唯一的发票id

C# 正确生成唯一的发票id,c#,asp.net-mvc,multithreading,entity-framework,C#,Asp.net Mvc,Multithreading,Entity Framework,我被要求清理其他人的控制器代码,这会生成发票,但我遇到了一些我不知道如何修复的问题。问题代码如下(这是使用EF 6:代码优先): 代码应该根据创建发票的公司生成一个invoiceId。下面是一个小表格: ------------------------------ | Id | CompanyId | InvoiceId | ------------------------------ | 1 | 1 | 1 | ------------------------

我被要求清理其他人的控制器代码,这会生成发票,但我遇到了一些我不知道如何修复的问题。问题代码如下(这是使用EF 6:代码优先):

代码应该根据创建发票的公司生成一个
invoiceId
。下面是一个小表格:

------------------------------
| Id | CompanyId | InvoiceId |
------------------------------
|  1 |         1 |         1 |
------------------------------
|  2 |         1 |         2 |
------------------------------
|  3 |         1 |         3 |
------------------------------
|  4 |         2 |         1 |
------------------------------
|  5 |         2 |         2 |
------------------------------
如您所见,
invoiceId
将根据相关公司的当前发票数量生成。但是,我认为有理由建议两个线程可以在计算此行之前执行查询:

var invoiceId = invid == null ? 1 : (int)invid + 1;
这将导致为两个不同的发票生成相同的
invoiceId

create procedure GetInvoiceIdForCustomer
  @CustomerId as Int,
  @InvoiceId as Int Output
as
begin
  set nocount on

  begin transaction

  update CustomerInvoiceNumbers 
    set @InvoiceId = NextId, NextId += 1
    where CustomerId = @CustomerId

  if @@RowCount = 0
    begin
    set @InvoiceId = 1
    insert into CustomerInvoiceNumbers ( CustomerId, NextId ) values ( @CustomerId, @InvoiceId + 1 )
    end

  commit transaction
end       

end

是否有一个简单的解决方案,可以利用实体框架自动执行此操作?

如果在SQL Server中使用
标识
字段,这将自动处理。

如果在SQL Server中使用
标识
字段,这将自动处理。

有两种可能性:

服务器端:不计算客户端上的max(ID)+1。相反,作为INSERT语句的一部分,通过INSERT..SELECT语句计算max(ID)+1

lock (myLock)
{
     var invid = db.TransportJobInvoice.Where(c => c.CompanyId == CompanyId)
            .Max(i => i.InvoiceId);
     var invoiceId = invid == null ? 1 : (int)invid + 1;
}
客户端:在客户端生成GUID,并将其用作InvoiceID,而不是递增的int。

有两种可能:

服务器端:不计算客户端上的max(ID)+1。相反,作为INSERT语句的一部分,通过INSERT..SELECT语句计算max(ID)+1

lock (myLock)
{
     var invid = db.TransportJobInvoice.Where(c => c.CompanyId == CompanyId)
            .Max(i => i.InvoiceId);
     var invoiceId = invid == null ? 1 : (int)invid + 1;
}

客户端:在客户端生成GUID,并将其用作发票id,而不是递增的int。

我不知道是否可以自动生成发票id,除非它被视为外键(我认为不是)

您可以使用lock语句解决多线程的问题

lock (myLock)
{
     var invid = db.TransportJobInvoice.Where(c => c.CompanyId == CompanyId)
            .Max(i => i.InvoiceId);
     var invoiceId = invid == null ? 1 : (int)invid + 1;
}
这将保证只有线程在执行这些语句


但是要小心,当这些语句并行执行时,这可能会导致性能问题,并且查询需要花费大量时间来执行。

我不知道是否可以自动生成发票id,除非它被当作外键威胁(我认为不是)

您可以使用lock语句解决多线程的问题

lock (myLock)
{
     var invid = db.TransportJobInvoice.Where(c => c.CompanyId == CompanyId)
            .Max(i => i.InvoiceId);
     var invoiceId = invid == null ? 1 : (int)invid + 1;
}
这将保证只有线程在执行这些语句


但是要小心,当这些语句大量并行执行时,这可能会导致性能问题,并且查询需要花费大量时间来执行。

我建议使用标识作为主键,这一点非常重要

然后,我将为“CustomerVoiceId”添加一列,并在CustomerID和CustomerVoiceId上放置一个复合唯一键

然后,创建一个存储过程,在插入CustomerInvoiceID字段后,该存储过程将填充该字段,下面是一些伪代码:

CREATE PROCEDURE usp_PopulateCustomerInvoiceID 
    @PrimaryKey INT, --this is your primary key identity column
    @CustomerID INT
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @cnt INT;

    SELECT @CNT = COUNT(1)
    FROM TBL 
    WHERE CustomerID = @CustomerID
        AND PrimaryKeyColumn <= @PrimaryKey

    UPDATE tbl
    SET CustomerInvoiceID = @cnt + 1
    WHERE PrimaryKeyColumn = @PrimaryKey
END
创建过程usp\u populateCustomerVoiceId
@PrimaryKey INT,--这是您的主键标识列
@客户ID INT
作为
开始
不计数;
声明@cnt INT;
选择@CNT=COUNT(1)
来自TBL
其中CustomerID=@CustomerID

PrimaryKeyColumn我建议使用主键的标识,这非常重要

然后,我将为“CustomerVoiceId”添加一列,并在CustomerID和CustomerVoiceId上放置一个复合唯一键

然后,创建一个存储过程,在插入CustomerInvoiceID字段后,该存储过程将填充该字段,下面是一些伪代码:

CREATE PROCEDURE usp_PopulateCustomerInvoiceID 
    @PrimaryKey INT, --this is your primary key identity column
    @CustomerID INT
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @cnt INT;

    SELECT @CNT = COUNT(1)
    FROM TBL 
    WHERE CustomerID = @CustomerID
        AND PrimaryKeyColumn <= @PrimaryKey

    UPDATE tbl
    SET CustomerInvoiceID = @cnt + 1
    WHERE PrimaryKeyColumn = @PrimaryKey
END
创建过程usp\u populateCustomerVoiceId
@PrimaryKey INT,--这是您的主键标识列
@客户ID INT
作为
开始
不计数;
声明@cnt INT;
选择@CNT=COUNT(1)
来自TBL
其中CustomerID=@CustomerID

PrimaryKeyColumn一种完全不同的方法是为每个
CustomerId
创建一个单独的表,其中包含
NextId
。添加新客户后,您将在此表中添加新行。它的优点是,即使允许删除发票,分配给发票的编号也可以保持唯一

create procedure GetInvoiceIdForCustomer
  @CustomerId as Int,
  @InvoiceId as Int Output
as
begin
  set nocount on

  begin transaction

  update CustomerInvoiceNumbers 
    set @InvoiceId = NextId, NextId += 1
    where CustomerId = @CustomerId

  if @@RowCount = 0
    begin
    set @InvoiceId = 1
    insert into CustomerInvoiceNumbers ( CustomerId, NextId ) values ( @CustomerId, @InvoiceId + 1 )
    end

  commit transaction
end       

end

一种完全不同的方法是为每个
CustomerId
创建一个单独的表,其中包含
NextId
。添加新客户后,您将在此表中添加新行。它的优点是,即使允许删除发票,分配给发票的编号也可以保持唯一

create procedure GetInvoiceIdForCustomer
  @CustomerId as Int,
  @InvoiceId as Int Output
as
begin
  set nocount on

  begin transaction

  update CustomerInvoiceNumbers 
    set @InvoiceId = NextId, NextId += 1
    where CustomerId = @CustomerId

  if @@RowCount = 0
    begin
    set @InvoiceId = 1
    insert into CustomerInvoiceNumbers ( CustomerId, NextId ) values ( @CustomerId, @InvoiceId + 1 )
    end

  commit transaction
end       

end

但它需要每个公司增加,而不是表global。您介意详细说明一下吗
invoiceId
不是密钥的一部分。如何强制
Identity
为每个公司生成序列号?抱歉,我错过了每个公司的部分。不过为什么要麻烦呢。只要每个invoiceId是唯一的,它们也将是每个公司的。会有间隙,但这不应该是一个问题。您肯定应该使用表上主键的标识。您应该使用一个单独的列,该列仅针对客户。但它需要按公司递增,而不是按表全局递增。您介意详细说明一下吗
invoiceId
不是密钥的一部分。如何强制
Identity
为每个公司生成序列号?抱歉,我错过了每个公司的部分。不过为什么要麻烦呢。只要每个invoiceId是唯一的,它们也将是每个公司的。会有间隙,但这不应该是一个问题。您肯定应该使用表上主键的标识。你应该使用一个单独的专栏,这是唯一的客户。谢谢乔迪。我更喜欢数据完整性和牺牲一些性能,而不是相反。注意-在ASP.NET中使用
lock
时必须小心,因为锁只适用于单个进程。谢谢Jordy。我更喜欢数据完整性和牺牲一些性能,而不是相反