7

I have a PostgreSQL table in which I have a column inv_seq declared as serial.

I have a Hibernate bean class to map the table. All the other columns are read properly except this column. Here is the declaration in the Hibernate bean class:

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

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

Is the declaration correct?
I am able to see the sequential numbers generated by the column in the database, but I am not able to access them in the java class.

Please help.

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
Aditya R
  • 383
  • 2
  • 7
  • 21

5 Answers5

19

Danger: Your question implies that you may be making a design mistake - you are trying to use a database sequence for a "business" value that is presented to users, in this case invoice numbers.

Don't use a sequence if you need to anything more than test the value for equality. It has no order. It has no "distance" from another value. It's just equal, or not equal.

Rollback: Sequences are not generally appropriate for such uses because changes to sequences are't rolled back with transaction ROLLBACK. See the footers on functions-sequence and CREATE SEQUENCE.

Rollbacks are expected and normal. They occur due to:

  • deadlocks caused by conflicting update order or other locks between two transactions;
  • optimistic locking rollbacks in Hibernate;
  • transient client errors;
  • server maintenance by the DBA;
  • serialization conflicts in SERIALIZABLE or snapshot isolation transactions

... and more.

Your application will have "holes" in the invoice numbering where those rollbacks occur. Additionally, there is no ordering guarantee, so it's entirely possible that a transaction with a later sequence number will commit earlier (sometimes much earlier) than one with a later number.

Chunking:

It's also normal for some applications, including Hibernate, to grab more than one value from a sequence at a time and hand them out to transactions internally. That's permissible because you are not supposed to expect sequence-generated values to have any meaningful order or be comparable in any way except for equality. For invoice numbering, you want ordering too, so you won't be at all happy if Hibernate grabs values 5900-5999 and starts handing them out from 5999 counting down or alternately up-then-down, so your invoice numbers go: n, n+1, n+49, n+2, n+48, ... n+50, n+99, n+51, n+98, [n+52 lost to rollback], n+97, .... Yes, the high-then-low allocator exists in Hibernate.

It doesn't help that unless you define individual @SequenceGenerators in your mappings, Hibernate likes to share a single sequence for every generated ID, too. Ugly.

Correct use:

A sequence is only appropriate if you only require the numbering to be unique. If you also need it to be monotonic and ordinal, you should think about using an ordinary table with a counter field via UPDATE ... RETURNING or SELECT ... FOR UPDATE ("pessimistic locking" in Hibernate) or via Hibernate optimistic locking. That way you can guarantee gapless increments without holes or out-of-order entries.

What to do instead:

Create a table just for a counter. Have a single row in it, and update it as you read it. That'll lock it, preventing other transactions from getting an ID until yours commits.

Because it forces all your transactions to operate serially, try to keep transactions that generate invoice IDs short and avoid doing more work in them than you need to.

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;

Alternately, you can:

  • Define an entity for invoice_number, add a @Version column, and let optimistic locking take care of conflicts;
  • Define an entity for invoice_number and use explicit pessimistic locking in Hibernate to do a select ... for update then an update.

All these options will serialize your transactions - either by rolling back conflicts using @Version, or blocking them (locking) until the lock holder commits. Either way, gapless sequences will really slow that area of your application down, so only use gapless sequences when you have to.

@GenerationType.TABLE: It's tempting to use @GenerationType.TABLE with a @TableGenerator(initialValue=1, ...). Unfortunately, while GenerationType.TABLE lets you specify an allocation size via @TableGenerator, it doesn't provide any guarantees about ordering or rollback behaviour. See the JPA 2.0 spec, section 11.1.46, and 11.1.17. In particular "This specification does not define the exact behavior of these strategies. and footnote 102 "Portable applications should not use the GeneratedValue annotation on other persistent fields or properties [than @Id primary keys]". So it is unsafe to use @GenerationType.TABLE for numbering that you require to be gapless or numbering that isn't on a primary key property unless your JPA provider makes more guarantees than the standard.

If you're stuck with a sequence:

The poster notes that they have existing apps using the DB that use a sequence already, so they're stuck with it.

