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:
- Coleções (Collections): Contêineres de documentos (análogos a tabelas, mas sem esquema rígido).
- Documentos (Documents): Unidades de armazenamento contendo pares de chave-valor (semelhantes a objetos JSON ou dicionários Python).
1. Instalação e Configuração
Existem dois SDKs principais para interagir com o Firestore em Python:
google-cloud-firestore: SDK nativo do Google Cloud, recomendado para microsserviços, backends corporativos, bots e processamentos em lote.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:
- Acesse o Google Cloud Console IAM & Admin Contas de Serviço (Service Accounts).
- Crie uma chave em formato JSON para a conta com a role
Cloud Datastore User(ou permissões equivalentes no Firestore). - 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:
==: Igualdade.!=: Diferente de.<,<=,>,>=: Comparações numéricas, temporais ou alfabéticas.in: Verifica se o valor está em uma lista (máximo de 30 elementos).array_contains: Verifica se uma lista contém determinado valor.array_contains_any: Verifica se uma lista contém ao menos um dos valores fornecidos.
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
- 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()). - 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.
- Crie Índices Compostos quando Solicitado: Ao combinar múltiplos filtros de desigualdade (
>,<) ou filtros de campos distintos comorder_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. - 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".