Wilberhg's blog

Guia Prático: Firestore com Python

O Google Cloud Firestore é um banco de dados NoSQL de documentos flexível, escalável e totalmente gerenciado, ideal para armazenamento de dados hierárquicos e aplicações distribuídas.

Diferente de bancos relacionais (tabelas e linhas), o Firestore organiza os dados no modelo Coleção → Documento → Subcoleção / Campos:


1. Instalação e Configuração

Existem dois SDKs principais para interagir com o Firestore em Python:

  1. google-cloud-firestore: SDK nativo do Google Cloud, recomendado para microsserviços, backends corporativos, bots e processamentos em lote.
  2. firebase-admin: Recomendado quando o projeto faz uso de todo o ecossistema Firebase (Firebase Auth, Cloud Messaging, Storage).

Neste guia, utilizaremos o SDK oficial do Google Cloud:

pip install google-cloud-firestore

Configuração de Credenciais

Para autenticar em ambientes de desenvolvimento ou servidores corporativos:

  1. Acesse o Google Cloud Console → IAM & Admin → Contas de Serviço (Service Accounts).
  2. Crie uma chave em formato JSON para a conta com a role Cloud Datastore User (ou permissões equivalentes no Firestore).
  3. Defina a variável de ambiente apontando para o arquivo da chave:
# Linux/macOS
export GOOGLE_APPLICATION_CREDENTIALS="/caminho/para/service_account.json"

# Windows (PowerShell)
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\caminho\para\service_account.json"

2. Inicializando o Cliente Firestore

Com a variável de ambiente configurada, a inicialização é direta:

from google.cloud import firestore

# Inicializa o cliente usando o projeto padrão das credenciais
db = firestore.Client()

# Caso queira especificar explicitamente o projeto e/ou banco de dados:
# db = firestore.Client(project="meu-projeto-id", database="(default)")

3. Operações Fundamentais de CRUD

3.1. Create (Inserção de Documentos)

Podemos inserir documentos com IDs definidos manualmente (set) ou gerar IDs únicos automaticamente (add).

from datetime import datetime

# 1. Definindo o ID do documento manualmente (set)
doc_ref = db.collection("usuarios").document("user_001")
doc_ref.set({
    "nome": "Wilber Godoy",
    "email": "wilber@exemplo.com",
    "ativo": True,
    "departamento": "TI",
    "criado_em": firestore.SERVER_TIMESTAMP,
})

# 2. Gerando um ID aleatório gerado pelo Firestore (add)
timestamp, nova_ref = db.collection("usuarios").add({
    "nome": "Carlos Souza",
    "email": "carlos@exemplo.com",
    "ativo": False,
    "departamento": "Engenharia",
    "criado_em": datetime.utcnow(),
})

print(f"Documento criado com ID automático: {nova_ref.id}")

3.2. Read (Leitura e Consultas)

Lendo um documento específico por ID:

doc_ref = db.collection("usuarios").document("user_001")
snapshot = doc_ref.get()

if snapshot.exists:
    dados = snapshot.to_dict()
    print(f"Dados do usuário: {dados}")
    print(f"E-mail: {dados.get('email')}")
else:
    print("Documento não encontrado!")

Listando todos os documentos de uma coleção:

usuarios_ref = db.collection("usuarios")
docs = usuarios_ref.stream()

print("--- Lista de Usuários ---")
for doc in docs:
    print(f"ID: {doc.id} -> {doc.to_dict()}")

3.3. Update (Atualização de Campos)

O método update() modifica apenas os campos informados, sem sobrescrever o documento inteiro (diferente do set() puro):

doc_ref = db.collection("usuarios").document("user_001")

# Atualização de campos simples
doc_ref.update({
    "departamento": "Automação & Dados",
    "ativo": True,
    "atualizado_em": firestore.SERVER_TIMESTAMP,
})

# Atualização de campos aninhados (sem sobrescrever o mapa completo)
doc_ref.update({
    "preferencias.notificacoes_email": True,
    "preferencias.tema": "dark",
})

Operações Especiais de Campo:

O Firestore fornece funções atômicas para atualizar campos sem precisar ler o documento antes:

from google.cloud.firestore import Increment, ArrayUnion, ArrayRemove, DELETE_FIELD

doc_ref = db.collection("metricas").document("execucoes")

doc_ref.update({
    # Incrementa ou decrementa contadores atomicamente
    "total_processado": Increment(1),
    # Adiciona elementos a uma lista sem duplicatas
    "tags": ArrayUnion(["python", "cloud"]),
    # Remove elementos de uma lista
    "status_antigos": ArrayRemove(["pendente"]),
    # Exclui um campo específico do documento
    "observacao_temporaria": DELETE_FIELD,
})

