Il existe de nombreuses façons de gérer la génération d’ID dans PostgreSQL, mais j’ai choisi d’étudier ces quatre approches :
- Incrémentation automatique (
SERIAL
Type de données) - Mise en cache des séquences
- Incrémentation de séquence avec gestion des ID côté client
- Génération UUID
En fonction de votre application et de vos tables de base de données sous-jacentes, vous pouvez choisir d’utiliser une ou plusieurs de ces options. Ci-dessous, j’expliquerai comment chacun peut être réalisé dans Node.js en utilisant l’ORM Sequelize.
1. Incrémentation automatique
La plupart des développeurs choisissent l’option la plus simple avant d’explorer les optimisations potentielles, et je ne suis pas différent. Voici comment vous pouvez créer un champ ID auto-incrémenté dans vos définitions de modèle Sequelize :
// Sequelize
const { DataTypes } = require('sequelize');
const Product = sequelize.define(
"product",
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
title: {
type: DataTypes.STRING,
}
}
);
Si vous connaissez Sequelize, vous ne serez pas étranger à cette syntaxe, mais d’autres pourraient se demander ce qui se passe réellement sous le capot.
Les autoIncrement
flag indique à PostgreSQL de créer un id
colonne avec un SERIAL
Type de données. Ce type de données crée implicitement un SEQUENCE
qui appartient à la products
les tables id
colonne.
// PostgreSQL equivalent
CREATE SEQUENCE products_id_seq;
CREATE TABLE products
(
id INT NOT NULL DEFAULT NEXTVAL('products_id_seq'),
title VARCHAR(255)
);
Lors de l’insertion d’un produit dans notre table, nous n’avons pas besoin de fournir une valeur pour id
car il est automatiquement généré à partir de la séquence sous-jacente.
Nous pouvons simplement exécuter ce qui suit pour insérer un produit :
// Sequelize
await Product.create({title: "iPad Pro"});
//PostgreSQL equivalent
INSERT INTO products (title) VALUES ('iPad Pro');
La suppression de notre table supprimera également la séquence créée automatiquement, products_id_seq
:
// Sequelize
await Product.drop();
// PostgreSQL equivalent
DROP TABLE products CASCADE;
Bien que cette approche soit extrêmement facile à mettre en œuvre, notre serveur PostgreSQL doit accéder à la séquence pour obtenir sa prochaine valeur à chaque écriture, ce qui a un coût de latence. Ceci est particulièrement mauvais dans les déploiements distribués.
Maintenant que nous avons les bases, essayons d’accélérer les choses. Comme nous le savons tous, « le cache est roi ».
2. Mise en cache des séquences
Bien que le autoIncrement
flag dans la définition du modèle Sequelize élimine totalement le besoin d’interagir directement avec les séquences, il existe des scénarios où vous pourriez envisager de le faire. Par exemple, que se passe-t-il si vous souhaitez accélérer les écritures en mettant en cache les valeurs de séquence ? N’ayez crainte, avec un petit effort supplémentaire, nous pouvons y arriver.
Sequelize n’a pas de support API pour que cela se produise, comme indiqué sur Github, mais il existe une solution de contournement simple. En utilisant le literal
fonction, nous pouvons accéder à une séquence prédéfinie dans notre modèle :
const { literal, DataTypes } = require('sequelize');
const Product = sequelize.define("product", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
defaultValue: literal("nextval('custom_sequence')"),
},
});
sequelize.beforeSync(() => {
await sequelize.query('CREATE SEQUENCE IF NOT EXISTS custom_sequence CACHE 50');
});
await sequelize.sync();
Ce n’est pas si mal. Alors voilà ce qui a changé :
- Nous avons créé notre propre séquence, nommée
custom_sequence
qui est utilisé pour définir la valeur par défaut de notre ID de produit. - Cette séquence est créée dans le
beforeSync
crochet, il sera donc créé avant la table des produits et sonCACHE
valeur a été fixée à 50. - Les
defaultValue
est défini sur la valeur suivante dans notre séquence personnalisée.
Eh bien, qu’en est-il du cache ? Les séquences dans PostgreSQL peuvent éventuellement être fournies CACHE
valeur lors de la création, qui alloue un certain nombre de valeurs à stocker en mémoire par session. Avec notre cache défini sur 50, voici comment cela fonctionne :
//Database Session A
> SELECT nextval('custom_sequence');
1
> SELECT nextval('custom_sequence');
2
//Database Session B
> SELECT nextval('custom_sequence');
51
>
52
Pour une application avec plusieurs connexions de base de données, comme une exécutant des microservices ou plusieurs serveurs derrière un équilibreur de charge, chaque connexion recevra un ensemble de valeurs mises en cache. Aucune session ne contiendra de valeurs en double dans son cache, garantissant qu’il n’y a pas de collisions lors de l’insertion d’enregistrements. En fait, selon la configuration de votre base de données, vous pourriez trouver des lacunes dans votre séquencement id
colonne si une connexion à la base de données échoue et est redémarrée sans utiliser toutes les valeurs allouées dans son cache. Cependant, ce n’est généralement pas un problème, car nous ne nous préoccupons que de l’unicité.
Alors, quel est le point? Vitesse. La vitesse est le point!
En mettant en cache les valeurs sur notre backend PostgreSQL et en les stockant en mémoire, nous sommes en mesure de récupérer très rapidement la valeur suivante. Cela permet à la base de données de s’adapter, sans avoir besoin d’obtenir à plusieurs reprises la valeur de séquence suivante à partir du nœud maître lors des écritures. Bien sûr, la mise en cache présente l’inconvénient d’une contrainte de mémoire accrue sur le serveur PostgreSQL.
Selon votre infrastructure, cela pourrait être une optimisation valable.
3. Séquençage côté client
La mise en cache des séquences améliore les performances en mettant en cache les valeurs sur notre backend PostgreSQL. Comment pourrions-nous utiliser une séquence pour mettre en cache des valeurs sur notre client à la place ?
Les séquences dans PostgreSQL ont un paramètre supplémentaire appelé INCREMENT BY
qui peut être utilisé pour y parvenir :
// DB Initialization
const { literal, DataTypes } = require('sequelize');
const Product = sequelize.define("product", {
id: {
type: DataTypes.INTEGER,
primaryKey: true
},
});
sequelize.beforeSync(() => {
await sequelize.query('CREATE SEQUENCE IF NOT EXISTS custom_sequence INCREMENT BY 50');
});
await sequelize.sync();
// Caller
let startVal = await sequelize.query("SELECT nextval('custom_sequence')");
let limit = startVal + 50;
if (startVal >= limit) {
startVal = await sequelize.query("SELECT nextval('custom_sequence')");
limit = startVal + 50;
}
await Product.create({id: startVal, title: "iPad Pro"})
startVal += 1;
Ici, nous utilisons notre séquence personnalisée d’une manière légèrement différente. Aucune valeur par défaut n’est fournie à notre définition de modèle. Au lieu de cela, nous utilisons cette séquence pour définir des valeurs uniques côté client, en parcourant les valeurs de la plage d’incréments. Lorsque nous avons épuisé toutes les valeurs de cette plage, nous effectuons un autre appel à notre base de données pour obtenir la valeur suivante de notre séquence afin de « rafraîchir » notre plage.
Voici un exemple :
// Database Session A
> SELECT nextval('custom_sequence');
1
*
inserts 50 records
// id 1
// id 2
...
// id 50
*
> SELECT nextval('custom_sequence');
151
// Database Session B
> SELECT nextval('custom_sequence');
51
* inserts 50 records before Session A has used all numbers in its range *
> SELECT nextval('custom_sequence');
101
Session de base de données A se connecte et reçoit la première valeur de la séquence. Base de données Session B se connecte et reçoit la valeur de 51 car nous avons défini notre INCREMENT BY
valeur à 50
. Comme nos solutions d’auto-incrémentation, nous pouvons nous assurer qu’il n’y a pas de collisions d’ID en référençant notre séquence PostgreSQL pour déterminer la valeur de départ de notre plage.
Quels problèmes pourraient survenir de cette solution ?
Eh bien, il est possible qu’un administrateur de base de données choisisse d’augmenter ou de diminuer le INCREMENT BY
valeur pour une séquence particulière, sans que les développeurs d’applications soient informés de ce changement. Cela briserait la logique de l’application.
Comment pouvons-nous bénéficier du séquençage côté client ?
Si vous disposez d’une grande quantité de mémoire disponible sur vos nœuds de serveur d’applications, cela peut constituer un avantage potentiel en termes de performances par rapport à la mise en cache des séquences sur les nœuds de base de données.
En fait, vous vous demandez peut-être s’il est possible d’utiliser un cache sur le client et le serveur dans la même implémentation. La réponse courte est oui. En créant une séquence avec CACHE
et INCREMENT BY
valeurs, nous bénéficions d’un cache côté serveur de nos valeurs de séquence et d’un cache côté client pour la prochaine valeur de notre plage. Cette optimisation des performances offre le meilleur des deux mondes si les contraintes de mémoire ne sont pas la principale préoccupation.
Assez avec les séquences déjà. Passons aux identifiants uniques.
4. Génération d’UUID
Jusqu’à présent, nous avons couvert trois façons de générer des identifiants séquentiels basés sur des nombres entiers. Un autre type de données, l’identifiant universel unique (UUID), supprime entièrement le besoin de séquences.
Un UUID est un identifiant de 128 bits, qui est livré avec la garantie d’unicité en raison de la probabilité incroyablement faible que le même identifiant soit généré deux fois.
PostgreSQL est livré avec une extension appelée pgcrypto, qui peut être installée pour générer des UUID avec le gen_random_uuid
une fonction. Cette fonction génère une valeur UUID pour une colonne de base de données, à peu près la même que nextval
est utilisé avec des séquences.
De plus, Node.js possède plusieurs packages qui génèrent des UUID, tels que, vous l’avez deviné, uuid.
// Sequelize
const { literal, DataTypes } = require('sequelize');
const Product = sequelize.define(
"product",
{
id: {
type: DataTypes.UUID,
defaultValue: literal('gen_random_uuid()')
primaryKey: true,
},
title: {
type: DataTypes.STRING,
}
}
);
sequelize.beforeSync(() => {
await sequelize.query('CREATE EXTENSION IF NOT EXISTS "pgcrypto"');
});
// PostreSQL Equivalent
CREATE TABLE products
(
id UUID NOT NULL DEFAULT gen_random_uuid(),
title VARCHAR(255)
);
Cela nous permet de générer un UUID côté client, avec un côté serveur par défaut, si nécessaire.
Une approche basée sur l’UUID apporte des avantages uniques, la nature aléatoire du type de données étant utile pour certaines migrations de données. Ceci est également utile pour la sécurité de l’API, car l’identifiant unique est…