Использование Collection API: and(), or()

В Dexie.js объект Collection представляет собой цепочку операций над набором записей. Большинство методов выборки данных, таких как where(), filter(), orderBy(), reverse(), offset(), limit(), возвращают именно коллекцию, которая затем может быть дополнительно модифицирована.

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

Среди методов расширения условий особое место занимают and() и or(). Они позволяют создавать более сложные критерии отбора, приближая возможности IndexedDB к привычным SQL-запросам.


Метод and()

Назначение

Метод and() добавляет дополнительное условие к уже существующей коллекции.

Синтаксис:

collection.and(predicate)

где:

  • predicate — функция-предикат;
  • функция должна возвращать true для подходящих записей;
  • объекты, для которых возвращается false, исключаются из результата.

Фактически and() работает аналогично дополнительному фильтру после основного индексного поиска.


Принцип работы

Предположим, имеется таблица пользователей:

db.version(1).stores({
    users: '++id, name, age, city, active'
});

Содержимое:

[
    {
        id: 1,
        name: "Анна",
        age: 25,
        city: "Москва",
        active: true
    },
    {
        id: 2,
        name: "Иван",
        age: 30,
        city: "Москва",
        active: false
    },
    {
        id: 3,
        name: "Пётр",
        age: 35,
        city: "Казань",
        active: true
    }
]

Выбор пользователей из Москвы:

const users = await db.users
    .where('city')
    .equals('Москва')
    .toArray();

Результат:

[
    { id: 1, ... },
    { id: 2, ... }
]

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

const users = await db.users
    .where('city')
    .equals('Москва')
    .and(user => user.active)
    .toArray();

Результат:

[
    { id: 1, ... }
]

Сначала выполняется индексный поиск по полю city, затем каждая найденная запись проходит через функцию-предикат.


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

На практике многие замечают сходство между and() и filter().

Пример:

db.users
    .where('city')
    .equals('Москва')
    .and(user => user.age > 18);

и

db.users
    .where('city')
    .equals('Москва')
    .filter(user => user.age > 18);

дают одинаковый результат.

Внутри Dexie.js метод and() фактически является специализированной формой фильтрации коллекции.

Однако с точки зрения читаемости кода существует важное различие.

Вариант с and():

db.users
    .where('city')
    .equals('Москва')
    .and(user => user.active);

логически читается как:

город Москва И активный пользователь.

Вариант с filter():

db.users
    .where('city')
    .equals('Москва')
    .filter(user => user.active);

воспринимается скорее как отдельный этап фильтрации.

Поэтому and() часто используется для описания составных условий.


Несколько вызовов and()

Количество вызовов не ограничено.

Пример:

const users = await db.users
    .where('city')
    .equals('Москва')
    .and(user => user.active)
    .and(user => user.age >= 21)
    .and(user => user.name.startsWith('А'))
    .toArray();

Эквивалентная логика:

user.city === "Москва"
&& user.active
&& user.age >= 21
&& user.name.startsWith("А")

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


Использование сложных выражений

Предикат может содержать произвольную бизнес-логику.

Пример:

const users = await db.users
    .where('city')
    .equals('Москва')
    .and(user => {
        return (
            user.active &&
            user.age >= 18 &&
            user.age <= 35
        );
    })
    .toArray();

Подобный подход удобен, когда часть критериев невозможно выразить индексным поиском.


Проверка вложенных объектов

Метод работает с любыми свойствами объекта.

Например:

{
    id: 1,
    profile: {
        verified: true,
        premium: true
    }
}

Запрос:

const users = await db.users
    .toCollection()
    .and(user => user.profile.verified)
    .toArray();

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


Работа с массивами

Часто требуется проверять наличие элементов внутри массива.

Пример записи:

{
    id: 1,
    name: "Анна",
    roles: ["user", "editor"]
}

Запрос:

const users = await db.users
    .toCollection()
    .and(user => user.roles.includes("editor"))
    .toArray();

В выборку попадут только редакторы.


Метод or()

Назначение

Метод or() позволяет объединять условия через логическое «ИЛИ».

Синтаксис:

collection.or(indexName)

или

where(...).equals(...).or(...)

Метод создаёт новый диапазон поиска по другому индексу.


Простейший пример

Пусть имеется таблица:

db.version(1).stores({
    users: '++id, city, age'
});

Необходимо получить:

  • пользователей из Москвы;
  • пользователей из Казани.

Запрос:

const users = await db.users
    .where('city')
    .equals('Москва')
    .or('city')
    .equals('Казань')
    .toArray();

Результат:

[
    { city: "Москва" },
    { city: "Казань" }
]

Логика соответствует выражению:

city = 'Москва'
OR
city = 'Казань'

