我有一个法律要求,在我们的应用程序(使用 SQL Server)的发票集合中,我们的编号不能有间隙。因此,如果这些是发票编号,则不允许这样做:[1, 2, 3, 4, 8, 10]
因为它不是连续的。为此,我们的桌子InvoiceNumber
上有一个列Invoices
。除此之外,我们还有一个InvoiceNumbers
表格,其中包含每个组织的当前发票编号(因为每个组织都需要有自己的序列)。然后一个存储过程负责原子地InvoiceNumber
填充;Invoices
它要么将表中的当前计数器增加 1InvoiceNumbers
并将新值填充到Invoices
表中,要么在发生错误时回滚事务。这很好用。
现在添加了一个新要求:某些订单必须共享同一张发票,因此必须使用相同的发票编号,而以前每个订单都是单独开票的。为此,我们在一天开始时创建一张发票,并将其与当前FinancialPeriod
(基本上是工作日)相关联,这将是用于每个订单的发票。但是,一个组织可能不会创建任何需要共享发票类型的订单,因此在一天内没有任何可开票的东西,这会“浪费”最初创建的发票(因为第二天会创建一个新发票)并创建一个差距。
现在,对我来说最简单的解决方案是懒洋洋地填写在InvoiceNumber
一天开始时创建的共享发票。如果当天创建了订单并且InvoiceNumber
仍然是NULL
,则创建该号码。这将确保 InvoiceNumber 永远不会被使用(Invoice
记录未被使用并不重要,它没有真正的意义)。
为此,我创建了下面的存储过程,对于现有的Invoice
,填充InvoiceNumber
但仅当它仍然是NULL
. 我只是不确定 SQL Server 是如何锁定的,以及是否有可能出现竞争条件,即两个数据库事务决定它InvoiceNumber
仍然存在NULL
,并且都会增加计数器并浪费一个数字,从而产生间隙。
从本质上讲,这个冗长的问题归结为:两个同时的数据库事务是否可以决定在这里输入if(@currentNumber is null)
相同的块@invoiceID
?
您看到的锁定部分是从这里得到的,但我不确定它是否适用于我的情况:
CREATE PROCEDURE [dbo].[CreateInvoiceNumber]
@invoiceID int,
@appID int
AS
BEGIN
SET NOCOUNT ON;
if not exists (select 1 from InvoiceNumbers where ApplicationID = @appID) insert into InvoiceNumbers values (@appID, 1)
declare @currentNumber int = null;
select @currentNumber = convert(int, i.InvoiceNumber)
from Invoices i
with (HOLDLOCK, ROWLOCK)
where i.ID = @invoiceID
if(@currentNumber is null)
begin
update InvoiceNumbers set @currentNumber = Value = Value + 1
where ApplicationID = @appID
update Invoices set InvoiceNumber = @currentNumber where ID = @invoiceID
end
select convert(nvarchar, @currentNumber)
END
编辑
正如我在评论中提到的,这些和其他写入操作是从 C# 应用程序逻辑启动的数据库事务的一部分。BeginTransaction
只是带有默认选项的常规SqlConnection
选项,如果出现任何异常,它当然会回滚。