Научитесь управлять данными вашего приложения Firebase в Python

Firestore, предоставляемая Firebase и Google Cloud, является популярной облачной базой данных NoSQL для мобильных и веб-приложений. Как и MongoDB, Firestores хранят данные в документах, содержащих поля, сопоставленные со значениями. Документы организованы в коллекции, соответствующие таблицам в реляционных базах данных.

Чтобы управлять данными Firestore с помощью Python, нам нужно использовать Firebase Admin SDK, который представляет собой набор библиотек, позволяющих взаимодействовать с Firebase из привилегированных сред. В этом посте мы расскажем, как управлять данными в Firestore с помощью Admin SDK, на нескольких простых примерах, которые охватывают распространенные операции CRUD.

Создайте проект Firebase

Прежде чем мы сможем работать с Firestore в Python, нам нужно иметь активный проект Firebase. Если у вас его еще нет, вы можете сначала проверить этот пост, чтобы быстро начать работу с Firebase.

Установите Firebase Admin SDK

Чтобы работать с Firestore на Python, нам нужно сначала установить Firebase Admin SDK, который можно установить в вашей виртуальной среде. Вы можете выбрать свой любимый инструмент для создания виртуальных сред и управления ими. Здесь используется Conda, потому что мы можем установить определенную версию Python в виртуальной среде, что может быть удобно, если версия Python в вашей системе устарела, и вы не хотите или не можете ее обновить.

# You need to specify a channel if you need to install the latest version of Python.
$ conda create --name firebase python=3.11 -c conda-forge
$ conda activate firebase

$ pip install --upgrade firebase-admin ipython

iPython установлен для более удобного интерактивного запуска кода Python.

Инициализируйте Firebase Admin SDK в GCP

Если ваш код Python работает в облачной среде Google, такой как Compute Engine, App Engine, облачные функции и т. д., вы можете инициализировать Firebase без параметров, поскольку поиск учетных данных выполняется автоматически:

import firebase_admin
from firebase_admin import firestore

app = firebase_admin.initialize_app()
firestore_client = firestore.client()

Инициализировать Firebase Admin SDK в среде, отличной от GCP.

Если ваш код Python запускается в среде, отличной от GCP, вам потребуется аутентифицировать Firebase с помощью файла закрытого ключа вашей служебной учетной записи Firebase. Эта учетная запись службы создается автоматически при создании проекта Firebase.

Чтобы сгенерировать файл закрытого ключа для своего сервисного аккаунта, перейдите в Консоль Firebase и следуйте следующим инструкциям:

После создания файла закрытого ключа его можно использовать для аутентификации Firebase. Вы можете использовать его с учетными данными приложения по умолчанию (ADC), что означает установку переменной среды GOOGLE_APPLICATION_CREDENTIALS на путь к файлу JSON, который содержит закрытый ключ вашей учетной записи службы. Таким образом, учетные данные приложения по умолчанию (ADC) могут неявно определять учетные данные Firebase. Этот способ более безопасен и рекомендуется, если он применим.

$ export GOOGLE_APPLICATION_CREDENTIALS="/home/lynn/Downloads/service-account-file.json"

Затем вы можете инициализировать Firebase SDK следующим образом:

import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore

# Use the application default credentials.
cred = credentials.ApplicationDefault()
firebase_admin.initialize_app(cred)
firestore_client = firestore.client()

Однако установка переменной среды GOOGLE_APPLICATION_CREDENTIALS неприменима, если у вас есть несколько проектов Firebase или ваш проект Firebase не принадлежит вашему проекту Google Cloud по умолчанию. В этих случаях нам необходимо пройти аутентификацию напрямую с помощью файла закрытого ключа:

import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore

# Use the private key file of the service account directly.
cred = credentials.Certificate("/home/lynn/Downloads/service-account-file.json")
app = firebase_admin.initialize_app(cred)
firestore_client = firestore.client()

Вы можете инициализировать Firebase Admin SDK любым из трех способов, показанных выше. Если вы работаете локально на своем ноутбуке, то, скорее всего, вам подойдет третий.

Теперь, когда Firebase Admin SDK аутентифицирован и инициализирован, мы можем начать создавать с его помощью коллекции и документы. Мы рассмотрим распространенные операции Create, Read, Update и Delete (CRUD) с помощью простых фрагменты кода.

Создание документов

