Fara: Din fråga antyder att du kan göra ett designfel - du försöker använda en databassekvens för ett "affärsvärde" som presenteras för användarna, i det här fallet fakturanummer.
Använd inte en sekvens om du behöver något mer än att testa värdet för jämlikhet. Den har ingen ordning. Den har inget "avstånd" från ett annat värde. Det är bara lika, eller inte lika.
Återställ: Sekvenser är i allmänhet inte lämpliga för sådan användning eftersom ändringar av sekvenser inte återställs med transaktionen ROLLBACK
. Se sidfotarna på functions-sequence och CREATE SEQUENCE
.
Återställningar förväntas och normalt. De uppstår på grund av:
- deadlocks orsakade av motstridiga uppdateringsorder eller andra låsningar mellan två transaktioner;
- optimistiska återställningar av låsning i Hibernate;
- övergående klientfel;
- serverunderhåll av DBA;
- serialiseringskonflikter i
SERIALIZABLE
eller isoleringstransaktioner för ögonblicksbilder
... och mer.
Din ansökan kommer att ha "hål" i fakturanumreringen där dessa återkallningar sker. Dessutom finns det ingen beställningsgaranti, så det är fullt möjligt att en transaktion med ett senare sekvensnummer genomförs tidigare (ibland mycket tidigare) än en med ett senare nummer.
Chunking:
Det är också normalt att vissa applikationer, inklusive Hibernate, tar mer än ett värde från en sekvens åt gången och delar ut dem till transaktioner internt. Det är tillåtet eftersom du inte ska förvänta dig att sekvensgenererade värden ska ha någon meningsfull ordning eller vara jämförbara på något sätt förutom för jämlikhet. För fakturanumrering vill du också beställa, så du blir inte alls glad om Hibernate tar värden 5900-5999 och börjar dela ut dem från 5999 räknar ned eller alternativt upp-och-ner, så dina fakturanummer går:n, n+1, n+49, n+2, n+48, ... n+50, n+99, n+51, n+98, [n+52 förlorade till återställning], n+97, ... . Ja, hög-sedan-låg-allokatorn finns i Hibernate.
Det hjälper inte om du inte definierar individuell @SequenceGenerator
I dina mappningar gillar Hibernate att dela en enda sekvens för varje genererat ID också. Ful.
Korrekt användning:
En sekvens är bara lämplig om du bara kräver att numreringen är unik. Om du också behöver att den ska vara monoton och ordningsföljd bör du tänka på att använda en vanlig tabell med ett räknarfält via UPDATE ... RETURNING
eller SELECT ... FOR UPDATE
("pessimistisk låsning" i Hibernate) eller via Hibernate optimistisk låsning. På så sätt kan du garantera mellanrumsfria inkrement utan hål eller felaktiga poster.
Vad du ska göra istället:
Skapa ett bord bara för en disk. Ha en enda rad i den och uppdatera den när du läser den. Det låser det, vilket förhindrar att andra transaktioner får ett ID tills ditt begår.
Eftersom det tvingar alla dina transaktioner att fungera i serie, försök att hålla transaktioner som genererar faktura-ID korta och undvika att göra mer arbete i dem än du behöver.
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;
Alternativt kan du:
- Definiera en enhet för invoice_number, lägg till en
@Version
kolumn, och låt optimistisk låsning ta hand om konflikter; - Definiera en enhet för invoice_number och använd explicit pessimistisk låsning i Hibernate för att välja ... för uppdatering och sedan en uppdatering.
Alla dessa alternativ kommer att serialisera dina transaktioner - antingen genom att rulla tillbaka konflikter med @Version, eller blockera dem (låsa) tills låshållaren förbinder sig. Oavsett vilket kommer sekvenser utan luckor verkligen sakta ner den delen av din applikation, så använd bara sekvenser utan mellanrum när du måste.
@GenerationType.TABLE
:Det är frestande att använda @GenerationType.TABLE
med en @TableGenerator(initialValue=1, ...)
. Tyvärr, medan GenerationType.TABLE låter dig ange en tilldelningsstorlek via @TableGenerator, ger den inga garantier om beställnings- eller återställningsbeteende. Se JPA 2.0-specifikationen, avsnitt 11.1.46 och 11.1.17. I synnerhet "Denna specifikation definierar inte det exakta beteendet för dessa strategier. och fotnot 102 "Bärbara applikationer bör inte använda GeneratedValue-anteckningen på andra beständiga fält eller egenskaper [än @Id
primärnycklar]" . Så det är osäkert att använda @GenerationType.TABLE
för numrering som du behöver vara mellanrumsfri eller numrering som inte finns på en primärnyckelegenskap om inte din JPA-leverantör ger fler garantier än standarden.
Om du har fastnat med en sekvens :
Affischen noterar att de har befintliga appar som använder DB som redan använder en sekvens, så de har fastnat med den.
JPA-standarden garanterar inte att du kan använda genererade kolumner förutom på @Id, du kan (a) ignorera det och gå vidare så länge din leverantör tillåter dig, eller (b) infoga med ett standardvärde och åter -läs från databasen. Det senare är säkrare:
@Column(name = "inv_seq", insertable=false, updatable=false)
public Integer getInvoiceSeq() {
return invoiceSeq;
}
På grund av insertable=false
leverantören kommer inte att försöka ange ett värde för kolumnen. Du kan nu ställa in en lämplig DEFAULT
i databasen, som nextval('some_sequence')
och det kommer att hedras. Du kanske måste läsa entiteten igen från databasen med EntityManager.refresh()
efter att ha behållit det - jag är inte säker på om persistensleverantören kommer att göra det åt dig och jag har inte kontrollerat specifikationerna eller skrivit något demoprogram.
Den enda nackdelen är att det verkar som om kolumnen inte kan göras @ NotNull eller nullable=false
, eftersom leverantören inte förstår att databasen har en standard för kolumnen. Det kan fortfarande vara NOT NULL
i databasen.
Om du har tur kommer dina andra appar också att använda standardmetoden att antingen utelämna sekvenskolumnen från INSERT
s kolumnlista eller uttryckligen ange nyckelordet DEFAULT
som värdet, istället för att anropa nextval
. Det kommer inte att vara svårt att ta reda på det genom att aktivera log_statement = 'all'
i postgresql.conf
och söka i loggarna. Om de gör det kan du faktiskt ändra allt till gapless om du bestämmer dig för att du behöver det genom att ersätta din DEFAULT
med en BEFORE INSERT ... FOR EACH ROW
triggerfunktion som ställer in NEW.invoice_number
från bänkbordet.