Метод filter() для произвольных условий

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

Ключевая особенность:

filter() не использует индексы IndexedDB, поэтому всегда следует после первичного извлечения данных.


Место filter() в цепочке запросов

Dexie.js строит запросы как цепочку операций над объектом Collection. Типичный порядок выглядит так:

  1. выбор индекса или всей таблицы (where(), toCollection())
  2. сужение диапазона (например, between, above, below)
  3. постфильтрация (filter())
  4. преобразования (map(), each())
  5. финализация (toArray(), first(), count())

Пример базовой цепочки:

db.users
  .where('age')
  .above(18)
  .filter(user => user.isActive && user.country === 'KZ')
  .toArray();

Принцип работы filter()

Метод принимает синхронную функцию-предикат:

collection.filter((item, index, array) => boolean)

Параметры:

  • item — текущий объект записи
  • index — позиция в результирующей коллекции
  • array — массив (виртуальный, не всегда материализован полностью)

Возвращаемое значение:

  • true — элемент сохраняется в результате
  • false — элемент исключается

Важное отличие от where()

filter() часто используется неправильно как замена индексным запросам, что приводит к деградации производительности.

where():

  • использует индекс
  • выполняется на уровне IndexedDB
  • эффективно работает с большими данными

filter():

  • выполняется в памяти (JavaScript runtime)
  • требует предварительной загрузки данных
  • не масштабируется на большие коллекции

Пример неправильного подхода:

db.orders
  .toCollection()
  .filter(order => order.total > 1000);

Этот код загружает ВСЕ записи таблицы перед фильтрацией.


Оптимальная стратегия использования

filter() применяется только после максимального сужения выборки через индексы:

db.orders
  .where('status')
  .equals('paid')
  .filter(order => order.total > 1000 && order.items.length > 3)
  .toArray();

Здесь:

  • where('status') уменьшает объём данных
  • filter() выполняет сложную бизнес-логику

Производительность и стоимость операций

Каждый вызов filter():

  • проходит по каждому элементу коллекции
  • выполняет JS-функцию синхронно
  • увеличивает время выполнения пропорционально размеру данных

Сложность:

O(n) по количеству элементов после индексации

Важно учитывать:

  • отсутствие lazy-индексации
  • невозможность раннего прерывания без специальных методов (until, limit)
  • дополнительную нагрузку на память при больших выборках

Композиция с другими методами

filter() часто используется в цепочках с преобразованиями данных.

С map():

db.products
  .where('inStock')
  .equals(1)
  .filter(p => p.price < 5000)
  .map(p => ({
    id: p.id,
    label: p.name
  }))
  .toArray();

С sortBy():

db.users
  .toCollection()
  .filter(u => u.age >= 21)
  .sortBy('lastLogin');

Важно учитывать: сортировка после filter() происходит уже в памяти.


Поведение с большими выборками

При обработке больших таблиц использование filter() может привести к:

  • резкому росту потребления памяти
  • блокировке event loop
  • задержкам UI в браузере

Типичный анти-паттерн:

db.logs
  .toCollection()
  .filter(log => log.message.includes('error'))
  .toArray();

Если таблица содержит миллионы записей, весь набор будет загружен в память.


Асинхронные ограничения

filter() принимает только синхронную функцию. Это принципиальное ограничение Dexie.js.

Нельзя:

.filter(async item => await check(item)) // не поддерживается логически

Причина:

  • IndexedDB pipeline не рассчитан на асинхронную фильтрацию
  • цепочка должна оставаться синхронной до финального Promise

Сочетание с until()

Для раннего завершения обработки иногда используется until(), но он работает иначе:

db.items
  .orderBy('created')
  .filter(i => i.active)
  .until(i => i.archived === true);

Здесь until() может сократить количество обрабатываемых элементов, но не заменяет filter().


Типовые сценарии применения

Сложные бизнес-условия

db.payments
  .where('status')
  .equals('completed')
  .filter(p =>
    p.amount > 100 &&
    p.currency === 'USD' &&
    p.refunded === false
  )
  .toArray();

Множественные поля без составного индекса

db.sessions
  .toCollection()
  .filter(s =>
    s.device === 'mobile' &&
    s.duration > 300 &&
    s.country === 'KZ'
  );

Когда нет составного индекса, filter() становится единственным вариантом.


filter() и составные индексы

Во многих случаях filter() используется там, где правильнее создать compound index.

Плохой вариант:

db.users
  .toCollection()
  .filter(u => u.country === 'KZ' && u.age > 30);

Лучше:

db.users.where('[country+age]').between(['KZ', 30], ['KZ', 99]);

Вывод: filter() компенсирует отсутствие индексов, но не заменяет их.


Поведение при цепочках reverse() и offset()

Порядок операций влияет на результат:

db.orders
  .where('status')
  .equals('paid')
  .offset(10)
  .filter(o => o.total > 100)
  .reverse();

Здесь:

  • offset() применяется до filter()
  • reverse() применяется после

Это важно, потому что filter() не изменяет порядок индексации, а лишь отсекает элементы.


Особенности реализации внутри Dexie.js

Внутренне filter():

  • создаёт обёртку над Collection
  • последовательно итерирует курсор IndexedDB
  • применяет predicate к каждому элементу
  • формирует новый поток значений

Это означает:

  • нет предварительного построения массива (до финализации)
  • нет SQL-подобной оптимизации условий
  • нет параллелизма выполнения

Ограничения и потенциальные ошибки

Основные проблемы использования:

  • попытка заменить индексные запросы
  • тяжёлые вычисления внутри predicate
  • использование побочных эффектов в filter()
  • зависимость от внешнего состояния

Антипаттерн:

let threshold = 100;

db.items
  .toCollection()
  .filter(i => i.value > threshold++);

Такой код нарушает детерминированность запроса.


Практическая модель принятия решений

Использование filter() оправдано, когда:

  • условие невозможно выразить индексом
  • выборка уже ограничена индексным запросом
  • требуется сложная логика на уровне объекта
  • объём данных после индексации относительно мал

Не оправдано:

  • фильтрация всей таблицы
  • поиск по одному полю без индекса
  • высоконагруженные операции в больших коллекциях