Подобно MongoDB, Cloud Firestore не имеет схемы и имеет динамическое сопоставление. Он неявно создает коллекции и документы при первом добавлении данных в документ. Поэтому нам не нужно явно создавать коллекции или документы и указывать сопоставления (а именно определения типов полей). Мы можем создать документ напрямую и присвоить ему данные:

doc_ref = firestore_client.collection("laptops").document("1")
doc_ref.set(
    {
        "name": "HP EliteBook Model 1",
        "brand": "HP",
    }
)

Обратите внимание, что идентификатор документа («1») должен быть уникальным и представлять собой строку.

Ссылка — это объект, указывающий на расположение целевой коллекции или документа в базе данных Firestore. Целевая коллекция или документ не должны существовать при создании ссылки на них. Мы можем добавить данные со ссылкой после ее создания. Все действия CRUD в Firestore выполняются со ссылками, как мы увидим позже.

После запуска этого фрагмента кода создается коллекция и документ, которые можно проверить в консоли Firebase:

Поскольку документы в Firestore не имеют схемы, то есть для документов нет предопределенных полей, мы можем иметь разные поля в каждом документе. Теперь давайте добавим новый документ ноутбука с некоторыми дополнительными полями:

doc_ref = firestore_client.collection("laptops").document("2")
doc_ref.set(
    {
        "name": "Lenovo IdeaPad Model 2",
        "brand": "Lenovo",
        "tags": ["Popular", "Latest"],
        "order": {"price": 9405.0, "quantity": 2},
    }
)

Для второго документа добавляются два новых поля, а именно поле массива и поле карты, которое является вложенным объектом (или словарем в Python). Вот как они отображаются в консоли Firebase:

В приведенных выше примерах мы указали идентификаторы документов, которые представляют собой уникальные строки. Однако, если в документах нет поля с уникальными значениями, мы можем опустить идентификатор документа и позволить Firestore назначить нам автоматически сгенерированный идентификатор с помощью метода add():

coll_ref = firestore_client.collection("laptops")
create_time, doc_ref = coll_ref.add(
    {
        "name": "Apple macbook air",
        "brand": "Apple",
    }
)

print(f"{doc_ref.id} is created at {create_time}")
# CnidNv3f6ZQD9K7MnLyy is created at 2022-11-13 09:55:23.989902+00:00

Создайте документ с подколлекцией

Подколлекция — это коллекция, связанная с конкретным документом. В этом примере мы создадим подколлекцию, содержащую атрибуты ноутбука.

laptop_ref = firestore_client.collection("laptops").document("4")
laptop_ref.set(
    {
        "name": "Apple Macbook Pro",
        "brand": "Apple",
    }
)

# Specify the subcollection for a laptop document.
attr_coll = laptop_ref.collection("attributes")

# Add documents to the subcollection.
attr_ref = attr_coll.document("storage")
attr_ref.set({"name": "Storage", "value": "1", "unit": "TB"})

# We don't need to create the doc ref beforehand if the metadata is not needed.
attr_coll.document("ram").set({"name": "ram", "value": "16", "unit": "GB"})

Обратите внимание, как подколлекция отображается в консоли Firebase:

Использование вложенных коллекций имеет много ограничений. Главный из них, который мне кажется глючным, заключается в том, что подколлекция не удаляется при удалении родительского документа. Однако это не может быть хорошим примером использования вложенных коллекций. Лучшим вариантом является пример чата, приведенный в официальной документации, где сообщения в каждой подколлекции являются независимыми и эквивалентными объектами, что имеет смысл оставить при удалении родительского документа.

В этом простом примере подколлекцию атрибутов можно заменить массивом карт:

laptop_ref = firestore_client.collection("laptops").document("5")
laptop_ref.set(
    {
        "name": "Apple Macbook Pro",
        "brand": "Apple",
        "attributes": [
            {"name": "Storage", "value": "1", "unit": "TB"},
            {"name": "ram", "value": "16", "unit": "GB"},
        ],
    }
)

Firestore похож на наводку для разработчиков интерфейса. Если вам нужны более продвинутые функции для вложенных документов, вы можете попробовать более специализированные базы данных на стороне сервера, такие как MongoDB или Elasticsearch.

Читать документы

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

Во-первых, давайте прочитаем один документ по его идентификатору.

doc_ref = firestore_client.collection('laptops').document("1")

# We can read the id directly:
print(f"The document id is {doc_ref.id}")
# The document id is 1