Использование разных индексов

Наиболее интересный сценарий — объединение разных индексов.

Например:

const users = await db.users
    .where('city')
    .equals('Москва')
    .or('age')
    .above(60)
    .toArray();

Будут выбраны:

  • все пользователи из Москвы;
  • все пользователи старше 60 лет.

Логика:

city === "Москва"
||
age > 60

Как работает объединение результатов

Dexie.js автоматически объединяет результаты обоих запросов.

Если объект удовлетворяет обоим условиям одновременно, дубликат не появляется.

Пример:

{
    id: 10,
    city: "Москва",
    age: 70
}

Такая запись соответствует обоим критериям:

city === "Москва"
age > 60

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


Цепочки нескольких or()

Возможно объединение большого числа условий.

Пример:

const users = await db.users
    .where('city')
    .equals('Москва')
    .or('city')
    .equals('Казань')
    .or('city')
    .equals('Самара')
    .or('city')
    .equals('Уфа')
    .toArray();

Фактически формируется запрос:

city === "Москва"
||
city === "Казань"
||
city === "Самара"
||
city === "Уфа"

Комбинирование and() и or()

Именно здесь раскрываются основные возможности Collection API.

Пример:

const users = await db.users
    .where('city')
    .equals('Москва')
    .or('city')
    .equals('Казань')
    .and(user => user.active)
    .toArray();

Логика:

(
    city === "Москва"
    ||
    city === "Казань"
)
&&
active === true

В результат попадут только активные пользователи из указанных городов.


Поиск по нескольким критериям

Пример более сложного запроса:

const users = await db.users
    .where('city')
    .equals('Москва')
    .or('city')
    .equals('Казань')
    .and(user => user.age >= 18)
    .and(user => user.age <= 35)
    .and(user => user.active)
    .toArray();

Эквивалентное условие:

(
    city === "Москва"
    ||
    city === "Казань"
)
&&
age >= 18
&&
age <= 35
&&
active

Ограничения метода or()

Требуются индексы

Метод работает через индексированный поиск.

Пример:

db.users
    .where('city')
    .equals('Москва')
    .or('salary')
    .above(100000);

Если индекс salary отсутствует:

users: '++id, city'

Dexie выбросит исключение.

Поэтому каждое поле, используемое в or(), должно быть индексировано.


Не заменяет полноценные логические деревья

Dexie.js позволяет создавать достаточно сложные запросы, однако IndexedDB не предоставляет SQL-подобный механизм построения произвольных логических выражений.

Иногда более удобно получить промежуточный набор данных и затем выполнить дополнительную фильтрацию через and() или filter().


Производительность and()

Важно понимать механизм работы.

Запрос:

db.users
    .where('city')
    .equals('Москва')
    .and(user => user.age > 30);

Выполняется следующим образом:

  1. Индекс находит пользователей из Москвы.
  2. Каждая найденная запись загружается.
  3. Для каждой записи вызывается предикат.

Следовательно, скорость зависит от количества объектов, найденных первым индексным условием.

Хорошая практика заключается в том, чтобы максимально сузить выборку индексом до применения and().


Производительность or()

Метод or() обычно работает эффективнее, чем фильтрация больших массивов вручную.

Например:

db.users
    .where('city')
    .equals('Москва')
    .or('city')
    .equals('Казань');

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

На больших объёмах данных это обеспечивает значительно более высокую производительность по сравнению с конструкциями вида:

db.users
    .toCollection()
    .filter(user =>
        user.city === "Москва" ||
        user.city === "Казань"
    );

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


Типичные сценарии использования

Поиск активных пользователей нескольких городов

const users = await db.users
    .where('city')
    .equals('Москва')
    .or('city')
    .equals('Казань')
    .and(user => user.active)
    .toArray();

Поиск товаров нескольких категорий

const products = await db.products
    .where('category')
    .equals('Ноутбуки')
    .or('category')
    .equals('Планшеты')
    .toArray();

Выбор заказов с дополнительной проверкой

const orders = await db.orders
    .where('status')
    .equals('new')
    .and(order => order.total > 5000)
    .toArray();

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

const users = await db.users
    .where('role')
    .equals('admin')
    .or('city')
    .equals('Москва')
    .and(user => user.active)
    .toArray();

Логика:

(role === "admin" || city === "Москва")
&& active

Методы and() и or() являются важнейшими инструментами Collection API, позволяющими строить составные запросы поверх IndexedDB. and() добавляет дополнительную фильтрацию к уже найденным данным, тогда как or() объединяет результаты нескольких индексных поисков. Их грамотное комбинирование позволяет создавать выразительные и производительные запросы без необходимости выполнять полное сканирование таблиц и сложную ручную обработку результатов.