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

Введение
Firebase Cloud Firestore — это NoSQL база данных в облаке, которая отлично интегрируется с Angular через библиотеку @angular/fire. При выполнении запросов с фильтрами по нескольким полям Firestore может потребовать создание составного индекса (composite index). Индекс ускоряет поиск и позволяет системе сопоставлять комбинации полей в запросе.
В этом материале вы получите:
- Полный пошаговый пример настройки Angular + Firebase.
- Пример сервисного слоя для выполнения двух связанных запросов: сначала поиск поставщика по имени, затем поиск продукта с наименьшим количеством на складе.
- Как создать составной индекс в Firebase и проверить его статус.
- Альтернативные подходы, чеклисты ролей, сценарии отказа и рекомендации по безопасности.
Важно: даны практические решения и советы по отладке; не используются фиктивные метрики.
Основные понятия в одну строку
- Cloud Firestore: NoSQL документная база данных от Firebase.
- Составной индекс: индекс, покрывающий комбинацию нескольких полей для одного запроса.
- AngularFire: официальная библиотека интеграции Angular с Firebase.
Подготовка Angular-приложения и базы данных Firebase
Перед тем как писать запросы, выполните базовую настройку: создайте Angular-проект, проект Firebase, подключите Angular к Firebase и заполните коллекции Product и Supplier.
- Если у вас ещё нет Angular-приложения, создайте новый проект:
ng new new-angular-appСоздайте проект в консоли Firebase и добавьте Cloud Firestore.
В Cloud Firestore создайте две коллекции: Product и Supplier. Связь реализована через поле supplierId в документах Product — поставщик может иметь множество товаров.
Пример данных для коллекции Product (поля name и productId и supplierId — строки; price и inStock — числа):
| Document ID | Fields |
|---|---|
| product1 | - name: “Ribbons”\n- price: 12.99\n- inStock: 10\n- productId: “P1”\n- supplierId: “S1” |
| product2 | - name: “Balloons”\n- price: 1.5\n- inStock: 2\n- productId: “P2”\n- supplierId: “S1” |
| product3 | - name: “Paper”\n- price: 2.99\n- inStock: 20\n- productId: “P3”\n- supplierId: “S1” |
| product4 | - name: “Table”\n- price: 199\n- inStock: 1\n- productId: “P4”\n- supplierId: “S2” |
Вот пример как это выглядит в консоли:
- Пример данных для коллекции Supplier (все поля — строки):
| Document ID | Fields |
|---|---|
| supplier1 | - name: “Arts and Crafts Supplier”\n- location: “California, USA”\n- supplierId: “S1” |
| supplier2 | - name: “Amazing Tables”\n- location: “Sydney, Australia”\n- supplierId: “S2” |
Пример записи:
- Установите AngularFire в приложение:
npm i @angular/fireВойдите в консоль Firebase, откройте Project settings и добавьте Firebase в ваше приложение (кнопка со значком угловых скобок).
Скопируйте конфигурацию Firebase для вашего приложения.
В файл environments/environment.ts вставьте объект конфигурации, заменив значения на свои:
export const environment = {
production: false,
firebaseConfig: {
apiKey: "ВАШ_API_KEY",
authDomain: "ВАШ_PROJECT.firebaseapp.com",
projectId: "ВАШ_PROJECT",
storageBucket: "ВАШ_PROJECT.appspot.com",
messagingSenderId: "ВАШ_SENDER_ID",
appId: "ВАШ_APP_ID"
}
};- В src/app/app.module.ts импортируйте модули AngularFire и AngularFirestore:
import { environment } from "../environments/environment";
import { AngularFireModule } from '@angular/fire/compat';
import { AngularFirestoreModule } from "@angular/fire/compat/firestore";
@NgModule({
imports: [
AngularFirestoreModule,
AngularFireModule.initializeApp(environment.firebaseConfig)
]
})
export class AppModule { }Пример: сложный запрос в сервисе Angular
Архитектурное решение: вынесите логику работы с Firestore в сервис. В нашем примере сначала получаем поставщика по имени, затем по supplierId — продукт с минимальным значением inStock.
Создайте папку services и файл service.ts в ней.
service.ts:
import { Injectable } from '@angular/core';
import { AngularFirestore } from '@angular/fire/compat/firestore';
@Injectable({
providedIn: 'root'
})
export class Service {
constructor(private db: AngularFirestore) { }
getSupplier(name: string) {
return new Promise((resolve) => {
this.db.collection('Supplier', ref => ref.where('name', '==', name)).valueChanges().subscribe(supplier => resolve(supplier));
});
}
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));
});
}
} Пояснения по коду:
- getSupplier возвращает массив совпавших документов (даже если это один объект) — используйте supplier[0] для первого результата.
- getProductsFromSupplier сортирует по inStock и ограничивает результат одним документом с наименьшим inStock.
- В реальном приложении лучше отписываться от подписок и обрабатывать ошибки.
Интеграция в компонент
В src/app/app.component.ts импортируйте сервис и используйте асинхронную логику:
import { Component, OnInit } from '@angular/core';
import { Service } from 'src/app/services/service';
@Component({ selector: 'app-root', templateUrl: './app.component.html' })
export class AppComponent implements OnInit {
products: any;
constructor(private service: Service) { }
ngOnInit(): void {
this.getProductStock();
}
async getProductStock() {
try {
let supplier = await this.service.getSupplier('Arts and Crafts Supplier');
if (!supplier || !supplier.length) {
console.warn('Поставщик не найден');
this.products = [];
return;
}
this.products = await this.service.getProductsFromSupplier(supplier[0].supplierId);
} catch (err) {
console.error('Ошибка при загрузке данных', err);
this.products = [];
}
}
}Шаблон компонента (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.
В консоли может появиться ошибка с приглашением создать индекс.
Создание составного индекса
Когда Firestore требует индекс, он генерирует ссылку в сообщении об ошибке, ведущую прямо на страницу создания индекса.
- Нажмите на ссылку в сообщении об ошибке.
- Войдите в аккаунт Firebase, если требуется.
- Подтвердите создание индекса, нажав Create index.
- Ожидайте изменения статуса индекса с Building на Enabled. Время сборки зависит от размера коллекций.
- После того как индекс станет Enabled, обновите страницу приложения — запрос выполнится корректно.
Важно: индекс создаётся на уровне проекта Firebase и влияет на все приложения, использующие этот проект.
Когда составной индекс не нужен или не подходит
- Запрос использует только одно поле и порядок по нему — для таких случаев чаще всего хватает автоматических индексов Firestore.
- Частые и сложные агрегации лучше выносить в фоновые задачи (Cloud Functions) и хранить результаты в отдельной коллекции.
- Если вы делаете много фильтров и соединений, рассмотрите денормализацию данных — добавление полей или документов, чтобы сократить количество соединений.
Альтернативные подходы
- Денормализация данных: дублируйте необходимые значения (например, количество на складе) в документе поставщика, если он содержит небольшую часть информации.
- Cloud Functions: выполняйте сложную агрегацию на сервере по расписанию и сохраняйте результат в отдельной коллекции.
- Realtime Database: если нужна очень простая модель и низкая задержка, но без сложных запросов — рассмотреть Realtime Database.
- Внешняя аналитика: экспорт данных в BigQuery для сложной аналитики.
Модель мышления (mental model)
Представьте индекс как телефонную книгу: вместо поиска по всему списку документов, Firestore использует индекс, чтобы быстро найти документы по комбинации полей, указанной в запросе. Без индекса система вынуждена выполнять более дорогой обход, поэтому Firestore требует создать индекс заранее.
Процесс создания индекса — мини-методология
- Выполнить запрос из приложения.
- Получить сообщение об ошибке с ссылкой на создание индекса.
- Перейти по ссылке и подтвердить создание индекса.
- Подождать пока статус станет Enabled.
- Обновить приложение и проверить результат.
Decision tree для реагирования на ошибку индекса
flowchart TD
A[Пользователь запускает запрос] --> B{Появилась ошибка в консоли}
B -- Нет --> C[Запрос выполнен успешно]
B -- Да --> D[Перейти по ссылке из ошибки]
D --> E{Создать индекс?}
E -- Да --> F[Нажать Create index и ждать Enabled]
E -- Нет --> G[Рассмотреть альтернативу: денормализация/Cloud Function]
F --> H[Обновить страницу]
H --> I[Проверить результат]Чеклист по ролям
Разработчик:
- Развернуть Angular и подключить AngularFire.
- Вынести логику запросов в сервис.
- Обрабатывать отсутствие данных и ошибки.
DevOps / Администратор Firebase:
- Создать проект Firebase и включить Firestore.
- Проверить права доступа правил безопасности Firestore.
- Отслеживать статус индексов в консоли Firebase.
Безопасность / Privacy:
- Проверить правила безопасности Firestore, чтобы пользователи видели только разрешённые данные.
- Минимизировать хранение личных данных.
Playbook: добавить составной индекс — пошагово
- Выполнить запрос в приложении и просмотреть ошибку в консоли браузера.
- Перейти по ссылке «Create index».
- Подтвердить параметры индекса и создать.
- Мониторить процесс сборки индекса (может занять минуты).
- После статуса Enabled — проверить приложение.
- При повторяющихся проблемах проверить порядок полей в запросе и индексах.
Критерии приёмки
- Индекс создан и имеет статус Enabled.
- Запрос возвращает ожидаемые данные на странице приложения.
- В консоли браузера нет ошибок связанных с индексами.
- Правила безопасности Firestore позволяют доступ к запрашиваемым документам.
Обработка ошибок и план отката
- Если индекс строится слишком долго: проверить объём коллекции, временно использовать тестовую выборку или выполнить агрегацию через Cloud Function.
- Если новый индекс нарушил производительность других запросов: удалить индекс или скорректировать его поля.
- Откат изменений конфигурации — восстановить предыдущее состояние rules или удалить добавленный индекс через консоль.
Безопасность и приватность
- Ограничьте чтение коллекций правилами Firestore: разрешать чтение только аутентифицированным пользователям или по условию.
- Не храните в открытых документах чувствительные личные данные.
- Для соответствия локальным требованиям приватности (например, GDPR) минимизируйте объем сохраняемых персональных данных и документируйте процесс обработки данных.
Совместимость и миграция
- angular/fire имеет версии compat и modular; в примерах используется compat API. При переходе на модульную версию API потребует другой синтаксис импорта и инициализации.
- При обновлении Angular и angular/fire проверьте changelog и миграционные инструкции.
Частые ошибки и как их исправлять
- Ошибка индекса — перейти по ссылке в консоли и создать индекс.
- Пустой результат из getSupplier — проверяйте точность имени и регистр символов.
- Подписка без отписки — используйте take(1) или unsubscribe, чтобы избежать утечек памяти.
Локальные рекомендации для разработчиков
- Разработчикам, работающим из локальной сети, стоит убедиться, что доступ к Firebase не ограничён прокси/брандмауэром.
- Для тестирования больших наборов данных используйте эмулятор Firebase (Firestore emulator) — он позволяет быстро тестировать индексирование и запросы локально.
Заключение
Составные индексы — обычная часть работы с Cloud Firestore при выполнении запросов по комбинации полей. Используйте автоматически сгенерированную ссылку из ошибки, чтобы быстро создать индекс, следите за статусом индекса и учитывайте альтернативные подходы (денормализация, Cloud Functions) для случаев с высокой нагрузкой или сложной агрегацией.
Краткие рекомендации:
- Держите запросы простыми и предсказуемыми.
- Создавайте сервисы для работы с Firestore и обрабатывайте ошибки и отсутствующие данные.
- Планируйте создание индексов вместе с дизайном схемы данных.
Конец руководства.
Похожие материалы
Twitter-бот на Raspberry Pi: твиты с температурой и фото
Навигация и выделение текста на iPhone и iPad
Неограниченная история Bash — настройка и советы
Исправить ошибку vcruntime140_app.dll в Windows
Как превратить монитор в телевизор