Niebezpieczeństwo: Twoje pytanie sugeruje, że możesz popełnić błąd projektowy — próbujesz użyć sekwencji bazy danych dla wartości „biznesowej”, która jest prezentowana użytkownikom, w tym przypadku numerów faktur.
Nie używaj sekwencji, jeśli potrzebujesz czegoś więcej niż przetestowanie wartości pod kątem równości. Nie ma porządku. Nie ma „odległości” od innej wartości. Jest po prostu równy lub nie równy.
Wycofanie: Sekwencje generalnie nie są odpowiednie do takich zastosowań, ponieważ zmiany w sekwencjach nie są wycofywane z transakcją ROLLBACK
. Zobacz stopki dotyczące sekwencji funkcji i CREATE SEQUENCE
.
Wycofywania są oczekiwane i normalne. Występują z powodu:
- zakleszczenia spowodowane konfliktem kolejności aktualizacji lub innymi blokadami między dwiema transakcjami;
- optymistyczne cofanie blokowania w Hibernate;
- przejściowe błędy klienta;
- konserwacja serwera przez administratora danych;
- konflikty serializacji w
SERIALIZABLE
lub migawki izolacji transakcji
... i nie tylko.
Twoja aplikacja będzie miała „dziury” w numeracji faktur, w których występują te wycofania. Dodatkowo nie ma gwarancji zamówienia, więc jest całkowicie możliwe, że transakcja o późniejszym numerze sekwencji zostanie zatwierdzona wcześniej (czasami dużo wcześniej) niż jeden z późniejszym numerem.
Chrupanie:
Jest również normalne, że niektóre aplikacje, w tym Hibernate, pobierają więcej niż jedną wartość z sekwencji na raz i przekazują je do wewnętrznych transakcji. Jest to dopuszczalne, ponieważ nie należy oczekiwać, że wartości generowane przez sekwencję będą miały jakikolwiek sensowny porządek lub będą porównywalne w jakikolwiek sposób z wyjątkiem równości. W przypadku numerowania faktur również chcesz złożyć zamówienie, więc nie będziesz w ogóle szczęśliwy, jeśli Hibernate złapie wartości 5900-5999 i zacznie je rozdawać od 5999 licząc w dół lub na przemian w górę i w dół, więc numery faktur są następujące:n, n+1, n+49, n+2, n+48, ... n+50, n+99, n+51, n+98, [n+52 utracone do wycofania], n+97, ... . Tak, alokator „góra-dół” istnieje w trybie Hibernate.
To nie pomoże, chyba że zdefiniujesz indywidualny @SequenceGenerator
s w Twoich mapach Hibernate lubi udostępniać jedną sekwencję dla każdej wygenerowany identyfikator. Brzydkie.
Właściwe użycie:
Sekwencja jest odpowiednia tylko wtedy, gdy tylko wymagać, aby numeracja była niepowtarzalna. Jeśli chcesz, aby był on również monotoniczny i porządkowy, powinieneś pomyśleć o użyciu zwykłej tabeli z polem licznika poprzez UPDATE ... RETURNING
lub SELECT ... FOR UPDATE
("pesymistyczne blokowanie" w Hibernate) lub poprzez Hibernate optymistyczne blokowanie. W ten sposób możesz zagwarantować przyrosty bez przerw bez dziur i niepoprawnych wpisów.
Co zrobić zamiast tego:
Stwórz stół tylko dla licznika. Umieść w nim jeden wiersz i aktualizuj go w miarę czytania. To go zablokuje, uniemożliwiając innym transakcjom uzyskanie identyfikatora, dopóki twoje nie zostanie zatwierdzone.
Ponieważ wymusza to szeregowe działanie wszystkich transakcji, staraj się, aby transakcje generujące identyfikatory faktur były krótkie i unikaj wykonywania w nich więcej pracy, niż jest to konieczne.
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;
Alternatywnie możesz:
- Zdefiniuj encję dla numeru faktury, dodaj
@Version
kolumna i pozwól optymistycznemu blokowaniu zająć się konfliktami; - Zdefiniuj encję dla numeru faktury i użyj jawnego, pesymistycznego blokowania w Hibernate, aby wybrać ... do aktualizacji, a następnie do aktualizacji.
Wszystkie te opcje będą serializować twoje transakcje - albo przez wycofanie konfliktów za pomocą @Version, albo ich zablokowanie (zablokowanie) do momentu zatwierdzenia przez posiadacza blokady. Tak czy inaczej, sekwencje bez przerw naprawdę spowolnij ten obszar aplikacji, więc używaj sekwencji bez przerw tylko wtedy, gdy musisz.
@GenerationType.TABLE
:Kuszące jest użycie @GenerationType.TABLE
z @TableGenerator(initialValue=1, ...)
. Niestety, chociaż GenerationType.TABLE pozwala określić rozmiar alokacji za pośrednictwem @TableGenerator, nie zapewnia żadnych gwarancji dotyczących zachowania kolejności lub wycofywania. Zobacz specyfikację JPA 2.0, rozdział 11.1.46 i 11.1.17. W szczególności „Ta specyfikacja nie definiuje dokładnego zachowania tych strategii. i przypis 102 "Aplikacje przenośne nie powinny używać adnotacji GeneratedValue w innych trwałych polach lub właściwościach [niż @Id
klucze podstawowe]" . Dlatego używanie @GenerationType.TABLE
jest niebezpieczne za numerację, która wymaga, aby była bez przerw lub numeracja, która nie znajduje się we właściwości klucza podstawowego, chyba że dostawca JPA daje więcej gwarancji niż standardowe.
Jeśli utkniesz z sekwencją :
Nadawca zauważa, że mają istniejące aplikacje korzystające z bazy danych, które już używają sekwencji, więc utknęli z nią.
Standard JPA nie gwarantuje, że możesz używać wygenerowanych kolumn z wyjątkiem @Id, możesz (a) to zignorować i kontynuować, o ile Twój dostawca Ci na to pozwoli, lub (b) wstawić z wartością domyślną i ponownie -odczytaj z bazy danych. To drugie jest bezpieczniejsze:
@Column(name = "inv_seq", insertable=false, updatable=false)
public Integer getInvoiceSeq() {
return invoiceSeq;
}
Z powodu insertable=false
dostawca nie będzie próbował określić wartości dla kolumny. Możesz teraz ustawić odpowiedni DEFAULT
w bazie danych, np. nextval('some_sequence')
i będzie uhonorowany. Być może będziesz musiał ponownie odczytać encję z bazy danych za pomocą EntityManager.refresh()
po utrwaleniu - nie jestem pewien, czy dostawca trwałości zrobi to za Ciebie, a nie sprawdziłem specyfikacji ani nie napisałem programu demonstracyjnego.
Jedynym minusem jest to, że wygląda na to, że kolumna nie może zostać utworzona @ NotNull lub nullable=false
, ponieważ dostawca nie rozumie, że baza danych ma wartość domyślną dla kolumny. Nadal może być NOT NULL
w bazie danych.
Jeśli masz szczęście, Twoje inne aplikacje również będą używać standardowego podejścia polegającego na pominięciu kolumny sekwencji w INSERT
listy kolumn lub jawne określenie słowa kluczowego DEFAULT
jako wartość, zamiast wywoływania nextval
. Nie będzie trudno się tego dowiedzieć, włączając log_statement = 'all'
w postgresql.conf
i przeszukiwanie dzienników. Jeśli tak, możesz faktycznie przełączyć wszystko na bez przerw, jeśli zdecydujesz, że musisz, zastępując DEFAULT
z BEFORE INSERT ... FOR EACH ROW
funkcja wyzwalacza, która ustawia NEW.invoice_number
ze stołu.