PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Annotazione di sospensione per il tipo seriale PostgreSQL

Pericolo: La tua domanda implica che potresti commettere un errore di progettazione:stai cercando di utilizzare una sequenza di database per un valore "aziendale" presentato agli utenti, in questo caso i numeri di fattura.

Non usare una sequenza se hai bisogno di qualcosa di più che testare il valore per l'uguaglianza. Non ha ordine. Non ha "distanza" da un altro valore. È solo uguale, o non uguale.

Ripristino: Le sequenze non sono generalmente appropriate per tali usi perché le modifiche alle sequenze non vengono ripristinate con la transazione ROLLBACK . Vedi i piè di pagina su functions-sequence e CREATE SEQUENCE .

I rollback sono previsti e normali. Si verificano a causa di:

  • Deadlock causati da ordini di aggiornamento in conflitto o altri blocchi tra due transazioni;
  • rollback ottimistici dei blocchi in Hibernate;
  • errori temporanei del client;
  • Manutenzione del server da parte del DBA;
  • Conflitti di serializzazione in SERIALIZABLE o transazioni di isolamento degli snapshot

... e altro ancora.

La tua applicazione avrà "buchi" nella numerazione delle fatture in cui si verificano tali rollback. Inoltre, non esiste alcuna garanzia di ordinazione, quindi è del tutto possibile che una transazione con un numero di sequenza successivo venga confermata prima (a volte molto precedente) di uno con un numero successivo.

Chunking:

È anche normale che alcune applicazioni, incluso Hibernate, prendano più di un valore da una sequenza alla volta e li distribuiscano alle transazioni internamente. Ciò è consentito perché non dovresti aspettarti che i valori generati dalla sequenza abbiano un ordine significativo o siano comparabili in alcun modo tranne che per l'uguaglianza. Per la numerazione delle fatture, vuoi anche ordinare, quindi non sarai per niente felice se Hibernate prende i valori 5900-5999 e inizia a distribuirli da 5999 contando alla rovescia o in alternativa up-the-down, in modo che i numeri di fattura siano:n, n+1, n+49, n+2, n+48, ... n+50, n+99, n+51, n+98, [n+52 persi per rollback], n+97, ... . Sì, l'allocatore high-the-low esiste in Hibernate.

Non aiuta a meno che tu non definisca il singolo @SequenceGenerator s nelle tue mappature, Hibernate ama condividere una singola sequenza per ogni ID generato, anche. Brutto.

Uso corretto:

Una sequenza è appropriata solo se solo richiedono che la numerazione sia univoca. Se anche tu hai bisogno che sia monotono e ordinale, dovresti pensare di usare una normale tabella con un campo contatore tramite UPDATE ... RETURNING o SELECT ... FOR UPDATE ("blocco pessimistico" in Hibernate) o tramite il blocco ottimistico di Hibernate. In questo modo puoi garantire incrementi gapless senza buchi o voci fuori ordine.

Cosa fare invece:

Crea una tabella solo per un contatore. Avere una singola riga in esso e aggiornarlo mentre lo leggi. Questo lo bloccherà, impedendo ad altre transazioni di ottenere un ID fino a quando il tuo non si impegna.

Poiché obbliga tutte le tue transazioni a funzionare in serie, cerca di mantenere brevi le transazioni che generano ID fattura ed evita di fare più lavoro del necessario.

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;

In alternativa, puoi:

  • Definisci un'entità per fattura_number, aggiungi un @Version colonna e lascia che il blocco ottimistico si occupi dei conflitti;
  • Definisci un'entità per numero_fattura e usa il blocco pessimistico esplicito in Hibernate per fare una selezione... per l'aggiornamento, quindi un aggiornamento.

Tutte queste opzioni serializzeranno le tue transazioni, sia ripristinando i conflitti utilizzando @Version, sia bloccandole (blocco) fino a quando il titolare del blocco non esegue il commit. In ogni caso, le sequenze gapless veramente rallenta quell'area dell'applicazione, quindi usa sequenze gapless solo quando necessario.

@GenerationType.TABLE :Si è tentati di usare @GenerationType.TABLE con un @TableGenerator(initialValue=1, ...) . Sfortunatamente, mentre GenerationType.TABLE ti consente di specificare una dimensione di allocazione tramite @TableGenerator, non fornisce alcuna garanzia sul comportamento di ordinazione o rollback. Vedere la specifica JPA 2.0, sezione 11.1.46 e 11.1.17. In particolare "Questa specifica non definisce il comportamento esatto di queste strategie. e nota 102 "Le applicazioni portatili non devono utilizzare l'annotazione GeneratedValue su altri campi o proprietà persistenti [di @Id chiavi primarie]" . Quindi non è sicuro usare @GenerationType.TABLE per la numerazione che devi essere gapless o per la numerazione che non si trova su una proprietà della chiave primaria, a meno che il tuo provider JPA non fornisca più garanzie rispetto allo standard.

Se sei bloccato con una sequenza :

Il poster nota che hanno app esistenti che utilizzano il DB che utilizzano già una sequenza, quindi sono bloccati con esso.

Lo standard JPA non garantisce che tu possa utilizzare le colonne generate tranne che su @Id, puoi (a) ignorarlo e andare avanti purché il tuo provider te lo consenta, oppure (b) eseguire l'inserimento con un valore predefinito e ri -lettura dal database. Quest'ultimo è più sicuro:

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

A causa di insertable=false il provider non proverà a specificare un valore per la colonna. Ora puoi impostare un DEFAULT adatto nel database, come nextval('some_sequence') e sarà onorato. Potrebbe essere necessario rileggere l'entità dal database con EntityManager.refresh() dopo averlo persistente - non sono sicuro se il provider di persistenza lo farà per te e non ho controllato le specifiche o scritto un programma demo.

L'unico aspetto negativo è che sembra che la colonna non possa essere creata @ NotNull o nullable=false , poiché il provider non comprende che il database ha un valore predefinito per la colonna. Può essere ancora NOT NULL nel database.

Se sei fortunato, anche le altre tue app utilizzeranno l'approccio standard di omettere la colonna sequenza da INSERT o specificando esplicitamente la parola chiave DEFAULT come valore, invece di chiamare nextval . Non sarà difficile scoprirlo abilitando log_statement = 'all' in postgresql.conf e cercare i log. Se lo fanno, puoi effettivamente cambiare tutto su gapless se decidi di farlo sostituendo il tuo DEFAULT con un BEFORE INSERT ... FOR EACH ROW funzione di attivazione che imposta NEW.invoice_number dal bancone.