警告:你的问题暗示你可能正在犯设计错误,你试图使用数据库序列作为呈现给用户的“业务”值,在这种情况下是发票号码。
如果您需要进行比相等性测试更多的操作,请勿使用序列。它没有顺序。它与另一个值没有“距离”。 它只是相等或不相等。
回滚:
序列通常不适用于此类用途,因为对序列的更改不能随事务ROLLBACK
回滚。请参见functions-sequence和CREATE SEQUENCE
上的脚注。
回滚是预期且正常的。它们发生的原因包括:
- 两个事务之间的更新顺序或其他锁定引起的死锁;
- Hibernate中的乐观锁定回滚;
- 瞬态客户端错误;
- 由DBA执行的服务器维护;
SERIALIZABLE
或快照隔离事务中的序列化冲突
...... 等等。
在发生回滚的地方,您的应用程序将具有发票编号中的“空隙”。此外,没有排序保证,因此具有较大序列号的事务可能比具有较小序列号的事务更早提交(有时甚至更早)。
分块:
对于包括Hibernate在内的一些应用程序,每次从序列中获取多个值并将其内部交给事务是正常的。这是允许的,因为您不应该指望由序列生成的值具有任何有意义的顺序或可比性,除了相等性之外。对于发票编号,您也希望进行排序,因此如果Hibernate抓取5900-5999中的值并从5999开始向下或交替上升然后向下交付它们,则您将不会非常高兴,这样您的发票编号将变成:n、n + 1、n + 49、n + 2、n + 48、... n + 50、n + 99、n + 51、n + 98、[n + 52由于回滚而丢失],n + 97,... 是的,Hibernate中存在高然后低的分配器。
除非您在映射中定义了单个的@SequenceGenerator
,否则Hibernate也喜欢共享每个生成的ID的单个序列。丑陋。
正确使用:
如果您仅需要编号唯一,则序列是合适的。如果您还需要它是单调和序数,那么应考虑使用普通表格和计数器字段,通过 UPDATE ... RETURNING
或 SELECT ... FOR UPDATE
(在 Hibernate 中称为“悲观锁定”)或通过 Hibernate 乐观锁定。这样,您可以保证增量无间隙,没有缺失或乱序的条目。
相反的做法:
创建一个只用于计数的表格。它只有一行,并在读取时进行更新。这将锁定该行,防止其他事务在您提交之前获取ID。
由于它强制使所有操作序列化,因此请尝试使生成发票ID的事务短并避免在其中执行不必要的工作。
CREATE TABLE invoice_number (
last_invoice_number integer primary key
);
CREATE UNIQUE INDEX there_can_be_only_one
ON invoice_number( (1) );
INSERT INTO invoice_number(last_invoice_number) VALUES (0);
UPDATE invoice_number
SET last_invoice_number = last_invoice_number + 1
RETURNING last_invoice_number;
另外,您还可以:
- 为invoice_number定义一个实体,添加
@Version
列,并让乐观锁处理冲突;
- 为invoice_number定义一个实体,并在Hibernate中使用显式悲观锁定,执行select ... for update,然后进行更新。
所有这些选项都会序列化您的事务-通过使用@Version回滚冲突或通过阻止(锁定)它们直到锁定持有者提交。无论哪种方式,无缝序列将使您的应用程序变慢,因此只有在必须时才使用无缝序列。
@GenerationType.TABLE
:使用@GenerationType.TABLE
和@TableGenerator(initialValue=1, ...)
会让人心动。不幸的是,虽然GenerationType.TABLE允许您通过@TableGenerator指定分配大小,但它不提供任何关于排序或回滚行为的保证。请参见JPA 2.0规范第11.1.46节和11.1.17节。特别是“此规范不定义这些策略的确切行为。”和脚注102“可移植应用程序不应在除@Id
主键之外的其他持久字段或属性上使用GeneratedValue注释”。因此,除非您的JPA提供程序提供了比标准更多的保证,否则使用@GenerationType.TABLE
来进行需要无缝或不在主键属性上的编号是不安全的。
如果您被困在一个序列中:
海报指出他们有现有的使用DB的应用程序已经使用了序列,所以他们被卡住了。
JPA标准不保证您可以在@Id以外的生成列上使用它,您可以(a)忽略它并继续使用,只要您的提供程序允许您这样做,或者(b)使用默认值插入并从数据库重新读取。后者更安全:
@Column(name = "inv_seq", insertable=false, updatable=false)
public Integer getInvoiceSeq() {
return invoiceSeq;
}
因为
insertable=false
,提供程序不会为该列指定值。您现在可以在数据库中设置一个适当的
DEFAULT
,例如
nextval('some_sequence')
,并且它将被尊重。在持久化后,您可能需要使用
EntityManager.refresh()
从数据库重新读取实体 - 我不确定持久性提供程序是否会为您执行此操作,我也没有检查规范或编写演示程序。
唯一的缺点是似乎无法将该列设置为@NotNull或nullable=false
,因为提供程序无法理解数据库对该列具有默认值。它仍然可以在数据库中为NOT NULL
。如果你幸运的话,你的其他应用程序也将使用标准方法,即从INSERT
的列列表中省略序列列或显式指定关键字DEFAULT
作为值,而不是调用nextval
。通过在postgresql.conf
中启用log_statement = 'all'
并搜索日志,很容易找出。如果他们这样做,那么您实际上可以将所有内容切换到无间隙,如果您决定需要,可以通过使用BEFORE INSERT ... FOR EACH ROW
触发器函数将DEFAULT
替换为计数器表中的NEW.invoice_number
。
@@SequenceGenerator
中使用alloacationSize = 1,并处理无序条目和间隙。如果其他应用程序过去一直在使用该方法,则您的要求明显允许 - 隐含或明确地 - 在生成的ID中存在间隙。 - Craig Ringernextval
,然后分配结果。或者你可以将该列设置为@Basic(insertable = false)
并在持久化后立即刷新实体,以便分配和获取数据库中的DEFAULT
值。 - Craig Ringer