The JPA standard doesn't guarantee that you can use generated columns except on @Id, you can (a) ignore that and go ahead so long as your provider does let you, or (b) do the insert with a default value and re-read from the database. The latter is safer:

    @Column(name = "inv_seq", insertable=false, updatable=false)
    public Integer getInvoiceSeq() {
        return invoiceSeq;
    }

Because of insertable=false the provider won't try to specify a value for the column. You can now set a suitable DEFAULT in the database, like nextval('some_sequence') and it'll be honoured. You might have to re-read the entity from the database with EntityManager.refresh() after persisting it - I'm not sure if the persistence provider will do that for you and I haven't checked the spec or written a demo program.

The only downside is that it seems the column can't be made @ NotNull or nullable=false, as the provider doesn't understand that the database has a default for the column. It can still be NOT NULL in the database.

If you're lucky your other apps will also use the standard approach of either omitting the sequence column from the INSERT's column list or explicitly specifying the keyword DEFAULT as the value, instead of calling nextval. It won't be hard to find that out by enabling log_statement = 'all' in postgresql.conf and searching the logs. If they do, then you can actually switch everything to gapless if you decide you need to by replacing your DEFAULT with a BEFORE INSERT ... FOR EACH ROW trigger function that sets NEW.invoice_number from the counter table.

Craig Ringer
  • 307,061
  • 76
  • 688
  • 778
  • this is a great alternative and very well thought answer, but it is not a way to use serial type in database. Assume you have been working some years with the database and that serial field exists and many other applications update that table and use Postgres serial type to create the id. How could I develop a new application using hibernate that insert in that table and take advantage of the serial field? – Christian Vielma Aug 01 '12 at 18:09
  • @ChristianVielma In that case you have to use alloacationSize=1 in your `@@SequenceGenerator` and deal with the out-of-order entries and the gaps. If other apps have been using that method in the past then your requirements clearly permit - implicitly or explicitly - gaps in the generated ID. – Craig Ringer Aug 01 '12 at 23:35
  • .... I think Hibernate supports non-ID generated columns, but you should test that. If it doesn't, use a native query to call `nextval` on the sequence directly then assign the result. Or you can set the column as `@ Basic(insertable=false)` and refresh the entity immediately after persist, so the database's `DEFAULT` value is assigned and fetched. – Craig Ringer Aug 01 '12 at 23:43
  • 1
    +1 High quality answer. You suffer from knowing to much. :) Trying to be unambiguous and cover all possibilities .. can make an answer very long. – Erwin Brandstetter Aug 02 '12 at 01:23
  • @ErwinBrandstetter I'm not normally *this* bad, but I want to be able to link to this one from elsewhere (including the Pg lists) when this comes up - as it does over, and over, and over again. Plus I'm a pedant ;-) – Craig Ringer Aug 02 '12 at 02:03
  • @CraigRinger: Takes one to know one. ;) – Erwin Brandstetter Aug 02 '12 at 02:25
  • @CraigRinger man that seems like the answer :) as Erwin said, +1 for your answer. I'll give it a try and give you the bounty if it works. Thanks – Christian Vielma Aug 02 '12 at 15:15
  • @CraigRinger you nailed it! I'm glad to give you the bounty. – Christian Vielma Aug 02 '12 at 17:00
  • @ErwinBrandstetter Christian didn't post the original question, so can't AFAIK. – Craig Ringer Aug 06 '12 at 03:30
2

I have found that hibernate 3.6 tries to use a single sequence for all entities when you set it to AUTO so in my application I use IDENTITY as the generation strategy.

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

@Craig had some very good points about invoice numbers needing to incrementing if you are presenting them to users and he suggested using a table for that. If you do end up using a table to store the next id you might be able to use a mapping similar to this one.

