PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Annotation Hibernate pour le type de série PostgreSQL

Danger : Votre question implique que vous commettez peut-être une erreur de conception - vous essayez d'utiliser une séquence de base de données pour une valeur "commerciale" qui est présentée aux utilisateurs, dans ce cas des numéros de facture.

N'utilisez pas de séquence si vous avez besoin d'autre chose que de tester la valeur d'égalité. Il n'a pas d'ordre. Il n'a pas de "distance" par rapport à une autre valeur. C'est juste égal, ou pas égal.

Rollback : Les séquences ne sont généralement pas appropriées pour de telles utilisations car les modifications apportées aux séquences ne sont pas annulées avec la transaction ROLLBACK . Voir les pieds de page sur functions-sequence et CREATE SEQUENCE .

Les retours en arrière sont attendus et normaux. Ils se produisent en raison de :

  • les interblocages causés par un ordre de mise à jour conflictuel ou d'autres blocages entre deux transactions ;
  • annulations de verrouillage optimistes dans Hibernate ;
  • erreurs client passagères ;
  • maintenance du serveur par le DBA ;
  • conflits de sérialisation dans SERIALIZABLE ou transactions d'isolement d'instantané

... et plus encore.

Votre application aura des "trous" dans la numérotation des factures là où ces retours en arrière se produisent. De plus, il n'y a aucune garantie de commande, il est donc tout à fait possible qu'une transaction avec un numéro de séquence ultérieur soit validée plus tôt (parfois beaucoup plus tôt) qu'un avec un numéro plus tard.

Fragmentation :

Il est également normal que certaines applications, y compris Hibernate, saisissent plusieurs valeurs d'une séquence à la fois et les distribuent aux transactions en interne. Cela est permis car vous n'êtes pas censé vous attendre à ce que les valeurs générées par la séquence aient un ordre significatif ou soient comparables de quelque manière que ce soit, sauf pour l'égalité. Pour la numérotation des factures, vous voulez aussi commander, donc vous ne serez pas du tout heureux si Hibernate saisit les valeurs 5900-5999 et commence à les distribuer à partir de 5999 en comptant vers le bas ou alternativement vers le haut puis vers le bas, donc vos numéros de facture vont :n, n+1, n+49, n+2, n+48, ... n+50, n+99, n+51, n+98, [n+52 perdus pour rollback], n+97, ... . Oui, l'allocateur haut puis bas existe dans Hibernate.

Cela n'aide pas à moins que vous ne définissiez un @SequenceGenerator individuel s dans vos mappages, Hibernate aime partager une seule séquence pour chaque ID généré, aussi. Moche.

Utilisation correcte :

Une séquence n'est appropriée que si vous seulement exigent que la numérotation soit unique. Si vous avez également besoin qu'il soit monotone et ordinal, vous devriez penser à utiliser une table ordinaire avec un champ compteur via UPDATE ... RETURNING ou SELECT ... FOR UPDATE ("verrouillage pessimiste" dans Hibernate) ou via le verrouillage optimiste Hibernate. De cette façon, vous pouvez garantir des incréments sans espace, sans trous ni entrées dans le désordre.

Que faire à la place :

Créez une table juste pour un comptoir. Conservez une seule ligne et mettez-la à jour au fur et à mesure que vous la lisez. Cela le verrouillera, empêchant les autres transactions d'obtenir un identifiant jusqu'à ce que la vôtre soit validée.

Parce que cela force toutes vos transactions à fonctionner en série, essayez de garder les transactions qui génèrent des ID de facture courtes et évitez d'y travailler plus que nécessaire.

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;

Vous pouvez également :

  • Définissez une entité pour le numéro de facture, ajoutez un @Version et laissez le verrouillage optimiste s'occuper des conflits ;
  • Définissez une entité pour le numéro de facture et utilisez un verrouillage pessimiste explicite dans Hibernate pour effectuer une sélection... pour une mise à jour puis une mise à jour.

Toutes ces options sérialiseront vos transactions - soit en annulant les conflits à l'aide de @Version, soit en les bloquant (verrouillant) jusqu'à ce que le détenteur du verrou s'engage. Dans tous les cas, les séquences sans interruption seront vraiment ralentissez cette zone de votre application, n'utilisez donc des séquences sans interruption que lorsque vous en avez besoin.

@GenerationType.TABLE :Il est tentant d'utiliser @GenerationType.TABLE avec un @TableGenerator(initialValue=1, ...) . Malheureusement, alors que GenerationType.TABLE vous permet de spécifier une taille d'allocation via @TableGenerator, il ne fournit aucune garantie sur le comportement de commande ou de restauration. Voir la spécification JPA 2.0, section 11.1.46 et 11.1.17. En particulier "Cette spécification ne définit pas le comportement exact de ces stratégies. et note de bas de page 102 "Les applications portables ne doivent pas utiliser l'annotation GeneratedValue sur d'autres champs ou propriétés persistants [que @Id clés primaires]" . Il n'est donc pas sûr d'utiliser @GenerationType.TABLE pour la numérotation dont vous avez besoin pour être sans espace ou la numérotation qui n'est pas sur une propriété de clé primaire à moins que votre fournisseur JPA ne fasse plus de garanties que la norme.

Si vous êtes coincé avec une séquence :

L'affiche note qu'ils ont des applications existantes utilisant la base de données qui utilisent déjà une séquence, donc ils sont coincés avec.

La norme JPA ne garantit pas que vous pouvez utiliser des colonnes générées sauf sur @Id, vous pouvez (a) ignorer cela et continuer tant que votre fournisseur vous le permet, ou (b) faire l'insertion avec une valeur par défaut et re -lire à partir de la base de données. Ce dernier est plus sûr :

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

À cause de insertable=false le fournisseur n'essaiera pas de spécifier une valeur pour la colonne. Vous pouvez maintenant définir un DEFAULT approprié dans la base de données, comme nextval('some_sequence') et ce sera honoré. Vous devrez peut-être relire l'entité à partir de la base de données avec EntityManager.refresh() après l'avoir persisté - je ne sais pas si le fournisseur de persistance le fera pour vous et je n'ai pas vérifié les spécifications ni écrit de programme de démonstration.

Le seul inconvénient est qu'il semble que la colonne ne peut pas être faite @ NotNull ou nullable=false , car le fournisseur ne comprend pas que la base de données a une valeur par défaut pour la colonne. Il peut toujours être NOT NULL dans la base de données.

Si vous avez de la chance, vos autres applications utiliseront également l'approche standard consistant à omettre la colonne de séquence de INSERT ou en spécifiant explicitement le mot-clé DEFAULT comme valeur, au lieu d'appeler nextval . Il ne sera pas difficile de le savoir en activant log_statement = 'all' dans postgresql.conf et rechercher les journaux. Si tel est le cas, vous pouvez en fait tout passer en mode sans interruption si vous en décidez en remplaçant votre DEFAULT avec un BEFORE INSERT ... FOR EACH ROW fonction de déclenchement qui définit NEW.invoice_number de la table du comptoir.