Gevaar: Uw vraag houdt in dat u een ontwerpfout maakt - u probeert een databasereeks te gebruiken voor een "zakelijke" waarde die aan gebruikers wordt gepresenteerd, in dit geval factuurnummers.
Gebruik geen reeks als u meer wilt dan de waarde op gelijkheid testen. Het heeft geen volgorde. Het heeft geen "afstand" tot een andere waarde. Het is gewoon gelijk, of niet gelijk.
Terugdraaien: Sequenties zijn over het algemeen niet geschikt voor dergelijk gebruik, omdat wijzigingen aan sequenties niet worden teruggedraaid met transactie ROLLBACK
. Zie de voetteksten op function-sequence en CREATE SEQUENCE
.
Rollbacks worden verwacht en zijn normaal. Ze treden op vanwege:
- deadlocks veroorzaakt door een conflicterende updatevolgorde of andere vergrendelingen tussen twee transacties;
- optimistische vergrendelingsterugdraaiingen in Hibernate;
- tijdelijke clientfouten;
- serveronderhoud door de DBA;
- serialisatieconflicten in
SERIALIZABLE
of momentopname isolatietransacties
... en meer.
Uw applicatie heeft "gaten" in de factuurnummering waar deze terugdraaiingen plaatsvinden. Bovendien is er geen bestelgarantie, dus het is heel goed mogelijk dat een transactie met een later volgnummer eerder wordt vastgelegd (soms veel eerder) dan een met een later nummer.
Chunking:
Het is ook normaal voor sommige toepassingen, waaronder Hibernate, om meer dan één waarde tegelijk uit een reeks te halen en deze intern uit te delen aan transacties. Dat is toegestaan, omdat je niet mag verwachten dat de door een reeks gegenereerde waarden een betekenisvolle volgorde hebben of op enigerlei wijze vergelijkbaar zijn, behalve voor gelijkheid. Voor factuurnummering wil je ook bestellen, dus je bent helemaal niet blij als Hibernate de waarden 5900-5999 pakt en ze vanaf 5999 begint uit te delen down of afwisselend omhoog en omlaag, zodat uw factuurnummers gaan:n, n+1, n+49, n+2, n+48, ... n+50, n+99, n+51, n+98, [n+52 verloren bij terugdraaien], n+97, ... . Ja, de hoog-dan-laag-allocator bestaat in Hibernate.
Dat helpt niet, tenzij je een individuele @SequenceGenerator
definieert s in uw toewijzingen, deelt Hibernate graag een enkele reeks voor elke gegenereerde ID ook. Lelijk.
Correct gebruik:
Een reeks is alleen geschikt als u alleen vereisen dat de nummering uniek is. Als je het ook monotoon en ordinaal wilt hebben, kun je overwegen om een gewone tabel met een tellerveld te gebruiken via UPDATE ... RETURNING
of SELECT ... FOR UPDATE
("pessimistische vergrendeling" in sluimerstand) of via sluimerstand optimistische vergrendeling. Op die manier kunt u ononderbroken incrementen garanderen zonder gaten of inzendingen die niet in orde zijn.
Wat u in plaats daarvan kunt doen:
Maak een tafel alleen voor een toonbank. Zorg dat er een enkele rij in staat en werk deze bij terwijl u hem leest. Dat vergrendelt het en voorkomt dat andere transacties een ID krijgen totdat de jouwe zich commit.
Omdat het al uw transacties dwingt om serieel te werken, moet u transacties die factuur-ID's genereren kort houden en voorkomen dat u er meer werk aan doet dan nodig is.
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;
Als alternatief kunt u:
- Definieer een entiteit voor factuurnummer, voeg een
@Version
toe kolom, en laat optimistische vergrendeling conflicten oplossen; - Definieer een entiteit voor factuurnummer en gebruik expliciete pessimistische vergrendeling in Hibernate om een selectie uit te voeren ... voor update en vervolgens voor update.
Al deze opties zullen uw transacties serialiseren - ofwel door conflicten terug te draaien met @Version, of ze te blokkeren (vergrendelen) totdat de slothouder zich commit. Hoe dan ook, reeksen zonder onderbrekingen zullen echt vertraag dat deel van uw toepassing, dus gebruik alleen reeksen zonder onderbrekingen als dat nodig is.
@GenerationType.TABLE
:Het is verleidelijk om @GenerationType.TABLE
te gebruiken met een @TableGenerator(initialValue=1, ...)
. Helaas, hoewel GenerationType.TABLE u een toewijzingsgrootte laat specificeren via @TableGenerator, biedt het helaas geen garanties over bestel- of terugdraaigedrag. Zie de JPA 2.0-specificatie, sectie 11.1.46 en 11.1.17. In het bijzonder "Deze specificatie definieert niet het exacte gedrag van deze strategieën. en voetnoot 102 "Draagbare toepassingen mogen de GeneratedValue-annotatie niet gebruiken op andere persistente velden of eigenschappen [dan @Id
primaire sleutels]" . Het is dus onveilig om @GenerationType.TABLE
te gebruiken voor nummering die u nodig hebt zonder tussenruimte of nummering die niet op een primaire sleuteleigenschap staat, tenzij uw JPA-provider meer garanties geeft dan de standaard.
Als je vastzit aan een reeks :
De poster merkt op dat ze bestaande apps hebben die de DB gebruiken en die al een reeks gebruiken, dus ze zitten eraan vast.
De JPA-standaard garandeert niet dat u gegenereerde kolommen kunt gebruiken, behalve op @Id, u kunt (a) dat negeren en doorgaan zolang uw provider u dit toestaat, of (b) het invoegen met een standaardwaarde en opnieuw - uit de database lezen. Dat laatste is veiliger:
@Column(name = "inv_seq", insertable=false, updatable=false)
public Integer getInvoiceSeq() {
return invoiceSeq;
}
Vanwege insertable=false
de provider zal niet proberen een waarde voor de kolom op te geven. U kunt nu een geschikte DEFAULT
. instellen in de database, zoals nextval('some_sequence')
en het zal gehonoreerd worden. Mogelijk moet u de entiteit opnieuw uit de database lezen met EntityManager.refresh()
nadat je het hebt volgehouden - ik weet niet zeker of de persistentie-provider dat voor je zal doen en ik heb de specificatie niet gecontroleerd of een demoprogramma geschreven.
Het enige nadeel is dat het lijkt alsof de kolom niet @ NotNull of nullable=false
kan worden gemaakt , omdat de provider niet begrijpt dat de database een standaardwaarde voor de kolom heeft. Het kan nog steeds NOT NULL
zijn in de database.
Als je geluk hebt, gebruiken je andere apps ook de standaardbenadering van het weglaten van de reekskolom uit de INSERT
's kolomlijst of expliciet specificeren van het trefwoord DEFAULT
als de waarde, in plaats van nextval
. aan te roepen . Het zal niet moeilijk zijn om daar achter te komen door log_statement = 'all'
. in te schakelen in postgresql.conf
en het doorzoeken van de logs. Als dat het geval is, kunt u alles naar gapless overschakelen als u besluit dat dit nodig is, door uw DEFAULT
te vervangen met een BEFORE INSERT ... FOR EACH ROW
triggerfunctie die NEW.invoice_number
instelt van de toonbank.