@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; 
ams
  • 60,316
  • 68
  • 200
  • 288
  • 1
    Unfortunately, while `GenerationType.TABLE` lets you specify an allocation size via `@TableGenerator`, it doesn't provide any guarantees about ordering or rollback behaviour. See the JPA 2.0 spec, section 11.1.46, and 11.1.17. In particular "This specification does not define the exact behavior of these strategies." and footnote 102 "Portable applications should not use the GeneratedValue annotation on other persistent fields or properties [than @Id primary keys]". It is unsafe to use for numbering that you require to be gapless unless your JPA provider makes more guarantees than the standard. – Craig Ringer Aug 01 '12 at 12:38
  • Thanks Craig for the clarification on the behavior of @TableGenerator in your post. – ams Aug 01 '12 at 23:59
1

Correct syntax is as follows:

@Column(name="idClass", unique=true, nullable=false, columnDefinition = "serial")
@Generated(GenerationTime.INSERT)
private Integer idClass;
Mohit Kumar
  • 2,898
  • 3
  • 21
  • 34
Arjun S
  • 11
  • 2
0

I use Postgres + Hibernate in my projects and this is what I do:

@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;
}

It works just fine for me.

Andre
  • 3,874
  • 3
  • 35
  • 50
  • and whithout being a primary key? Does that work without @Id? – Christian Vielma Jul 31 '12 at 19:06
  • Not gonna lie... I've never needed that so I've never tried. But there's only one way to find out... ;) Give it a shot and let us know – Andre Jul 31 '12 at 19:08
  • Well I had to write this to make it work for Postgres (Serial type column): @Basic(optional = false) @NotNull @Generated(GenerationTime.INSERT) @Column(name = "\"C_MODEL\"", unique=true) private int cModel; – Christian Vielma Jul 31 '12 at 20:43
  • @ChristianVielma The standard says "Portable applications should not use the GeneratedValue annotation on other persistent fields or properties [than \@Id primary keys]". See section 11.1.17. So it might work, but isn't guaranteed, and it isn't safe anyway - see my answer. – Craig Ringer Aug 01 '12 at 12:40
0

Depending on your situation, this may not work. There is a bug opened against Hibernate that documents this behavior.

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

If you are open to using a mapping file instead of annotations, I have been able to recreate the issue (NULL's in SERIAL columns that are not part of the primary key). Using the "generated" attribute of the property element causes Hibernate to re-read the row after an insert to pick up the generated column value:

<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>

KeithL
  • 1,150
  • 10
  • 12
  • But I already have the @Id tag for the primaryKey field in the bean class. – Aditya R Feb 02 '10 at 13:33
  • @Column(name = "oid" , nullable = false ) @Id @GeneratedValue(strategy=javax.persistence.GenerationType.AUTO) public Long getOid() { return oid; } – Aditya R Feb 02 '10 at 13:34
  • To clarify - there is more than one generated column in the table, and column inv_seq is NOT part of the primary key? – KeithL Feb 02 '10 at 14:43
  • Try using the GenerationType.IDENTITY strategy. Also, what value does the Java object return when you call getInvoiceSeq()? – KeithL Feb 02 '10 at 15:48
  • getInvoiceSeq() returns a null value. I will try the above method which you suggested and get back to you. Thanks. – Aditya R Feb 03 '10 at 04:57
  • Sorry it is returning null even when I use this: @Column(name = "inv_seq") @GeneratedValue(strategy=GenerationType.IDENTITY) public Integer getInvoiceSeq() { return invoiceSeq; } – Aditya R Feb 03 '10 at 05:35
  • Are you using the correct dialect in your Hibernate configuration file? Since SERIAL is specific to PostgreSQL it's possible that Hibernate is not recognizing that column type and ignoring it. Syntax is: org.hibernate.dialect.PostgreSQLDialect – KeithL Feb 03 '10 at 13:08
  • I was able to recreate your issue, and have updated my answer. – KeithL Feb 03 '10 at 15:37
  • Thanks a lot Keith. I don't think I can use the mapping file instead of the annotation, since we are using annotation throughout the application. I will consult my manager and get back. – Aditya R Feb 04 '10 at 10:00
  • @AdityaR `SERIAL` is just a macro for `integer` with a `default nextval('tblname_colname_seq')` in `CREATE TABLE`, not a real type. Use `integer`. – Craig Ringer Aug 01 '12 at 04:56