Запросы на основе курсора через openCursor()

Метод openCursor() относится к низкоуровневым возможностям IndexedDB и доступен в Dexie.js через объект коллекции или таблицы. Он предоставляет прямой доступ к курсору базы данных, позволяя последовательно обходить записи без предварительной загрузки всего набора данных в память.

В большинстве прикладных задач Dexie предлагает более удобные методы:

  • toArray()
  • each()
  • filter()
  • offset()
  • limit()
  • sortBy()

Однако существуют ситуации, когда требуется максимальный контроль над процессом чтения данных. Именно тогда используется курсор.

Основная идея курсора заключается в том, что записи извлекаются по одной. После обработки текущей записи курсор перемещается к следующей.


Что такое курсор в IndexedDB

Курсор представляет собой специальный объект-итератор, который последовательно проходит по диапазону записей.

С точки зрения IndexedDB курсор содержит:

  • текущий ключ;
  • первичный ключ;
  • значение записи;
  • методы перехода к следующей записи;
  • методы изменения или удаления текущей записи.

В Dexie курсор обычно используется внутри метода Collection.raw() или через прямое взаимодействие с API IndexedDB.

Логически работа курсора выглядит следующим образом:

Запись 1
   ↓
Запись 2
   ↓
Запись 3
   ↓
Запись 4

В память загружается только текущая запись, что особенно полезно при обработке больших объемов данных.


Получение доступа к native-курcору

Рассмотрим структуру базы данных:

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.


Использование openCursor() внутри транзакции

Наиболее распространенный способ работы с курсором выглядит так:

db.transaction('r', db.products, async () => {

    const table = db.products;

    await table
        .orderBy('price')
        .raw()
        .each(cursor => {

            console.log(cursor.value);

        });

});

Под капотом Dexie открывает курсор и последовательно передает его обработчику.


Прямой вызов openCursor()

Иногда требуется получить полный контроль над механизмом курсоров.

Для этого используется внутренний объект 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

Он содержит несколько важных свойств.

value

Текущая запись.

console.log(cursor.value);

Результат:

{
    id: 1,
    category: 'Ноутбуки',
    price: 1200
}

key

Ключ текущего индекса.

console.log(cursor.key);

Например:

1200

если курсор открыт по индексу price.


primaryKey

Первичный ключ записи.

console.log(cursor.primaryKey);

Результат:

1

direction

Направление обхода.

console.log(cursor.direction);

Возможные значения:

next
nextunique
prev
prevunique

Переход к следующей записи через continue()

Метод 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()

Метод advance() пропускает заданное количество записей.

Пример:

cursor.advance(10);

Курсор перескочит через десять элементов.

Практический пример:

request.onsucc ess = event => {

    const cursor = event.target.result;

    if (!cursor) {
        return;
    }

    console.log(cursor.value);

    cursor.advance(5);
};

Будет обработана каждая шестая запись.


Использование диапазонов с openCursor()

Курсор может работать не со всей таблицей, а только с определенным диапазоном ключей.

Пример диапазона:

const range = IDBKeyRange.bound(
    100,
    500
);

Открытие курсора:

const request = store.openCursor(range);

Будут получены только записи с ключами от 100 до 500.


Направления обхода

next

Стандартное направление.

store.openCursor(null, 'next');

Результат:

1
2
3
4
5

prev

Обратный обход.

store.openCursor(null, 'prev');

Результат:

5
4
3
2
1

nextunique

Используется для уникального обхода повторяющихся индексных значений.

Например:

10
10
10
20
20
30

После открытия:

store.openCursor(null, 'nextunique');

получим:

10
20
30

prevunique

Аналогично, но в обратном направлении.

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.

Получаем записи:

Мониторы
Мониторы
Ноутбуки
Ноутбуки
Планшеты

а не порядком первичных ключей.


Использование openKeyCursor()

Существует облегченная версия курсора:

index.openKeyCursor();

Отличие заключается в том, что значения записей не загружаются.

Доступны только ключи:

request.onsucc ess = event => {

    const cursor = event.target.result;

    if (!cursor) {
        return;
    }

    console.log(cursor.key);

    cursor.continue();
};

Такой подход быстрее и потребляет меньше памяти, когда необходимы исключительно ключи индекса.


Когда использование openCursor() оправдано

Наиболее типичные сценарии:

  • обработка очень больших таблиц;
  • массовое обновление записей;
  • массовое удаление данных;
  • реализация собственных алгоритмов пагинации;
  • обход диапазонов ключей;
  • получение полного контроля над порядком чтения;
  • работа напрямую с IndexedDB API;
  • оптимизация производительности при больших объемах данных.

Во многих прикладных задачах возможностей each(), toArray(), filter() и других методов Dexie оказывается достаточно. Однако openCursor() остается фундаментальным механизмом IndexedDB, на основе которого строятся многие высокоуровневые операции библиотеки и который предоставляет максимальную гибкость при последовательной обработке данных.