Hibernate对于PostgreSQL序列类型的注解

7

我有一个PostgreSQL表格,其中我有一个声明为serialinv_seq列。

我有一个Hibernate bean类来映射这个表。除了这一列,所有其他列都可以正确读取。以下是Hibernate bean类中的声明:

....
  ....
        @GeneratedValue(strategy=javax.persistence.GenerationType.AUTO)
        @Column(name = "inv_seq")
        public Integer getInvoiceSeq() {
            return invoiceSeq;
        }

         public void setInvoiceSeq(Integer invoiceSeq) {
        this.invoiceSeq = invoiceSeq;
    }
  ....
....

声明是否正确?
我能够看到数据库中列生成的顺序号,但是我无法在Java类中访问它们。

请帮忙。

5个回答

19

警告:你的问题暗示你可能正在犯设计错误,你试图使用数据库序列作为呈现给用户的“业务”值,在这种情况下是发票号码。

如果您需要进行比相等性测试更多的操作,请勿使用序列。它没有顺序。它与另一个值没有“距离”。 它只是相等或不相等。

回滚: 序列通常不适用于此类用途,因为对序列的更改不能随事务ROLLBACK回滚。请参见functions-sequenceCREATE 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 ... RETURNINGSELECT ... FOR UPDATE (在 Hibernate 中称为“悲观锁定”)或通过 Hibernate 乐观锁定。这样,您可以保证增量无间隙,没有缺失或乱序的条目。

相反的做法:

创建一个只用于计数的表格。它只有一行,并在读取时进行更新。这将锁定该行,防止其他事务在您提交之前获取ID。

由于它强制使所有操作序列化,因此请尝试使生成发票ID的事务短并避免在其中执行不必要的工作。

CREATE TABLE invoice_number (
    last_invoice_number integer primary key
);

-- PostgreSQL specific hack you can use to make
-- really sure only one row ever exists
CREATE UNIQUE INDEX there_can_be_only_one 
ON invoice_number( (1) );

-- Start the sequence so the first returned value is 1
INSERT INTO invoice_number(last_invoice_number) VALUES (0);

-- To get a number; PostgreSQL specific but cleaner.
-- Use as a native query from Hibernate.
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


这是一个很好的替代方案,非常周密,但它不是在数据库中使用序列类型的方法。假设您已经与数据库一起工作了几年,并且该序列字段存在,许多其他应用程序更新该表并使用Postgres序列类型创建ID。我如何开发一个新的应用程序,使用Hibernate插入该表并利用序列字段呢? - Christian Vielma
在这种情况下,您必须在@@SequenceGenerator中使用alloacationSize = 1,并处理无序条目和间隙。如果其他应用程序过去一直在使用该方法,则您的要求明显允许 - 隐含或明确地 - 在生成的ID中存在间隙。 - Craig Ringer
我认为Hibernate支持非ID生成列,但你应该测试一下。如果不支持,可以使用本地查询直接调用序列上的nextval,然后分配结果。或者你可以将该列设置为@Basic(insertable = false)并在持久化后立即刷新实体,以便分配和获取数据库中的DEFAULT值。 - Craig Ringer
1
+1 高质量回答。你知道得太多反而会受苦。 :) 试图不含糊并涵盖所有可能性.. 可能会使答案变得很长。 - Erwin Brandstetter
@CraigRinger,你真棒!很高兴给你打赏。 - Christian Vielma
显示剩余4条评论

2

我发现当你把Hibernate 3.6设置为自动模式时,它会尝试为所有实体使用单个序列,因此在我的应用程序中,我使用IDENTITY作为生成策略。

@Id
@Column(name="Id")
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id; 

@Craig在发表关于发票号码需要递增的观点时提到,如果您要向用户展示它们,建议使用表格。如果您最终决定使用表格来存储下一个ID,您可能可以使用类似于此映射的映射。

@Column(name="Id")
@GeneratedValue(strategy=GenerationType.TABLE,generator="user_table_generator")
@TableGenerator(
    name="user_table_generator", 
    table="keys",
    schema="primarykeys",
    pkColumnName="key_name",
    pkColumnValue="xxx",
    valueColumnName="key_value",
    initialValue=1,
    allocationSize=1)
private Integer id; 

1
很不幸,虽然GenerationType.TABLE允许您通过@TableGenerator指定分配大小,但它并不提供任何关于排序或回滚行为的保证。请参阅JPA 2.0规范,第11.1.46节和11.1.17节。特别是“此规范未定义这些策略的确切行为。”和脚注102“便携式应用程序不应在其他持久字段或属性[而不是@Id主键]上使用GeneratedValue注释”。除非您的JPA提供程序提供比标准更多的保证,否则对于您需要无间隙编号的情况,使用它是不安全的。 - Craig Ringer
感谢Craig在他的帖子中对@TableGenerator行为的澄清。 - ams

1

正确的语法如下:

@Column(name="idClass", unique=true, nullable=false, columnDefinition = "serial")
@Generated(GenerationTime.INSERT)
private Integer idClass;

0
根据您的情况,这可能不起作用。已经有一个针对Hibernate的错误报告记录了这种行为。

http://opensource.atlassian.com/projects/hibernate/browse/HHH-4159

如果您愿意使用映射文件而不是注释,我已经能够重新创建出现的问题(SERIAL列中的NULL不是主键的一部分)。使用属性元素的“generated”属性会导致Hibernate在插入后重新读取行以获取生成的列值:

<class name="Ticket" table="ticket_t">
    <id name="id" column="ticket_id">
        <generator class="identity"/>
    </id>
    <property name="customerName" column="customer_name"/>
    <property name="sequence" column="sequence" generated="always"/>
</class>


但是我已经在bean类中的primaryKey字段上有@Id标签了。 - Aditya R
@Column(name = "oid", nullable = false)@Id @GeneratedValue(strategy=javax.persistence.GenerationType.AUTO) public Long getOid() { return oid; } - Aditya R
请澄清 - 表中有多个生成的列,并且inv_seq列不是主键的一部分? - KeithL
尝试使用GenerationType.IDENTITY策略。另外,当您调用getInvoiceSeq()方法时,Java对象返回什么值? - KeithL
@AdityaR SERIAL只是在CREATE TABLE中使用integer的宏,带有default nextval('tblname_colname_seq'),而不是真正的类型。请使用integer - Craig Ringer
显示剩余5条评论

0

我在我的项目中使用Postgres + Hibernate,这是我的做法:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY, generator = "hibernate_sequence")
@SequenceGenerator(name = "hibernate_sequence", sequenceName = "hibernate_sequence")
@Column(name = "id", unique = true, nullable = false)
protected Long id;

public Long getId() {
    return id;
}
public void setId(Long id) {
    this.id = id;
}

对我来说它完全正常运行。


没有主键,这个能工作吗?没有@Id行不行? - Christian Vielma
不撒谎...我从来没有需要过那个,所以我也从来没有尝试过。但是只有一种方法可以找出答案...;) 试一试并让我们知道结果。 - Andre
我不得不写这个来让它适用于Postgres(Serial类型列):@Basic(optional = false)     @NotNull     @Generated(GenerationTime.INSERT)        @Column(name = ""C_MODEL"", unique=true)     private int cModel; - Christian Vielma
@ChristianVielma 标准规定:“可移植应用程序不应在除主键之外的其他持久字段或属性上使用 GeneratedValue 注释”。请参见第 11.1.17 节。因此它可能有效,但不能保证,而且无论如何都不安全-请看我的答案。 - Craig Ringer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接