Nebezpečí: Z vaší otázky vyplývá, že možná děláte chybu v návrhu – pokoušíte se použít posloupnost databáze pro „obchodní“ hodnotu, která je prezentována uživatelům, v tomto případě čísla faktur.
Nepoužívejte sekvenci, pokud potřebujete něco víc než otestovat hodnotu pro rovnost. Nemá to žádný řád. Nemá žádnou "vzdálenost" od jiné hodnoty. Je to prostě stejné, nebo nestejné.
Vrácení zpět: Sekvence nejsou obecně vhodné pro taková použití, protože změny sekvencí nejsou vráceny zpět pomocí transakce ROLLBACK
. Viz zápatí u Functions-sequence a CREATE SEQUENCE
.
Návraty jsou očekávané a normální. Vyskytují se v důsledku:
- zablokování způsobené konfliktním pořadím aktualizací nebo jinými zámky mezi dvěma transakcemi;
- optimistický návrat zamykání v režimu spánku;
- přechodné chyby klienta;
- údržbu serverů ze strany DBA;
- konflikty serializace v
SERIALIZABLE
nebo snímek izolace transakcí
... a další.
Vaše aplikace bude mít „díry“ v číslování faktur, kde k těmto vrácením dochází. Navíc neexistuje žádná záruka na objednávku, takže je zcela možné, že transakce s pozdějším pořadovým číslem proběhne dříve (někdy hodně dříve) než číslo s pozdějším číslem.
Chunking:
Je také normální, že některé aplikace, včetně Hibernate, uchopí více než jednu hodnotu ze sekvence najednou a předají je interně transakcím. To je přípustné, protože se neočekává, že sekvenčně generované hodnoty budou mít nějaké smysluplné pořadí nebo budou srovnatelné jakýmkoliv způsobem kromě rovnosti. U číslování faktur chcete také objednávat, takže nebudete vůbec rádi, když Hibernate získá hodnoty 5900-5999 a začne je rozdávat od 5999 odpočítávání dolů nebo střídavě nahoru-pak-dolů, takže čísla vašich faktur budou:n, n+1, n+49, n+2, n+48, ... n+50, n+99, n+51, n+98, [n+52 ztraceno pro vrácení zpět], n+97, ... . Ano, alokátor high-then-low existuje v Hibernate.
To nepomůže, pokud nedefinujete individuální @SequenceGenerator
Ve vašich mapováních Hibernate ráda sdílí jednu sekvenci pro každý generované ID také. Ošklivý.
Správné použití:
Sekvence je vhodná pouze v případě, že pouze vyžadují, aby číslování bylo jedinečné. Pokud také potřebujete, aby byla monotónní a ordinální, měli byste přemýšlet o použití obyčejné tabulky s polem čítače přes UPDATE ... RETURNING
nebo SELECT ... FOR UPDATE
("pesimistické zamykání" v Hibernate) nebo přes Hibernate optimistické zamykání. Tímto způsobem můžete zaručit bez mezer bez děr nebo položek mimo pořadí.
Co dělat místo toho:
Vytvořte stůl jen pro pult. Mějte v něm jeden řádek a při čtení jej aktualizujte. Tím jej uzamknete a zabráníte tomu, aby ostatní transakce získaly ID, dokud se vaše nepotvrdí.
Protože to nutí všechny vaše transakce fungovat sériově, snažte se, aby transakce, které generují ID faktur, byly krátké a nedělali s nimi více práce, než potřebujete.
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;
Alternativně můžete:
- Definujte entitu pro číslo faktury, přidejte
@Version
sloupec a nechejte optimistické zamykání, aby se postaralo o konflikty; - Definujte entitu pro číslo faktury a pomocí explicitního pesimistického zamykání v režimu spánku proveďte výběr ... pro aktualizaci a poté aktualizaci.
Všechny tyto možnosti budou serializovat vaše transakce – buď vrácením konfliktů pomocí @Version, nebo jejich zablokováním (uzamčením), dokud se držitel zámku nepotvrdí. V každém případě budou sekvence bez mezer skutečně zpomalte tuto oblast vaší aplikace, takže sekvence bez mezer používejte pouze tehdy, když musíte.
@GenerationType.TABLE
:Je lákavé použít @GenerationType.TABLE
s @TableGenerator(initialValue=1, ...)
. Bohužel, zatímco GenerationType.TABLE umožňuje určit velikost alokace pomocí @TableGenerator, neposkytuje žádné záruky ohledně chování při objednávání nebo vrácení. Viz specifikace JPA 2.0, sekce 11.1.46 a 11.1.17. Zejména "Tato specifikace nedefinuje přesné chování těchto strategií." a poznámka pod čarou 102 "Přenosné aplikace by neměly používat anotaci GeneratedValue na jiných trvalých polích nebo vlastnostech [než @Id
primární klíče]" . Není tedy bezpečné používat @GenerationType.TABLE
pro číslování, které požadujete bez mezer, nebo číslování, které není ve vlastnosti primárního klíče, pokud váš poskytovatel JPA neposkytuje více záruk, než je standard.
Pokud jste uvízli v sekvenci :
Plakát uvádí, že mají existující aplikace využívající DB, které již používají sekvenci, takže u ní zůstali.
Standard JPA nezaručuje, že můžete použít vygenerované sloupce s výjimkou @Id, můžete (a) to ignorovat a pokračovat, pokud vám to váš poskytovatel dovolí, nebo (b) provést vložení s výchozí hodnotou a znovu -číst z databáze. To druhé je bezpečnější:
@Column(name = "inv_seq", insertable=false, updatable=false)
public Integer getInvoiceSeq() {
return invoiceSeq;
}
Kvůli insertable=false
poskytovatel se nepokusí zadat hodnotu pro sloupec. Nyní můžete nastavit vhodný DEFAULT
v databázi, jako nextval('some_sequence')
a bude to poctěno. Možná budete muset znovu načíst entitu z databáze pomocí EntityManager.refresh()
po přetrvání – nejsem si jistý, zda to poskytovatel persistence udělá za vás a nezkontroloval jsem specifikace ani nenapsal demo program.
Jedinou nevýhodou je, že se zdá, že sloupec nelze vytvořit @ NotNull nebo nullable=false
, protože poskytovatel nechápe, že databáze má pro sloupec výchozí hodnotu. Stále může být NOT NULL
v databázi.
Pokud budete mít štěstí, vaše ostatní aplikace budou také používat standardní přístup, a to buď vynecháním sloupce sekvence z INSERT
seznam sloupců nebo explicitně specifikující klíčové slovo DEFAULT
jako hodnotu, namísto volání nextval
. Nebude těžké to zjistit povolením log_statement = 'all'
v postgresql.conf
a prohledávání protokolů. Pokud ano, můžete ve skutečnosti přepnout vše na bez mezer, pokud se rozhodnete, že to potřebujete, a to nahrazením DEFAULT
s BEFORE INSERT ... FOR EACH ROW
spouštěcí funkce, která nastaví NEW.invoice_number
z pultového stolu.