Как писать сложные запросы Firebase в Angular и создавать составные индексы

Введение
Firebase предоставляет облачную NoSQL-базу данных Cloud Firestore, которую удобно интегрировать с Angular-приложениями. Firestore отлично подходит для быстрых CRUD-операций, но у него есть важная особенность: при выполнении запросов, использующих комбинацию полей (например, where + orderBy по разным полям), часто требуется составной индекс (composite index). Если индекс отсутствует, браузер вернёт ошибку с ссылкой на создание индекса.
Определения в одну строку
- Cloud Firestore — документно-ориентированная NoSQL-база в Firebase.
- Составной индекс — заранее построенная структура, которая позволяет быстро выполнять запросы по нескольким полям.
Что вы получите из этой статьи
- Пошаговая настройка Angular + Firebase.
- Пример сервиса и компонента для запроса «товар с минимальным запасом у поставщика».
- Инструкция по созданию составного индекса в консоли Firebase и через файл индексов.
- Методика отладки и проверка статуса индекса.
- Альтернативы и рекомендации по архитектуре данных.
Important: перед публикацией на продакшн убедитесь, что правила безопасности Firestore (Firestore Security Rules) не позволяют неавторизованному доступу к данным.
Подготовка: Angular-приложение и база данных Firebase
Перед написанием запросов выполните базовую настройку:
- Если у вас нет Angular-проекта, создайте его командой:
ng new new-angular-appСоздайте проект в Firebase и включите Cloud Firestore.
В Cloud Firestore создайте две коллекции: Product и Supplier. Поставщик может иметь несколько товаров; у товара есть поле supplierId, связывающее его с поставщиком.
Добавьте примеры документов в коллекцию Product. Поля name, productId и supplierId — строки; price и inStock — числа.
| Document ID | Fields |
|---|
| product1 | - name: “Ribbons”
- price: 12.99
- inStock: 10
- productId: “P1”
- supplierId: “S1” | | product2 | - name: “Balloons”
- price: 1.5
- inStock: 2
- productId: “P2”
- supplierId: “S1” | | product3 | - name: “Paper”
- price: 2.99
- inStock: 20
- productId: “P3”
- supplierId: “S1” | | product4 | - name: “Table”
- price: 199
- inStock: 1
- productId: “P4”
- supplierId: “S2” |
Вот пример того, как должна выглядеть таблица Product:
- Добавьте документы в коллекцию Supplier (все поля — строки).
| Document ID | Fields |
|---|
| supplier1 | - name: “Arts and Crafts Supplier”
- location: “California, USA”
- supplierId: “S1” | | supplier2 | - name: “Amazing Tables”
- location: “Sydney, Australia”
- supplierId: “S2” |
Пример записи supplier1:
- Установите angular/fire в проект:
npm i @angular/fireВ Firebase Console откройте Настройки проекта и добавьте приложение (иконка «угловые скобки»).
Firebase даст настройки подключения (firebaseConfig). Сохраните их для окружения.
В файле environments/environment.ts замените содержимое на объект с firebaseConfig (подставьте свои значения):
export const environment = {
production: false,
firebaseConfig: {
apiKey: "AIzaSyBzVyXBhDlvXQEltNkE9xKq-37UBIanDlM",
authDomain: "muo-firebase-queries.firebaseapp.com",
projectId: "muo-firebase-queries",
storageBucket: "muo-firebase-queries.appspot.com",
messagingSenderId: "569911365044",
appId: "1:569911365044:web:9557bfef800caa5cdaf6e1"
}
};(Замените все поля на свои значения из консоли.)
- Импортируйте environment и модули AngularFire в src/app/app.module.ts:
import { environment } from "../environments/environment";
import { AngularFireModule } from '@angular/fire/compat';
import { AngularFirestoreModule } from "@angular/fire/compat/firestore";- Добавьте в imports:
AngularFirestoreModule,
AngularFireModule.initializeApp(environment.firebaseConfig)Теперь приложение готово к выполнению запросов в Firestore.
Как написать сложный запрос в сервисе Angular
Идея: выполнить два запроса — сначала найти поставщика по имени, затем найти продукт у этого поставщика с минимальным значением inStock.
Создайте папку services и файл service.ts (или services.service.ts) в ней.
Пример сервиса с импортами и конструктором:
import { Injectable } from '@angular/core';
import { AngularFirestore } from '@angular/fire/compat/firestore';
@Injectable({
providedIn: 'root'
})
export class Service {
constructor(private db: AngularFirestore) { }
}Логика: Firestore не поддерживает прямой SQL-подобный JOIN, поэтому требуется два запроса: один к Supplier, другой к Product.
Функция getSupplier() — находит поставщика по имени:
getSupplier(name: string) {
return new Promise((resolve)=> {
this.db.collection('Supplier', ref => ref.where('name', '==', name)).valueChanges().subscribe(supplier => resolve(supplier))
})
} - Функция getProductsFromSupplier() — возвращает продукт с наименьшим inStock у поставщика:
getProductsFromSupplier(supplierId: string) {
return new Promise((resolve)=> {
this.db.collection('Product', ref => ref.where('supplierId', '==', supplierId).orderBy('inStock').startAt(0).limit(1)).valueChanges().subscribe(product => resolve(product))
})
} Обратите внимание: сочетание where(‘supplierId’, ‘==’, …) и orderBy(‘inStock’) — это пример запроса, который может требовать составного индекса.
- В src/app/app.component.ts импортируйте сервис:
import { Service } from 'src/app/services/service';- Добавьте сервис в конструктор компонента:
constructor(private service: Service) { }- Добавьте переменную для результата и вызов в ngOnInit:
products: any;
ngOnInit(): void {
this.getProductStock();
}
async getProductStock() {
}- Внутри getProductStock используйте сервисные функции последовательно:
let supplier = await this.service.getSupplier('Arts and Crafts Supplier');
this.products = await this.service.getProductsFromSupplier(supplier[0].supplierId);- Замените содержимое src/app/app.component.html на вывод результатов:
Products with lowest stock from "Arts and Crafts Supplier"
Name: {{item.name}}
Number in stock: {{item.inStock}}
Price: ${{item.price}}
- Запустите приложение:
ng serveОткройте http://localhost:4200
Если данные не отображаются, откройте Инструменты разработчика (Inspect) в браузере.
Вкладка Console покажет ошибку с предложением создать индекс.
Почему возникает ошибка и как её исправить
Firestore требует индекс для эффективного выполнения запросов, комбинирующих фильтры и порядок сортировки по разным полям. Ошибка содержит ссылку, по которой можно перейти в консоль и автоматически создать нужный индекс.
Создание составного индекса через консоль
- Нажмите по ссылке из сообщения об ошибке или откройте Firebase Console → Firestore → Индексы.
- Войдите в аккаунт Firebase, если требуется.
- Нажмите Create index (Создать индекс).
- Подтвердите поля и порядок (обычно supplierId ASC, inStock ASC для примера выше).
- Дождитесь статуса «Enabled» (сборка индекса может занять несколько минут).
- Обновите страницу приложения — запрос выполнится, и ошибка в консоли исчезнет.
Важно: индексы влияют на время записи (write latency) и хранилище. Чем больше индексов — тем больше накладных расходов на запись и хранение.
Как определить, нужен ли составной индекс
Правило простое: если вы комбинируете фильтры и одновременно используете orderBy по полю, которого нет в базовом предикате, Firestore может попросить индекс. Если запрос использует только одно поле (или простые equality/== по единственному полю), индекс обычно не требуется.
Примеры запросов и поведение:
- ref.where(‘supplierId’, ‘==’, id).orderBy(‘inStock’) — часто требует составного индекса.
- ref.where(‘supplierId’, ‘==’, id).where(‘category’, ‘==’, c) — может потребовать индекс, если вы используете оба поля в фильтре и их сочетание не переопределено автоматически.
- ref.orderBy(‘createdAt’).limit(10) — обычно не требует составного индекса.
Альтернативы и архитектурные варианты
Если создание индекса нежелательно (например, слишком много индексов или частые записи), рассмотрите альтернативы:
- Денормализация данных
- Преобразуйте модель: храните у поставщика поле с массивом id товаров или предвычисленный документ с минимальным запасом.
- Плюсы: быстрые чтения, меньше индексов.
- Минусы: сложнее поддерживать целостность, обновления становятся дороже.
- Материализованные представления (precomputed views)
- Отдельная коллекция с агрегированными данными (например, supplierMinStock), обновляемая Cloud Functions при изменении Product.
- Использование Cloud Functions или серверного слоя
- Выполняйте сложную логику на сервере (или в Cloud Functions) и возвращайте готовый результат клиенту.
- Переосмысление UX
- Загружайте больше данных на клиент (например, все продукты поставщика) и выполняйте фильтрацию/сортировку локально, если объём данных мал.
Когда эти подходы не подходят
- Если у вас требуются свежие данные в реальном времени для тысяч поставщиков, материализованные представления и индексы остаются предпочтительными.
Пример файла индексов firestore.indexes.json
Если вы используете CI/CD, можно хранить индексы в репозитории и деплоить их через Firebase CLI. Пример формата:
{
"indexes": [
{
"collectionGroup": "Product",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "supplierId", "order": "ASCENDING" },
{ "fieldPath": "inStock", "order": "ASCENDING" }
]
}
],
"fieldOverrides": []
}Команда деплоя:
firebase deploy --only firestore:indexesНе забудьте настроить Firebase CLI и авторизацию перед деплоем.
Методика отладки индексов (короткая)
- Запустите запрос в приложении.
- Если в консоли браузера есть ошибка — откройте ссылку на создание индекса.
- Создайте индекс в консоли или через firestore.indexes.json.
- Подождите статуса Enabled.
- Обновите страницу и проверьте результат.
- Если ошибка повторяется — проверьте порядок полей и типы данных (строка vs число).
Критерии приёмки
- Приложение успешно выполняет запрос без ошибок в консоли.
- Статус индекса в консоли — Enabled.
- Результат соответствует ожидаемому (товар с минимальным inStock у выбранного поставщика).
Чек-листы (роль-based)
Разработчик (Frontend):
- Подключил @angular/fire и AngularFirestoreModule.
- Правильно настроил environment.firebaseConfig.
- Реализовал сервисы с обработкой ошибок и подписями на valueChanges().
- Проверил типы полей (числа/строки).
- Открыл консоль браузера и проверил сообщения об индексах.
Администратор Firebase:
- Создал составной индекс в консоли или выгрузил firestore.indexes.json в репозиторий.
- Проверил статус индекса до Enabled.
- Оценил влияние индекса на стоимость и время записи.
- Убедился, что правила безопасности Firestore корректны.
Частые ошибки и как их исправлять
- Ошибка: “The query requires an index.” — следовать ссылке в сообщении и создать индекс.
- Ошибка: пустой результат после создания индекса — проверьте, что порядок полей в индексе совпадает с порядком в запросе (orderBy и where combinations).
- Неправильные типы данных (строка vs число) — убедитесь, что в документах типы полей соответствуют ожиданиям.
Безопасность и приватность
- Правила безопасности Firestore должны ограничивать чтение/запись данных только авторизованным пользователям или по бизнес-логике.
- Избегайте публикации API-ключей в публичном репозитории; firebaseConfig содержит apiKey, который по сути идентификатор приложения, но не секретный ключ. Тем не менее, правила безопасности обязаны защищать данные.
Ментальные модели и эвристики
- «Индекс — это карта»: если запрос использует два или более поля, Firestore просит иметь карту (индекс), чтобы не сканировать все документы.
- «Денормализация ради чтения»: в мобильных и web-приложениях часто стоит оптимизировать модель под быстрые чтения, даже если это удорожает записи.
Пример короткого runbook при проблеме с индексом
- Пользователь сообщает, что данные не отображаются.
- Откройте консоль браузера → снимок ошибки.
- Перейдите по ссылке для создания индекса.
- Если индекс уже создан, проверьте его статус в Firebase Console → Firestore → Indices.
- Если индекс «Building» — дождитесь; если «Error» — проверьте поля и попробуйте пересоздать.
- Сообщите пользователю, когда индекс Enabled.
Краткая галерея крайних случаев
- Много индексов + частые записи → рост затрат и замедление записи. Решение: материализованные представления, denormalization, серверные вычисления.
- Непредсказуемая схема данных → используйте строгие соглашения об именах полей и типах, либо схему на сервере.
Сравнение подходов (кратко)
- Денормализация vs индексы: денормализация даёт быстрые чтения, но усложняет обновления; индексы сохраняют нормализованную модель, но увеличивают расходы на запись.
Схема принятия решения (Mermaid)
flowchart TD
A[Нужно получить данные по нескольким полям?] -->|Нет| B[Обычный запрос, без индекса]
A -->|Да| C[Пробуем выполнить запрос]
C --> D{Ошибка 'requires index'?}
D -->|Нет| E[Результат готов]
D -->|Да| F[Создать индекс]
F --> G{Статус}
G -->|Building| H[Подождать]
G -->|Enabled| E
G -->|Error| I[Проверить поля/типы и пересоздать]Глоссарий (1 строка на термин)
- Composite index — индекс по комбинации полей для ускорения сложных запросов.
- Denormalization — хранение дублирующихся данных для оптимизации чтения.
- Cloud Functions — серверная логика в платформе Firebase/Google Cloud.
Заключение
Firestore делает сложные клиентские запросы быстрыми, но иногда требует составных индексов. В большинстве случаев решение простое: создать индекс по указанным полям и дождаться его включения. Если индексов слишком много или запись данных критична, рассмотрите денормализацию, материализованные представления или серверную агрегацию.
Summary: создание индекса через консоль занимает несколько минут, но решает проблему. Продумайте архитектуру данных заранее, чтобы минимизировать количество составных индексов.
Примечание: альтернативные NoSQL-решения (например MongoDB) предлагают отличные паттерны для агрегации и индексации — изучите их, если у вас сложные требования к аналитике.
Похожие материалы
Изменить скорость видео в TikTok — быстро и просто
Как пригласить и привлечь друзей в BeReal
Как удалить аккаунт BeReal — полное руководство
Исправить лаги и плохое качество стримов в Discord
Изменить фото профиля и видео в TikTok