3.4. Delete (Exclusão)

# Exclusão de um documento específico
db.collection("usuarios").document("user_001").delete()
print("Documento removido com sucesso.")

Atenção: A exclusão de um documento não remove automaticamente suas subcoleções. Se houver subcoleções associadas, elas continuarão existindo e deverão ser excluídas explicitamente.


4. Consultas Estruturadas (Queries)

Nas versões modernas do SDK, as condições de busca utilizam a classe FieldFilter:

from google.cloud.firestore_v1.base_query import FieldFilter

# 1. Filtro simples de igualdade
query_ativos = (
    db.collection("usuarios")
    .where(filter=FieldFilter("ativo", "==", True))
)

for doc in query_ativos.stream():
    print(doc.id, doc.to_dict())

# 2. Filtros múltiplos e ordenação
query_composta = (
    db.collection("usuarios")
    .where(filter=FieldFilter("departamento", "==", "TI"))
    .where(filter=FieldFilter("ativo", "==", True))
    .order_by("nome", direction=firestore.Query.ASCENDING)
    .limit(10)
)

for doc in query_composta.stream():
    print(f"Nome: {doc.to_dict()['nome']}")

Principais Operadores de Comparação:


5. Operações em Lote (Batched Writes)

Quando é necessário realizar múltiplas inserções, atualizações ou exclusões de forma atômica (tudo ou nada) com custo reduzido de rede, utilize lotes (batches):

batch = db.batch()

# Limite do Firestore: até 500 operações por batch
for i in range(5):
    doc_ref = db.collection("logs").document(f"log_{i}")
    batch.set(doc_ref, {
        "mensagem": f"Evento de processamento #{i}",
        "timestamp": firestore.SERVER_TIMESTAMP,
    })

# Executa todas as gravações atomicamente
batch.commit()
print("5 documentos gravados em um único batch.")

6. Transações Atômicas

Diferente de lotes (que apenas agrupam gravações), transações garantem consistência estrita para operações de leitura seguidas de gravação (Read-Modify-Write):

@firestore.transactional
def transferir_pontos(transaction, ref_origem, ref_destino, pontos: int):
    # Leituras OBRIGATORIAMENTE antes de qualquer gravação
    snap_origem = ref_origem.get(transaction=transaction)
    snap_destino = ref_destino.get(transaction=transaction)

    saldo_origem = snap_origem.get("saldo")
    if saldo_origem < pontos:
        raise ValueError("Saldo insuficiente para transferência!")

    saldo_destino = snap_destino.get("saldo")

    # Gravações após validação
    transaction.update(ref_origem, {"saldo": saldo_origem - pontos})
    transaction.update(ref_destino, {"saldo": saldo_destino + pontos})

# Uso:
ref_conta_a = db.collection("contas").document("conta_a")
ref_conta_b = db.collection("contas").document("conta_b")

transaction = db.transaction()
transferir_pontos(transaction, ref_conta_a, ref_conta_b, 50)

7. Subcoleções e Estruturação Hierárquica

Subcoleções organizam relacionamentos 1:N sem gerar documentos gigantescos:

# Adicionando um comentário dentro da subcoleção de um post
post_ref = db.collection("posts").document("post_123")
comentario_ref = post_ref.collection("comentarios").document()

comentario_ref.set({
    "autor": "Wilber Godoy",
    "texto": "Excelente tutorial!",
    "data": firestore.SERVER_TIMESTAMP,
})

# Lendo comentários daquele post específico
comentarios = post_ref.collection("comentarios").stream()
for c in comentarios:
    print(c.to_dict())

8. Boas Práticas e Recomendações

  1. Atenção aos Custos de Leitura/Gravação: O faturamento do Firestore é baseado no volume de operações de leitura, gravação e exclusão. Evite consultas genéricas sem paginação (limit()).
  2. Limite de 1 MB por Documento: Cada documento individual tem um tamanho máximo estrito de 1 megabyte. Guarde referências ou distribua dados em subcoleções caso haja listas com crescimento indefinido.
  3. Crie Índices Compostos quando Solicitado: Ao combinar múltiplos filtros de desigualdade (>, <) ou filtros de campos distintos com order_by, o Firestore exige um índice composto. A biblioteca gerará um erro no console contendo o link direto para criar o índice no GCP com um clique.
  4. Ambiente Local com Firestore Emulator: Para testes unitários e de integração sem gastar cotas ou depender de conexão de internet, utilize o Cloud Firestore Emulator do Firebase CLI e configure a variável FIRESTORE_EMULATOR_HOST="localhost:8080".