# We need to use .get() to get a snapshot of the document:
doc = doc_ref.get()
print(f"The document is {doc.to_dict()}")
# The document is {'brand': 'HP', 'name': 'HP EliteBook Model 1'}

Обратите внимание, что нам нужно вызвать метод .get() ссылки на документ, чтобы получить снимок данных документа.

Теперь давайте прочитаем все документы в коллекции:

coll_ref = firestore_client.collection('laptops')

# Using coll_ref.stream() is more efficient than coll_ref.get()
docs = coll_ref.stream()
for doc in docs:
    print(f'{doc.id} => {doc.to_dict()}')

Обратите внимание, что coll_ref.stream() возвращает генератор DocumentSnapshot, а coll_ref.get() возвращает их список. Следовательно, coll_ref.stream() более эффективен и в большинстве случаев ему следует отдать предпочтение.

Это результат для фрагмента кода:

1 => {'brand': 'HP', 'name': 'HP EliteBook Model 1'}
2 => {'tags': ['Popular', 'Latest'], 'order': {'quantity': 2, 'price': 9405.0}, 'brand': 'Lenovo', 'name': 'Lenovo IdeaPad Model 2'}
4 => {'brand': 'Apple', 'name': 'Apple Macbook Pro'}
5 => {'attributes': [{'value': '1', 'unit': 'TB', 'name': 'Storage'}, {'value': '16', 'unit': 'GB', 'name': 'ram'}], 'brand': 'Apple', 'name': 'Apple Macbook Pro'}
CnidNv3f6ZQD9K7MnLyy => {'brand': 'Apple', 'name': 'Apple macbook air'}

Обратите внимание, что подколлекция документа 4 по умолчанию не читается, а массив карт документа 5 читается. На самом деле документы в подколлекции должны читаться явно, как документы верхнего уровня. Давайте прочитаем документы атрибутов в подколлекции attributes документа 4:

attr_coll_ref = (
    firestore_client.collection("laptops")
    .document("4")
    .collection("attributes")
)

for attr_doc in attr_coll_ref.stream():
    print(f"{attr_doc.id} => {attr_doc.to_dict()}")

На этот раз документы атрибутов могут быть успешно прочитаны:

ram => {'value': '16', 'unit': 'GB', 'name': 'ram'}
storage => {'value': '1', 'unit': 'TB', 'name': 'Storage'}

Чтение документов с фильтрацией запросов

В приведенных выше операциях чтения документы считываются без условий фильтрации. На практике для получения необходимых данных обычно выполняются простые и составные запросы.

Для начала попробуем получить все ноутбуки марки Apple:

# Create a reference to the laptops collection.
coll_ref = firestore_client.collection("laptops")

# Create a query against the collection reference.
query_ref = coll_ref.where("brand", "==", "Apple")

# Print the documents returned from the query:
for doc in query_ref.stream():
    print(f"{doc.id} => {doc.to_dict()}")

И вот что возвращается из этого фрагмента кода:

4 => {'brand': 'Apple', 'name': 'Apple Macbook Pro'}
5 => {'attributes': [{'value': '1', 'unit': 'TB', 'name': 'Storage'}, {'value': '16', 'unit': 'GB', 'name': 'ram'}], 'brand': 'Apple', 'name': 'Apple Macbook Pro'}
CnidNv3f6ZQD9K7MnLyy => {'brand': 'Apple', 'name': 'Apple macbook air'}

Обратите внимание, что нам нужно сначала создать ссылку на коллекцию, а затем сгенерировать запрос на ее основе.

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

Есть два оператора, которые легко спутать, а именно in и array-contains, давайте проверим их на двух простых примерах.

Оператор in возвращает документы, в которых данное поле соответствует любому из указанных значений. Например, этот запрос находит ноутбуки марки «HP» или «Lenovo»:

query_ref = coll_ref.where("brand", "in", ["HP", "Lenovo"])

С другой стороны, оператор array-contains возвращает документы, в которых данное поле массива содержит указанное значение в качестве члена. Следующий запрос находит ноутбуки с тегом «Популярные»:

query_ref = coll_ref.where("tags", "array_contains", "Popular")

Запрос для подколлекции и добавление индексов

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

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

Метод collection_group используется для фильтрации по группе сбора. Найдем атрибуты с именем «Хранилище», единицей измерения «ТБ» и значением «1»:

