Aller au contenu

IndexedDB : CRUD et 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 volontairement

L’API native utilise des événements. Deux fonctions utilitaires simplifient tout :

src/lib/idb-outils.js
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).

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.

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).

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.

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.

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é.

// Incorrect : le fetch fait fermer la transaction
const 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 ensuite
const 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.

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.

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.

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.

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.