Explorer et modifier les données dans IndexedDB : curseurs, accès directs et transactions
Dans l’article Comprendre IndexedDB : une base de données locale en JavaScript, nous avons posé les bases : créer une base, définir sa structure, insérer des données. Nous allons maintenant apprendre à manipuler ces données côté client, en nous appuyant sur deux piliers essentiels : les transactions et les curseurs.
Ce second article montre comment accéder aux enregistrements (avec ou sans clé), parcourir une base avec finesse, modifier ou supprimer des entrées, et tirer parti d’une structure bien pensée. Pas à pas, nous poserons les fondations pour un usage fluide, souple et maîtrisé d’IndexedDB dans des contextes réels.
Accéder aux enregistrements : direct ou via curseur
Pour manipuler les données d’un store, deux approches complémentaires s’offrent à nous. La première consiste à utiliser des méthodes directes comme get, put ou delete, qui permettent de cibler un enregistrement précis à partir de sa clé — à condition bien sûr de connaître cette clé au préalable. Nous reviendrons sur ces méthodes plus loin dans l’article.
const request = store.get(3);
request.onsuccess = function(event) {
console.log(event.target.result);
};Mais si nous voulons explorer un ensemble de données, parcourir tous les éléments d’un store, ou appliquer des traitements sélectifs sans connaître à l’avance les clés concernées, une autre approche s’impose : l’usage d’un curseur (openCursor). Ce mécanisme permet de naviguer d’un enregistrement à l’autre, dans un ordre défini, et de manipuler dynamiquement les résultats rencontrés. C’est une manière souple et efficace de travailler avec IndexedDB lorsque les opérations doivent porter sur plusieurs entrées.
const request = store.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.value);
cursor.continue();
}
};Transactions : pourquoi et comment les utiliser
IndexedDB repose sur un modèle transactionnel, mais cela ne signifie pas que chaque opération doit nécessairement être incluse dans une transaction explicite. Pour des insertions ponctuelles ou des lectures simples, IndexedDB crée souvent des transactions implicites. En revanche, dès que plusieurs opérations doivent être coordonnées, une transaction structurée devient utile pour garantir la cohérence de l’ensemble.
// Création d'une transaction explicite en mode lecture-écriture sur le store "albums"
const tx = db.transaction(["albums"], "readwrite");
// Récupération du store ciblé dans le contexte de cette transaction
const store = tx.objectStore("albums");
store.put({ id: 2, titre: "Tago Mago", annee: 1971 });
store.delete(3); // Suppression d'un album obsolète
store.add({ id: 4, titre: "Ege Bamyasi", annee: 1972 });Dans l’exemple ci-dessus, les trois opérations sont encapsulées dans une même transaction (tx, ici un objet de type IDBTransaction). Le deuxième paramètre passé à db.transaction(), ici 'readwrite', indique le mode d’accès de la transaction :
'readonly': pour lire des données uniquement, sans pouvoir les modifier.'readwrite': pour lire et écrire dans le store.'versionchange': réservé aux phases de mise à jour de structure (dansonupgradeneeded).
Si l’une des opérations échoue, aucune ne sera appliquée : la transaction est entièrement annulée. Cela signifie que même les opérations qui semblaient avoir réussi ne seront pas enregistrées si l’une échoue. C’est un mécanisme comparable à un rollback automatique. : la transaction est entièrement annulée. Cela signifie que même les opérations qui semblaient avoir réussi ne seront pas enregistrées si l’une échoue.
Sans transaction explicite, chaque opération devrait être ouverte et gérée séparément, ce qui alourdit le code et augmente le risque d’incohérence si une action réussit et une autre échoue. En particulier, lorsqu’on souhaite modifier plusieurs données ou enchaîner des opérations dépendantes, une transaction structurée est indispensable.
Quand utiliser une transaction structurée ?
- Dès que plusieurs opérations doivent être coordonnées.
- Lorsque l’ordre ou la complétude des actions est critique.
- Pour les phases de mise à jour ou de suppression.
Quand peut-on s’en passer ?
- Pour une simple lecture ou écriture isolée.
- Lors d’une insertion ponctuelle, sans dépendance.
Même si IndexedDB crée parfois une transaction implicite pour une opération unique, mieux vaut comprendre et contrôler ce mécanisme pour éviter des effets indésirables ou silencieux.
Lire, mettre à jour et parcourir les données
Une fois les données enregistrées, il devient essentiel de savoir les retrouver, les modifier ou les supprimer. IndexedDB propose plusieurs méthodes pour cela, toutes accessibles via une transaction sur le store concerné. Ces opérations sont indépendantes du numéro de version : elles n’impliquent pas de changement structurel de la base, et ne déclenchent pas onupgradeneeded. Toutefois, elles modifient bien le contenu stocké localement.
Lire des données
Pour lire un enregistrement précis, il faut connaître sa clé. Si ce n’est pas le cas, on peut alors parcourir les données avec un curseur ou s’appuyer sur un index, si le store en dispose (voir le chapitre Parcourir les données à l’aide de curseurs et d’index). Ces approches permettent de localiser une entrée selon d’autres critères que sa clé principale, sans devoir tout balayer manuellement.
const transaction = db.transaction('albums', 'readonly');
const store = transaction.objectStore('albums');
const request = store.get(2);
request.onsuccess = (event) => {
const album = event.target.result;
console.log(album);
};Ici, nous récupérons l’album dont la clé est 2. Si cet identifiant n’existe pas dans la base, result renverra undefined. C’est une méthode simple, utilisée par exemple lorsqu’on veut préremplir un formulaire de modification.
Modifier des données existantes
Nous avons déjà croisé rapidement la méthode put() dans le premier article, lorsqu’il s’agissait d’insérer des données en laissant IndexedDB gérer la création ou la mise à jour automatique. Ici, nous allons l’utiliser pour remplacer une entrée existante.
La méthode put() est utile lorsqu’on souhaite corriger ou actualiser une entrée complète. Elle prend un objet avec une clé explicite (souvent un champ id), et le remplace entièrement dans le store s’il existe déjà. Cela signifie que les propriétés non mentionnées dans l’objet seront perdues.
// Remplacement de l’enregistrement id:2
store.put({ id: 2, title: 'Yeti', artist: 'Amon Düül II', year: 1970, remastered: true });Dans cet exemple, si l’objet initial avec l’identifiant 2 contenait un champ tags, celui-ci sera effacé car il n’est pas présent dans le nouvel objet. Il faut donc fournir un objet complet si l’on souhaite conserver les anciennes données. Pour une mise à jour partielle, nous verrons dans le chapitre suivant (Compléter des données) comment lire l’objet existant, le modifier, puis le réécrire avec put().
Autres cas à connaître et comportements à anticiper :
put() ne déclenche pas de mise à jour de la version de la base. Il agit uniquement dans le cadre de la structure actuelle. Si l’objet passé viole une contrainte (clé obligatoire absente, index unique violé, etc.), l’opération échouera : soit elle renverra une erreur explicite, soit elle sera ignorée silencieusement selon les navigateurs. Dans tous les cas, IndexedDB ne mettra pas à jour l’entrée concernée.
De même, l’ordre des identifiants n’est pas imposé : on peut insérer un id supérieur ou inférieur aux précédents, ce qui n’altère pas la validité de la commande. En revanche, le champ clé doit toujours être conforme à la structure du store — ce qui suppose que la version de la base ait déjà défini ce champ comme clé (keyPath) ou qu’un auto-incrément soit actif.
// Insertion d’un id non séquentiel (valide)
store.put({ id: 99, title: 'UFO', artist: 'Guru Guru', year: 1970 });
// Tentative avec une structure incompatible (ex : champ obligatoire manquant)
store.put({ title: 'Electric Silence' }); // générera une erreur si le keyPath est 'id' et non autoIncrementCompléter des données
Parfois, il ne s’agit pas de remplacer une entrée entière, mais d’ajouter une information complémentaire à une entrée existante. Cela suppose d’abord de la lire, puis de l’éditer.
const request = store.get(2);
request.onsuccess = (event) => {
const album = event.target.result;
album.tags = ['psychédélique', '1970s'];
store.put(album);
};Ce type d’opération permet d’enrichir progressivement les objets stockés, sans changer la structure globale du store. Cela ne nécessite pas de changement de version.
Supprimer des données
La méthode delete() permet de retirer un enregistrement de la base. Elle est utile pour les nettoyages manuels, les options de suppression, ou le contrôle d’espace.
store.delete(2); // Supprime l’entrée avec la clé 2Il est également possible de supprimer plusieurs entrées de manière conditionnelle. Par exemple, si l’on souhaite supprimer toutes les entrées comprises entre deux clés précises, on peut définir une plage et la parcourir avec un curseur :
const range = IDBKeyRange.bound(3, 9);
const indexRequest = store.openCursor(range);
indexRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
store.delete(cursor.primaryKey);
cursor.continue();
}
};Autre approche : supprimer selon une valeur de propriété. Comme IndexedDB ne permet pas d’exécuter directement une instruction conditionnelle sur une propriété non indexée, nous devons parcourir les enregistrements un à un avec un curseur, et supprimer ceux qui correspondent à notre critère. Par exemple, effacer tous les enregistrements dont l’année est 1970 :
const cursorReq = store.openCursor();
cursorReq.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && cursor.value.year === 1970) {
store.delete(cursor.primaryKey);
}
if (cursor) cursor.continue();
};Enfin, pour vider complètement le store, on peut utiliser la méthode clear() :
store.clear();Dans tous les cas, il est possible d’écouter les événements onsuccess pour s’assurer que l’opération a bien été prise en compte, ou déclencher d’autres actions ensuite :
const deleteRequest = store.delete(2);
deleteRequest.onsuccess = () => {
console.log("Suppression réussie");
};
deleteRequest.onerror = (event) => {
console.warn("Erreur lors de la suppression", event);
};Utiliser les index pour améliorer les recherches
Dans une base IndexedDB, la clé primaire (ou keyPath) permet de retrouver directement un enregistrement. Mais si nous voulons effectuer des recherches sur d’autres champs – comme l’année d’un album ou le nom d’un groupe – nous devons créer un index. Un index permet de rechercher plus rapidement des données en dehors de la clé principale.
Lors de la création ou mise à jour de la base (dans la phase onupgradeneeded), on utilise la méthode createIndex() sur un objectStore pour définir un nouvel index :
// Exemple : ajouter un index sur l'année de l'album
db.createObjectStore('albums', { keyPath: 'id' })
.createIndex('by_year', 'year');Le premier paramètre est le nom de l’index (by_year), le second est la propriété sur laquelle il portera (year). D’autres options permettent de le rendre unique (unique: true) ou d’indiquer que l’index doit extraire chaque élément d’un tableau présent dans le champ indexé (multiEntry: true). Cela est utile si, par exemple, un champ contient plusieurs tags ou genres musicaux : l’index permettra alors de rechercher un enregistrement à partir de chacun de ces éléments.
// Exemple avec multiEntry sur un champ 'tags' qui contient un tableau de mots-clés
store.createIndex('by_tag', 'tags', { multiEntry: true });Une fois l’index défini, on peut l’utiliser comme un point d’entrée pour rechercher ou parcourir les enregistrements plus efficacement :
const transaction = db.transaction('albums');
const store = transaction.objectStore('albums');
const index = store.index('by_year');
index.getAll(1973).onsuccess = (event) => {
const albums = event.target.result;
// Traiter les albums de l’année 1973
};Les index sont donc essentiels pour structurer l’accès à la donnée, surtout dans les applications réactives ou les interfaces filtrables. Ils rendent les parcours plus rapides et permettent d’écrire un code plus fluide.
Parcourir les données à l’aide de curseurs et d’index
Lorsqu’on souhaite explorer plusieurs enregistrements, openCursor() permet de les parcourir un à un dans l’ordre défini par leur clé ou par un index. Cela s’avère très utile lorsqu’on veut construire une liste à afficher, chercher un ensemble de données correspondant à un critère, ou encore exporter progressivement le contenu d’un store. On peut aussi filtrer cette navigation à l’aide d’un index, ce qui suppose qu’un index ait été préalablement défini dans la base.
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.key, cursor.value);
cursor.continue();
}
};Le curseur parcourt la base dans l’ordre des clés. On peut aussi l’orienter (next, prev) ou le filtrer (via index ou range). Cela reste une lecture, donc sans incidence sur la version ou la structure de la base.
// Exemple d'utilisation d'un index, d'une plage et d'une direction personnalisée
const transaction = db.transaction('albums', 'readonly');
const store = transaction.objectStore('albums');
const index = store.index('year'); // suppose qu’un index a été créé sur l’année
const range = IDBKeyRange.bound(1970, 1975); // on cible les années 1970 à 1975
const request = index.openCursor(range, 'prev'); // parcours en ordre décroissant
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.key, cursor.value.title);
cursor.continue();
}
};Conclusion
IndexedDB ne se limite pas à stocker des données côté client. Avec les transactions, les accès directs et les curseurs, nous disposons d’un éventail d’outils puissants pour organiser, parcourir et transformer nos données localement. Mieux encore, l’introduction des index permet d’optimiser les recherches et de structurer l’information à grande échelle. Nous sommes désormais capables d’exploiter pleinement le potentiel de cette base embarquée, dans un cadre sécurisé et cohérent.
Dans le prochain article, IndexedDB : relations, structures avancées et bonnes pratiques, nous explorerons les cas d’usage plus poussés : relations implicites entre stores, nettoyage, compatibilité, et organisation de données complexes dans des applications modernes.