query_ref = (
    firestore_client.collection_group("attributes")
    .where("name", "==", "Storage")
    .where("unit", "==", "TB")
    .where("value", "==", "1")
)

for doc in query_ref.stream():
    print(f"{doc.id} => {doc.to_dict()}")

Как мы видим, метод where() можно объединить для фильтрации по нескольким полям. Когда приведенный выше код запускается, возникает ошибка, говорящая о том, что индекс недоступен.

FailedPrecondition: 400 The query requires an index. You can create it here: https://console.firebase.google.com.....

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

Щелкните ссылку, указанную в консоли, и вы будете перенаправлены на страницу для создания соответствующего составного индекса для приведенного выше запроса:

Нажмите «Создать индекс», чтобы создать составной индекс. Это займет некоторое время. Как только это будет сделано, статус изменится на «Включено»:

И когда вы снова запустите вышеуказанный запрос для подколлекции, вы успешно получите результат:

storage => {'value': '1', 'unit': 'TB', 'name': 'Storage'}

Обратите внимание, что он просто возвращает документ в подколлекции attributes и не возвращает родительский документ.

Запросы Firestore имеют массу ограничений и не подходят для сложных запросов, особенно для вложенных полей и полнотекстового поиска. Вместо этого для более сложных поисков следует рассмотреть MongoDB и Elasticsearch. Однако для внешнего интерфейса в большинстве случаев достаточно базовых запросов, представленных выше.

Обновить документы

Поздравляем, когда вы достигнете здесь! Мы рассмотрели самые сложные части письма и чтения. Остальные части для обновления и удаления документов гораздо проще.

Давайте сначала обновим значение определенного поля. Например, давайте обновим название продукта Apple MacBook Air, чтобы оно было написано с заглавной буквы:

doc_ref = firestore_client.collection("laptops").document(
    "CnidNv3f6ZQD9K7MnLyy"
)

doc_ref.update({"name": "Apple MacBook Air"})

Изменения должны быть мгновенными, как можно найти в консоли Firebase.

Затем давайте посмотрим, как обновить вложенное поле. Изменим количество на 5 для документа с идентификатором «2»:

doc_ref = firestore_client.collection("laptops").document("2")
doc_ref.update({"order.quantity": 5})

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

Наконец, давайте обновим поле tags, которое является полем массива. Добавим тег «В наличии» и удалим «последний»:

doc_ref = firestore_client.collection('laptops').document('2')

# Add a new array element.
doc_ref.update({'tags': firestore.ArrayUnion(["In Stock"])})

# Remove an existing array element.
doc_ref.update({'tags': firestore.ArrayRemove(["Latest"])})

Обратите внимание, что элементы массива, которые нужно добавить или удалить, указываются как сам массив. Это может выглядеть странно, когда добавляется или удаляется один элемент, но становится более естественным, когда обрабатывается несколько элементов.

Удалить документы

Наконец, давайте посмотрим, как удалять документы. Ну, это на самом деле довольно просто. Нам просто нужно вызвать метод delete() для ссылки на документ:

doc_ref = firestore_client.collection("laptops").document(
    "CnidNv3f6ZQD9K7MnLyy"
)
doc_ref.delete()

Как мы неоднократно упоминали, удаление документа не приведет к удалению его подколлекции. Посмотрим на практике:

doc_ref = firestore_client.collection('laptops').document('4')
doc_ref.delete()

Когда мы проверяем документ «4» в консоли Firebase, мы видим, что данные удалены, но подколлекция атрибутов все еще существует. Идентификатор документа выделен курсивом и серым цветом, что указывает на то, что документ был удален.

Что касается самой коллекции, мы не можем удалить ее напрямую с помощью библиотеки. Однако мы можем сделать это в консоли Firebase. Чтобы удалить коллекцию с библиотекой, нам нужно сначала удалить все документы. И когда все документы будут удалены, коллекция будет удалена автоматически.

В этом посте мы впервые рассказали, как настроить Firebase Admin SDK для работы с Firestore в Python. Использование файла закрытого ключа сервисной учетной записи применимо в большинстве случаев для локальной разработки. Когда клиентская библиотека аутентифицирована и инициализирована, мы можем выполнять все виды операций CRUD в Python. Мы рассмотрели, как работать с базовыми полями, полями массива, вложенными полями, а также с вложенными коллекциями. С помощью этого руководства вы будете очень уверенно управлять данными, используемыми вашими мобильными или веб-приложениями в Python.

Статьи по Теме