使用数据库触发器

时间:2015-10-03 16:28:26

标签: php postgresql triggers auto-increment

与我的团队一起,我正致力于生成发票号码的功能。要求说:

  • 发票号码之间应该没有差距
  • 数字应该从每年0开始(与年份一起,我们将有一个唯一的密钥)
  • 发票编号应与创建发票的时间一致

我们正在使用php和postgres。我们应该通过以下方式实现这一点:

  • 每次在数据库上保留新发票时,我们都会使用BEFORE INSERT触发器
  • 触发器执行一个函数,该函数从postgres序列中检索新值并将其作为其编号写在发票上

考虑到在同一笔交易中可以创建多张发票,我的问题是:这是一种足够安全的方法吗?它的缺点是什么?您如何建议改进它?

1 个答案:

答案 0 :(得分:2)

简介

我认为这里最关键的一点是:

  
      
  • 发票号码之间应该没有差距
  •   

在这种情况下,您不能使用序列和自动增量字段(正如其他人在评论中提出的那样)。无论事务成功还是失败,您都可以自动增加字段使用顺序和nextval(regclass)函数递增序列的计数器(您自己指出)。

更新

我的意思是你根本不应该使用序列,特别是你提出的解决方案并没有消除差距的可能性。您的触发器会获得新的序列值,但INSERT仍可能失败。

序列以这种方式工作,因为它们主要用于PRIMARY KEYsOIDs值生成,其中唯一性和非阻塞机制是最终目标,值之间的差距确实没什么大不了的。

在您的情况下,优先级可能会有所不同,但有几点需要考虑。

简单解决方案

您问题的第一个可能的解决方案是将新号码作为当前现有号码的最大值返回。它可以在您的触发器中完成:

NEW.invoice_number =
        (SELECT foo.invoice_number
         FROM invoices foo
         WHERE foo._year = NEW._year
         ORDER BY foo.invoice_number DESC NULLS LAST LIMIT 1
        ); /*query 1*/

此查询可以使用您的复合UNIQUE INDEX,如果它是使用“正确”语法和列顺序创建的,那么它将是第一位的“年”列:

CREATE UNIQUE INDEX invoice_number_unique
ON invoices (_year, invoice_number DESC NULLS LAST);

在PostgreSQL中UNIQUE CONSTRAINTs简单地实现为UNIQUE INDEXes,所以大多数时候你将使用哪个命令没有区别。但是,使用上面提到的特定语法,可以定义该索引的顺序。这是一个非常好的技巧,如果发票表变大,/*query 1*/比简单SELECT max(invoice_number) FROM invoices WHERE _year = NEW.year更快。

这是一个简单的解决方案,但有一个很大的缺点。当两个交易试图同时插入发票时,存在竞争条件的可能性。两者都可以获得相同的最大值,UNIQUE CONSTRAINT将阻止第二个提交。尽管如此,在一些具有特殊插入策略的小型系统中它可能就足够了。

更好的解决方案

您可以创建表格

CREATE TABLE invoice_numbers(
   _year INTEGER NOT NULL PRIMARY KEY,
   next_number_within_year INTEGER
);

存储特定年份的下一个可能的数字。然后,在AFTER INSERT触发器中,您可以:

  1. 锁定invoice_numbers,其他任何交易甚至都无法读取数字LOCK TABLE invoice_numbers IN ACCESS EXCLUSIVE;
  2. 获取新发票号new_invoice_number = (SELECT foo.next_number_within_year FROM invoice_numbers foo where foo._year = NEW.year);
  3. 更新新添加的发票行的数量值
  4. 增量UPDATE invoice_numbers SET next_number_within_year = next_number_within_year + 1 WHERE _year = NEW._year;
  5. 因为表锁被事务保持为其提交,所以这可能应该是最后触发的触发器(read more about trigger execution order here

    更新

    而不是使用link 提供的LOCK命令检查Craig Ringer来锁定整个表

    这种情况的缺点是INSERT操作性能下降 - 当时只有一个事务可以执行插入。