Aller au contenu

IndexedDB : index, plages et curseurs

IndexedDB n’a pas de langage de requête : on interroge par clé, par index et par plage, et on parcourt avec des curseurs. Les exemples utilisent les magasins clients et commandes (Ouvrir une base et définir le schéma) et les utilitaires requete / terminee (CRUD et transactions).

async function clientParEmail(email) {
const db = await ouvrirBase();
const index = db.transaction('clients').objectStore('clients').index('email');
return requete(index.get(email)); // premier enregistrement correspondant
}
async function clientsParNom(nom) {
const db = await ouvrirBase();
const index = db.transaction('clients').objectStore('clients').index('nom');
return requete(index.getAll(nom)); // tous les enregistrements avec ce nom
}

Un index permet get, getAll, getAllKeys, count et openCursor, comme le magasin, mais selon la valeur du champ indexé.

IDBKeyRange.only('Alice'); // égal à
IDBKeyRange.lowerBound(18); // >= 18
IDBKeyRange.lowerBound(18, true); // > 18
IDBKeyRange.upperBound(65); // <= 65
IDBKeyRange.upperBound(65, true); // < 65
IDBKeyRange.bound(18, 65); // 18 <= x <= 65
IDBKeyRange.bound(18, 65, true, false); // 18 < x <= 65
async function commandesDuMois(debut, fin) {
const db = await ouvrirBase();
const index = db
.transaction('commandes')
.objectStore('commandes')
.index('date');
const plage = IDBKeyRange.bound(debut, fin); // dates : objets Date ou chaînes ISO
return requete(index.getAll(plage));
}

Les chaînes ISO (2026-01-15) se comparent correctement dans l’ordre alphabétique : pratique pour des plages de dates.

function plagePrefixe(prefixe) {
return IDBKeyRange.bound(prefixe, prefixe + '￿');
}
const clients = await requete(
db
.transaction('clients')
.objectStore('clients')
.index('nom')
.getAll(plagePrefixe('Al')),
);

￿ est le dernier caractère Unicode de ce plan : la plage couvre toutes les chaînes qui commencent par le préfixe. La comparaison est sensible à la casse : stocker une version en minuscules (nomMin) pour une recherche insensible à la casse.

const premiers = await requete(index.getAll(null, 20)); // 20 premiers
const cles = await requete(index.getAllKeys(plage, 100)); // 100 clés

Page suivante : repartir après la dernière valeur reçue (pagination par clé, plus efficace qu’un décalage) :

async function page(dernierNom, taille = 20) {
const db = await ouvrirBase();
const index = db.transaction('clients').objectStore('clients').index('nom');
const plage =
dernierNom === undefined ? null : IDBKeyRange.lowerBound(dernierNom, true);
return requete(index.getAll(plage, taille));
}

Si plusieurs enregistrements peuvent avoir la même valeur d’index, utiliser un curseur avec la clé primaire (ci-dessous) pour ne pas en sauter.

Un curseur parcourt les enregistrements un par un, dans l’ordre de la clé ou de l’index :

async function parcourir(magasinOuIndex, rappel) {
await new Promise((resolve, reject) => {
const req = magasinOuIndex.openCursor();
req.onsuccess = () => {
const curseur = req.result;
if (!curseur) return resolve(); // fin du parcours
rappel(curseur.value, curseur.key);
curseur.continue();
};
req.onerror = () => reject(req.error);
});
}
const db = await ouvrirBase();
const tx = db.transaction('clients');
await parcourir(tx.objectStore('clients').index('nom'), (client) =>
console.log(client.nom),
);
Élément Rôle
openCursor(plage?, direction?) Ouvre le curseur (valeurs)
openKeyCursor Curseur sur les clés seulement (plus léger)
curseur.value, .key, .primaryKey Valeur, clé de l’index, clé primaire
curseur.continue() Avance
curseur.advance(n) Saute n enregistrements
curseur.update(valeur) Remplace l’enregistrement courant
curseur.delete() Supprime l’enregistrement courant

Directions : 'next' (défaut), 'prev' (ordre décroissant), 'nextunique', 'prevunique'.

const req = index.openCursor(null, 'prev'); // du plus grand au plus petit
async function marquerExpediees(db) {
const tx = db.transaction('commandes', 'readwrite');
const index = tx.objectStore('commandes').index('date');
await new Promise((resolve, reject) => {
const req = index.openCursor(IDBKeyRange.upperBound('2026-01-01'));
req.onsuccess = () => {
const c = req.result;
if (!c) return resolve();
if (c.value.statut === 'payee')
c.update({ ...c.value, statut: 'expediee' });
c.continue();
};
req.onerror = () => reject(req.error);
});
await terminee(tx);
}

Un curseur est plus lent que getAll pour de petits volumes (un aller-retour par enregistrement) mais indispensable pour les gros volumes (mémoire constante) et pour modifier ou supprimer en parcourant.

Avec une bibliothèque comme idb, les curseurs s’écrivent avec for await (voir Avec idb et Dexie).

IndexedDB ne combine pas plusieurs index dans une requête. Solutions :

Approche Principe
Index composé createIndex('statutEtDate', ['statut', 'date']), puis IDBKeyRange.bound(['payee', debut], ['payee', fin])
Filtrer en JavaScript Lire par le champ le plus sélectif, filtrer le reste dans le code
Dexie Syntaxe de requête plus riche (voir la page suivante)
// à la création du schéma
commandes.createIndex('statutEtDate', ['statut', 'date']);
// requête : commandes payées en janvier
const plage = IDBKeyRange.bound(
['payee', '2026-01-01'],
['payee', '2026-01-31'],
);
const resultat = await requete(
db
.transaction('commandes')
.objectStore('commandes')
.index('statutEtDate')
.getAll(plage),
);

Les résultats sont triés par la clé de l’index utilisé ; pour un autre tri, créer un index sur ce champ, ou trier en JavaScript après lecture :

const tries = (await requete(store.getAll())).sort((a, b) =>
b.date.localeCompare(a.date),
);
Symptôme Piste
DataError: The parameter is not a valid key Plage ou valeur de type différent de la clé indexée (nombre contre chaîne), ou valeur null/booléen
Un enregistrement est absent de l’index Son champ manque ou n’est pas une clé valide
NotFoundError sur index('x') Index non créé (schéma ou version)
La pagination saute des éléments Plusieurs enregistrements ont la même valeur d’index : curseur avec clé primaire
Mémoire élevée avec getAll Limiter, ou parcourir avec un curseur
TransactionInactiveError pendant un curseur await d’une opération étrangère dans le rappel
Tri sensible à la casse Stocker un champ normalisé et l’indexer

Ubuntu 24.04 et 26.04 : différences à connaître

Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »

Aucune.