IndexedDB : CRUD et transactions
Les transactions
Section intitulée « Les transactions »Toute opération se fait dans une transaction qui indique les magasins concernés et le mode :
const tx = db.transaction(['clients', 'commandes'], 'readwrite');const clients = tx.objectStore('clients');const commandes = tx.objectStore('commandes');| Mode | Usage |
|---|---|
readonly (défaut) |
Lectures (plusieurs en parallèle) |
readwrite |
Écritures (exclusives sur les magasins concernés) |
Une transaction est atomique : toutes ses opérations réussissent, ou aucune n’est appliquée. Elle se termine automatiquement quand il n’y a plus de requêtes en attente ; événements complete, error, abort.
tx.oncomplete = () => console.log('validée');tx.onerror = () => console.error('erreur', tx.error);tx.onabort = () => console.warn('annulée', tx.error);tx.abort(); // annuler volontairementEnvelopper en promesses
Section intitulée « Envelopper en promesses »L’API native utilise des événements. Deux fonctions utilitaires simplifient tout :
export function requete(req) { return new Promise((resolve, reject) => { req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); });}
export function terminee(tx) { return new Promise((resolve, reject) => { tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error ?? new DOMException('Transaction annulée', 'AbortError')); });}requete donne le résultat d’une opération ; terminee attend la validation de toute la transaction (c’est elle qui garantit que les données sont enregistrées).
Créer : add et put
Section intitulée « Créer : add et put »import { ouvrirBase } from './db.js';import { requete, terminee } from './idb-outils.js';
export async function ajouterClient(client) { const db = await ouvrirBase(); const tx = db.transaction('clients', 'readwrite'); const id = await requete(tx.objectStore('clients').add(client)); // renvoie la clé await terminee(tx); return id;}| Méthode | Comportement si la clé existe déjà |
|---|---|
add(valeur) |
Échoue (ConstraintError) |
put(valeur) |
Remplace (« upsert » : crée ou remplace) |
put remplace l’objet entier : lire, modifier, puis écrire pour une mise à jour partielle.
Lire : get, getAll, count
Section intitulée « Lire : get, getAll, count »export async function lireClient(id) { const db = await ouvrirBase(); return requete(db.transaction('clients').objectStore('clients').get(id)); // undefined si absent}
export async function tousLesClients() { const db = await ouvrirBase(); return requete(db.transaction('clients').objectStore('clients').getAll());}
export async function compterClients() { const db = await ouvrirBase(); return requete(db.transaction('clients').objectStore('clients').count());}| Méthode | Résultat |
|---|---|
get(clé) |
Un enregistrement, ou undefined |
getAll(plage?, limite?) |
Tableau d’enregistrements |
getAllKeys(plage?, limite?) |
Tableau de clés |
count(plage?) |
Nombre d’enregistrements |
getAll() sans limite charge tout en mémoire : préciser une limite ou utiliser un curseur sur de gros volumes (voir Index, plages et curseurs).
Modifier
Section intitulée « Modifier »export async function modifierClient(id, changements) { const db = await ouvrirBase(); const tx = db.transaction('clients', 'readwrite'); const magasin = tx.objectStore('clients'); const actuel = await requete(magasin.get(id)); if (!actuel) { tx.abort(); throw new Error(`Client ${id} introuvable`); } await requete(magasin.put({ ...actuel, ...changements, id })); await terminee(tx);}La lecture et l’écriture se font dans la même transaction readwrite : aucune autre écriture ne peut s’intercaler entre les deux.
Supprimer
Section intitulée « Supprimer »export async function supprimerClient(id) { const db = await ouvrirBase(); const tx = db.transaction('clients', 'readwrite'); tx.objectStore('clients').delete(id); await terminee(tx);}
export async function toutSupprimer() { const db = await ouvrirBase(); const tx = db.transaction('clients', 'readwrite'); tx.objectStore('clients').clear(); await terminee(tx);}delete accepte aussi une plage de clés (IDBKeyRange). clear() vide le magasin : irréversible.
Plusieurs opérations atomiques
Section intitulée « Plusieurs opérations atomiques »export async function passerCommande(commande, clientId) { const db = await ouvrirBase(); const tx = db.transaction(['commandes', 'clients'], 'readwrite'); const commandes = tx.objectStore('commandes'); const clients = tx.objectStore('clients');
commandes.add(commande); const client = await requete(clients.get(clientId)); clients.put({ ...client, nbCommandes: (client.nbCommandes ?? 0) + 1 });
await terminee(tx); // tout est enregistré, ou rien}Si une opération échoue (clé en double, par exemple), la transaction entière est annulée : la commande n’est pas enregistrée et le compteur n’est pas modifié.
Durée de vie d’une transaction
Section intitulée « Durée de vie d’une transaction »// Incorrect : le fetch fait fermer la transactionconst tx = db.transaction('clients', 'readwrite');const reponse = await fetch('/api/clients/1');tx.objectStore('clients').put(await reponse.json()); // TransactionInactiveError
// Correct : récupérer les données d'abord, écrire ensuiteconst donnees = await (await fetch('/api/clients/1')).json();const tx2 = db.transaction('clients', 'readwrite');tx2.objectStore('clients').put(donnees);await terminee(tx2);Règles : ne rien attendre d’extérieur au milieu d’une transaction ; faire les appels réseau et les calculs avant ; garder les transactions courtes.
Écrire en lot
Section intitulée « Écrire en lot »export async function importerClients(liste) { const db = await ouvrirBase(); const tx = db.transaction('clients', 'readwrite'); const magasin = tx.objectStore('clients'); for (const client of liste) magasin.put(client); // sans await : toutes en file dans la même transaction await terminee(tx);}Mettre en file toutes les écritures sans attendre chacune est bien plus rapide que d’ouvrir une transaction par enregistrement. Pour des dizaines de milliers d’éléments, découper en lots de quelques milliers.
Gestion des erreurs
Section intitulée « Gestion des erreurs »try { await ajouterClient({ nom: 'Alice', email: 'alice@exemple.com' });} catch (erreur) { switch (erreur?.name) { case 'ConstraintError': console.warn('Clé ou valeur unique déjà utilisée'); break; case 'QuotaExceededError': console.warn('Espace de stockage insuffisant'); break; case 'TransactionInactiveError': console.error("Transaction fermée : revoir l'ordre des opérations"); break; default: throw erreur; }}Les erreurs d’une requête remontent à la transaction (événement error de tx) et l’annulent, sauf si on appelle event.preventDefault() sur l’erreur de la requête.
Durabilité
Section intitulée « Durabilité »Par défaut, une transaction est considérée comme terminée quand les données sont dans le journal du navigateur ; une option permet de demander plus de garanties :
db.transaction('clients', 'readwrite', { durability: 'strict' }); // 'default', 'strict' ou 'relaxed'relaxed accélère les écritures au risque de perdre les dernières en cas de panne ; strict attend l’écriture sur le disque. Garder default sauf besoin particulier.
Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
TransactionInactiveError |
await d’une opération non IndexedDB au milieu : tout préparer avant |
ReadOnlyError |
Écriture dans une transaction readonly |
ConstraintError |
add d’une clé existante, ou index unique violé : utiliser put ou traiter l’erreur |
DataError |
Clé invalide ou absente (aucun keyPath et pas de clé fournie) |
DataCloneError |
Donnée non clonable (fonction, élément du DOM) |
InvalidStateError |
Connexion fermée ou transaction terminée : rouvrir, ouvrir une nouvelle transaction |
| Les écritures semblent perdues | Ne pas attendre terminee(tx) : seule la validation de la transaction garantit l’écriture |
| Lenteur d’import | Une transaction par élément : regrouper dans une transaction |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Aucune.