在 Firebird 的服务器端使用生成器值时,如何不浪费它们?

Posted

技术标签:

【中文标题】在 Firebird 的服务器端使用生成器值时,如何不浪费它们?【英文标题】:How do I not waste Generator values when using them server side with Firebird? 【发布时间】:2015-01-25 12:58:50 【问题描述】:

检查这段使用生成器在 Firebird 表中创建唯一主键的简单代码:

CREATE OR ALTER TRIGGER ON_BEFOREINSERT_PK_BOOKING_ITEM FOR BOOKING_ITEM BEFORE INSERT POSITION 0
  AS
  BEGIN
    IF ((NEW.booking_item_id IS NULL) OR (NEW.booking_item_id = 0)) THEN BEGIN
      SELECT GEN_ID(LastIdBookingItem, 1) FROM RDB$DATABASE INTO :NEW.booking_item_id;
    END
  END!

此触发器抓取并递增,然后为预订项目 ID 分配生成的值,从而为 BOOKING_ITEM 表创建一个自动递增的键。触发器甚至会检查预订 ID 是否尚未被赋值。

问题是如果由于某种原因无法发布 BOOKING_ITEM 记录,则自动递增的值将丢失(浪费)。

关于如何避免这种浪费,我有几个想法,但对每一个都有顾虑。他们在这里:

    如果发生过帐错误,则减少计数器。在触发器中,我设置了一个 try-except 块(在 Firebird PSQL 中是否存在 try-except 块?)并在发布异常时运行 SELECT GEN_ID(LastIdBookingItem, -1) FROM RDB$DATABASE这行得通吗?如果另一个事务在我递减之前潜入并递增生成器怎么办?那真的会把事情搞砸。

    使用临时 ID。将 id 设置为一些唯一的临时值,我将其更改为我想要在插入后触发的生成器值。 这种方法感觉有些做作,需要一种方法来确保临时 id 是唯一的。但是,如果客户端提供了 booking_item_id 怎么办,我如何将它与临时 ID 区分开来?另外我需要另一个触发器

    使用事务控制。这类似于选项 1。除了使用 try-except 块来重置生成器之外,我启动了一个事务,然后如果记录无法发布,则将其回滚。 我不知道使用事务控制的语法。我以为我在某处读到 PSQL 中不允许 SAVEPOINT/SET TRANSACTION。另外,回滚必须在 AFTER INSERT 触发器中发生,所以我需要另一个触发器。

对于任何想要使用生成器的 Firebird 开发人员来说,这无疑是一个问题。还有其他想法吗?我有什么遗漏吗?

【问题讨论】:

答案与所有​​其他使用序列的 DBMS 相同:不用担心。序列并非设计为无间隙的,它们的值是没有意义的。它们的唯一目的是充当人工主键。而已。如果您的应用程序依赖于无间隙值,则您根本无法使用序列(在此站点中搜索“无间隙序列”以获取替代方案)。 我正在这个网站上搜索“无间隙序列”的替代方案。谢谢你。大多数情况下,“谁在乎”你是对的,但对于某些表格,我正在生成一个序列号并将其用作发票号。有时我们的审计员要求我们开具发票 X 到 X+n。当然,并非所有发票都必须有效,有些可能会被取消,但他们想知道是否缺少任何数字。他们会问诸如“发票 12345 在哪里”之类的问题? 对,对于发票编号,您需要不同的东西。一个序列不适合那个。 SELECT GEN_ID(LastIdBookingItem, 1) FROM RDB$DATABASE INTO :NEW.booking_item_id; 只是一种非常奇怪和过度设计的方式来表达简单的NEW.booking_item_id = GEN_ID(LastIdBookingItem, 1); 我不知道你能做到这一点@Arioch。谢谢你的提示。 【参考方案1】:

如果您不使用生成器,而是创建一个包含与生成器数量一样多的列的表,并为每列指定生成器的名称,该怎么办。比如:

create table generators
(
 invoiceNumber  integer default 0 not null,
 customerId     integer default 0 not null,
 other generators...
)

现在,您有一个表,您可以在其中使用事务中的 SQL 增加发票编号,例如:

begin transaction
  update generator set invoiceNumber = invoiceNumber + 1 returning invoiceNumber;
  insert into invoices set ..........  
end transaction. 

如果出现任何问题,事务将与新的事务一起回滚 发票编号。我认为序列中不会再有间隙了。

埃尼奥

【讨论】:

【参考方案2】:

序列不受事务控制,干预它们以获得“无间隙”数字只会造成麻烦,因为另一个事务也可能同时增加序列,导致间隙+重复而不是没有间隙:

开始:生成器值 = 1 T1:增量:值为 2 T2:增量:值为 3 T1:“回滚”,递减:值为 2(而不是您期望的 1) T3:增量:值为 3 => 重复值

序列应该主要用于生成人工主键,您不应该关心是否存在间隙:只要数字唯一标识记录就可以了。

如果您需要一个可审计的数字序列,并且要求没有间隙,那么您不应该使用数据库序列来生成它。您可以在创建并提交发票本身后使用序列来分配编号(以确保它被保留)。没有编号的发票根本还不是最终的。然而,即使在这里也有一个机会窗口,例如,如果在分配发票编号和提交之间发生错误或其他失败。

另一种方法可能是使用空白编号显式创建零发票(标记为已取消/编号丢失),以便审计员知道该发票发生了什么。

根据当地法律法规,您不应“重复使用”或回收丢失的号码,因为这可能被视为欺诈。

您可能会在"An Auditable Series of Numbers" 中找到其他想法。这也包含一个使用 IBObjects 的 Delphi 项目,但文档本身很好地描述了问题和可能的解决方案。

【讨论】:

以上是关于在 Firebird 的服务器端使用生成器值时,如何不浪费它们?的主要内容,如果未能解决你的问题,请参考以下文章

如何为 windows 的 firebird 数据库创建多个实例(用户)?

当下一个方法应该返回两个生成器的值时,如何为类创建下一个方法?

Delphi:如何以编程方式创建 Firebird 数据库

如何处理以下数据

Firebird - PGSQL 中的串行字段? [复制]

Firebird最小服务器安装