Метод openCursor() относится к низкоуровневым
возможностям IndexedDB и доступен в Dexie.js через объект коллекции или
таблицы. Он предоставляет прямой доступ к курсору базы данных, позволяя
последовательно обходить записи без предварительной загрузки всего
набора данных в память.
В большинстве прикладных задач Dexie предлагает более удобные методы:
toArray()each()filter()offset()limit()sortBy()Однако существуют ситуации, когда требуется максимальный контроль над процессом чтения данных. Именно тогда используется курсор.
Основная идея курсора заключается в том, что записи извлекаются по одной. После обработки текущей записи курсор перемещается к следующей.
Курсор представляет собой специальный объект-итератор, который последовательно проходит по диапазону записей.
С точки зрения IndexedDB курсор содержит:
В Dexie курсор обычно используется внутри метода
Collection.raw() или через прямое взаимодействие с API
IndexedDB.
Логически работа курсора выглядит следующим образом:
Запись 1
↓
Запись 2
↓
Запись 3
↓
Запись 4
В память загружается только текущая запись, что особенно полезно при обработке больших объемов данных.
Рассмотрим структуру базы данных:
const db = new Dexie('ShopDB');
db.version(1).stores({
products: '++id, category, price'
});
Добавим данные:
await db.products.bulkAdd([
{
category: 'Ноутбуки',
price: 1200
},
{
category: 'Ноутбуки',
price: 1400
},
{
category: 'Мониторы',
price: 350
}
]);
Получить курсор можно через низкоуровневый доступ:
db.products.toCollection().raw().each(cursor => {
console.log(cursor.value);
});
Здесь в обработчик передается не объект записи, а настоящий объект курсора IndexedDB.
Наиболее распространенный способ работы с курсором выглядит так:
db.transaction('r', db.products, async () => {
const table = db.products;
await table
.orderBy('price')
.raw()
.each(cursor => {
console.log(cursor.value);
});
});
Под капотом Dexie открывает курсор и последовательно передает его обработчику.
Иногда требуется получить полный контроль над механизмом курсоров.
Для этого используется внутренний объект IndexedDB:
db.transaction('r', db.products, async () => {
const coreTable = db.products;
await coreTable.each(item => {
console.log(item);
});
});
Однако настоящий вызов openCursor() обычно выполняется
через API IndexedDB:
const transaction = db.backendDB()
.transaction('products', 'readonly');
const store = transaction.objectStore('products');
const request = store.openCursor();
request.onsucc ess = event => {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.value);
cursor.continue();
}
};
В данном примере используется уже не высокоуровневый API Dexie, а базовый API IndexedDB.
После открытия курсора становится доступен объект:
cursor
Он содержит несколько важных свойств.
Текущая запись.
console.log(cursor.value);
Результат:
{
id: 1,
category: 'Ноутбуки',
price: 1200
}
Ключ текущего индекса.
console.log(cursor.key);
Например:
1200
если курсор открыт по индексу price.
Первичный ключ записи.
console.log(cursor.primaryKey);
Результат:
1
Направление обхода.
console.log(cursor.direction);
Возможные значения:
next
nextunique
prev
prevunique
Метод continue() перемещает курсор на следующую
запись.
Пример:
const request = store.openCursor();
request.onsucc ess = event => {
const cursor = event.target.result;
if (!cursor) {
return;
}
console.log(cursor.value);
cursor.continue();
};
Последовательно будут выведены все записи таблицы.
Метод continue(key) позволяет перескочить сразу к
указанному ключу.
Пример:
cursor.continue(100);
Курсор пропустит все записи до ключа 100.
Это удобно при реализации:
Метод advance() пропускает заданное количество
записей.
Пример:
cursor.advance(10);
Курсор перескочит через десять элементов.
Практический пример:
request.onsucc ess = event => {
const cursor = event.target.result;
if (!cursor) {
return;
}
console.log(cursor.value);
cursor.advance(5);
};
Будет обработана каждая шестая запись.
Курсор может работать не со всей таблицей, а только с определенным диапазоном ключей.
Пример диапазона:
const range = IDBKeyRange.bound(
100,
500
);
Открытие курсора:
const request = store.openCursor(range);
Будут получены только записи с ключами от 100 до 500.
Стандартное направление.
store.openCursor(null, 'next');
Результат:
1
2
3
4
5
Обратный обход.
store.openCursor(null, 'prev');
Результат:
5
4
3
2
1
Используется для уникального обхода повторяющихся индексных значений.
Например:
10
10
10
20
20
30
После открытия:
store.openCursor(null, 'nextunique');
получим:
10
20
30
Аналогично, но в обратном направлении.
store.openCursor(null, 'prevunique');
Результат:
30
20
10
Одним из преимуществ курсоров является возможность обновления записи без отдельного запроса.
Пример:
request.onsucc ess = event => {
const cursor = event.target.result;
if (!cursor) {
return;
}
const product = cursor.value;
product.price += 100;
cursor.update(product);
cursor.continue();
};
Все записи будут изменены во время обхода.
Курсор позволяет удалять записи непосредственно в процессе обхода.
Пример:
request.onsucc ess = event => {
const cursor = event.target.result;
if (!cursor) {
return;
}
if (cursor.value.price < 100) {
cursor.delete();
}
cursor.continue();
};
Будут удалены все товары дешевле 100 единиц.
Одно из главных преимуществ openCursor() заключается в
экономии памяти.
Сравнение подходов:
Получение массива:
const products =
await db.products.toArray();
Недостаток:
Все записи загружаются в память сразу.
Курсор:
store.openCursor();
Преимущество:
В память загружается только текущая запись.
При работе с десятками или сотнями тысяч объектов разница становится существенной.
Пример обработки данных блоками:
let counter = 0;
request.onsucc ess = event => {
const cursor = event.target.result;
if (!cursor) {
console.log('Готово');
return;
}
processItem(cursor.value);
counter++;
if (counter % 1000 === 0) {
console.log(
`Обработано ${counter}`
);
}
cursor.continue();
};
Подобный подход широко используется при:
Курсор может открываться не только по первичному ключу, но и по индексу.
Например:
const index =
store.index('category');
const request =
index.openCursor();
Теперь порядок обхода определяется индексом
category.
Получаем записи:
Мониторы
Мониторы
Ноутбуки
Ноутбуки
Планшеты
а не порядком первичных ключей.
Существует облегченная версия курсора:
index.openKeyCursor();
Отличие заключается в том, что значения записей не загружаются.
Доступны только ключи:
request.onsucc ess = event => {
const cursor = event.target.result;
if (!cursor) {
return;
}
console.log(cursor.key);
cursor.continue();
};
Такой подход быстрее и потребляет меньше памяти, когда необходимы исключительно ключи индекса.
Наиболее типичные сценарии:
Во многих прикладных задачах возможностей each(),
toArray(), filter() и других методов Dexie
оказывается достаточно. Однако openCursor() остается
фундаментальным механизмом IndexedDB, на основе которого строятся многие
высокоуровневые операции библиотеки и который предоставляет максимальную
гибкость при последовательной обработке данных.