Запросы по нескольким значениям: anyOf(), noneOf()

Работа с IndexedDB в Dexie.js часто требует фильтрации записей не по одному значению, а сразу по набору допустимых или исключаемых значений. Для этого используются методы anyOf() и noneOf(), позволяющие строить эффективные индексированные запросы без перехода к полному перебору коллекции.

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


Логика работы anyOf()

Метод anyOf() возвращает записи, у которых значение индексируемого поля совпадает хотя бы с одним элементом из переданного набора.

Базовый синтаксис

db.table.where('status').anyOf(['active', 'pending'])

В результате будут выбраны все записи, где поле status имеет значение:

  • active
  • pending

Простейший пример использования anyOf()

const db = new Dexie("app");

db.version(1).stores({
  users: "++id, name, role"
});

await db.users.bulkAdd([
  { name: "Alex", role: "admin" },
  { name: "John", role: "editor" },
  { name: "Kate", role: "viewer" },
  { name: "Mike", role: "editor" }
]);

const result = await db.users
  .where("role")
  .anyOf(["admin", "editor"])
  .toArray();

Результат содержит всех пользователей с ролями admin и editor.


anyOf() с числовыми значениями

Метод одинаково работает как со строками, так и с числами.

db.orders.where("statusCode")
  .anyOf([1, 3, 5])
  .toArray();

Где:

  • 1 — создан
  • 3 — в обработке
  • 5 — завершён

Поведение при пустом массиве

Особенность:

anyOf([])

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


Оптимизация через индексы

Ключевое условие эффективности anyOf() — наличие индекса:

stores({
  users: "++id, name, role"
})

Запрос:

db.users.where("role").anyOf(["admin", "editor"])

использует индекс role, избегая полного сканирования таблицы.


noneOf(): исключение набора значений

Метод noneOf() выполняет обратную операцию: исключает записи, содержащие указанные значения.

Синтаксис

db.table.where('status').noneOf(['deleted', 'blocked'])

Пример использования noneOf()

const activeUsers = await db.users
  .where("role")
  .noneOf(["banned", "deleted"])
  .toArray();

Результат содержит всех пользователей, кроме заблокированных и удалённых.


noneOf() с числовыми данными

db.orders.where("priority")
  .noneOf([0, 9])
  .toArray();

Исключаются заказы с минимальным и максимальным приоритетом.


Сравнение anyOf() и noneOf()

Метод Поведение
anyOf() включает только совпадения
noneOf() исключает указанные значения

Использование с диапазонами (совместно с where())

Методы могут комбинироваться с диапазонными запросами:

db.products
  .where("price")
  .above(100)
  .anyOf([150, 200, 300])

Однако важно учитывать: anyOf() применяется к уже ограниченной выборке.


Множественные значения и строки

При работе со строками учитывается точное совпадение:

db.tasks.where("tag")
  .anyOf(["frontend", "backend"])

Частичные совпадения не учитываются:

  • "frontend-react" не совпадёт с "frontend"

Для таких случаев используется startsWith() или filter().


Ограничения noneOf()

Метод имеет важное ограничение:

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

При слишком длинных массивах исключений возможна деградация производительности.


Комбинирование с compound-index

При составных индексах:

db.version(1).stores({
  logs: "++id, [level+source]"
});

Запрос:

db.logs
  .where("[level+source]")
  .anyOf([
    ["error", "api"],
    ["warn", "db"]
  ])

Каждый элемент массива соответствует полному ключу составного индекса.


Работа с объектными значениями индекса

Если индекс хранит сложные значения (например, массивы или структуры), anyOf() и noneOf() работают только с тем, что реально индексируется.

db.items.where("categoryIds")
  .anyOf([1, 2, 3])

Если categoryIds — массив, требуется предварительное нормализованное индексирование, иначе поведение будет ограничено.


Внутренний механизм выполнения

При использовании:

where("field").anyOf([...])

Dexie.js:

  1. Обращается к IndexedDB index cursor
  2. Выполняет несколько range-сканирований по ключам
  3. Объединяет результаты в одну коллекцию

Для noneOf() происходит аналогичный процесс, но с фильтрацией исключений на уровне курсора.


Отличие от filter()

db.users.filter(u => u.role === "admin" || u.role === "editor")

и

db.users.where("role").anyOf(["admin", "editor"])

разница принципиальная:

Способ Индекс используется Производительность
anyOf() да высокая
filter() нет низкая (скан)

Поведение с undefined и null

anyOf([null, undefined])

работает только если такие значения реально индексируются и присутствуют в данных. В IndexedDB undefined обычно не хранится как индексируемое значение, что ограничивает использование.


Масштабирование запросов

При больших наборах значений:

anyOf([1,2,3,4,5,...1000])

Dexie формирует серию индексных диапазонов. При экстремальных размерах массива возможны:

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

В таких случаях используется разбиение запроса на батчи.


Пример батчевой обработки anyOf()

async function batchAnyOf(values, size = 100) {
  const result = [];

  for (let i = 0; i < values.length; i += size) {
    const chunk = values.slice(i, i + size);

    const part = await db.users
      .where("role")
      .anyOf(chunk)
      .toArray();

    result.push(...part);
  }

  return result;
}

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

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

Поведение при отсутствии индекса

При попытке:

db.table.where("nonIndexedField").anyOf([...])

Dexie может:

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

Оптимальная модель — всегда индексировать поля, используемые в anyOf/noneOf.


Особенности логики совпадений

  • сравнение строгое (===)
  • порядок значений в массиве не влияет
  • дубликаты в массиве игнорируются на уровне логики результата
anyOf(["admin", "admin", "editor"])

эквивалентно:

anyOf(["admin", "editor"])

Совместимость с chain-запросами

db.users
  .where("role")
  .anyOf(["admin", "editor"])
  .filter(u => u.active)
  .limit(10)

anyOf() выполняется на уровне индекса, после чего остальные операции применяются к уже отфильтрованному набору.