API App Engine Datastore para serviços legados em pacote

Observação: os desenvolvedores que criam novos aplicativos são bastante incentivados a usar a Biblioteca de cliente NDB, que oferece diversos benefícios em comparação com esta biblioteca de cliente, como armazenamento em cache automático de entidades por meio da API Memcache. Se você estiver usando a antiga biblioteca de cliente DB, leia o Guia de migração de DB para NDB.

Neste documento, descrevemos o modelo de dados para objetos armazenados no Datastore, como as consultas são estruturadas usando a API e como as transações são processadas.

Entities

Os objetos no Datastore são conhecidos comoentidades. Uma entidade tem uma ou mais propriedades nomeadas, que podem ter um ou mais valores. Os valores da propriedade pertencem a uma série de tipos de dados, como números inteiros, números de ponto flutuante, strings, datas e dados binários, entre outros. Uma consulta em uma propriedade com múltiplos valores testa se qualquer um dos valores satisfaz os critérios de consulta. Isso torna essas propriedades úteis em testes de associação.

Tipos, chaves e identificadores

Cada entidade do Datastore é de um tipo específico, que categoriza a entidade para fins de consultas. Por exemplo, um aplicativo de recursos humanos pode representar cada funcionário de uma empresa com uma entidade do tipo Employee. Além disso, cada entidade tem a própria chave para garantir a identificação de maneira exclusiva. A chave tem os seguintes componentes:

  • Um tipo de entidade
  • Um identificador, sendo ele um dos seguintes:
    • uma string de nome da chave;
    • Um código de número inteiro.
  • Um caminho ancestral opcional que localiza a entidade na hierarquia do Datastore.

O identificador é atribuído quando a entidade é criada. Por ser parte da chave da entidade, ele é associado permanentemente à entidade e não pode ser alterado. Pode ser atribuído de duas formas:

  • O aplicativo especifica a própria string de nome da chave para a entidade.
  • O Datastore pode atribuir automaticamente um ID numérico inteiro à entidade.

Caminhos ancestrais

As entidades do Cloud Datastore formam um espaço hierarquicamente estruturado, semelhante à estrutura de diretórios de um sistema de arquivos. Ao criar uma entidade, é possível designar outra entidade como mãe e a nova como filha. Ao contrário do que ocorre em um sistema de arquivos, a entidade mãe não precisa existir de verdade. Uma entidade sem mãe é uma entidade raiz. A associação entre uma entidade e a mãe é permanente e não pode ser alterada depois que a entidade é criada. O Cloud Datastore nunca atribuirá o mesmo ID numérico a duas entidades com a mesma mãe ou a duas entidades raiz (sem mãe).

A mãe de uma entidade, a mãe da mãe, e assim por diante são ancestrais dela. A filha, a filha da filha, e assim por diante são descendentes dela. Uma entidade raiz e todos os descendentes pertencem ao mesmo grupo de entidades. A sequência de entidades começando com uma entidade raiz e prosseguindo de pai para filho, levando a uma determinada entidade, constitui o caminho do ancestral dessa entidade. A chave completa que identifica a entidade consiste em uma sequência de pares de identificadores de tipo que especifica o caminho ancestral e termina com os da própria entidade:

[Person:GreatGrandpa, Person:Grandpa, Person:Dad, Person:Me]

Para uma entidade raiz, o caminho ancestral está vazio, e a chave consiste unicamente no próprio tipo e identificador da entidade:

[Person:GreatGrandpa]

Esse conceito é ilustrado pelo seguinte diagrama:

Mostra a relação entre a entidade raiz e as entidades filho no grupo de entidades

Consultas e índices

Além de recuperar entidades do Datastore diretamente por meio das respectivas chaves, um aplicativo pode realizar uma consulta para recuperá-las pelos valores das suas propriedades. A consulta opera em entidades de um determinado tipo. Ela pode especificar filtros nos valores de propriedades, chaves e ancestrais das entidades e pode retornar zero ou mais entidades como resultados. Também especifica ordens de classificação para colocar os resultados em sequência pelos respectivos valores de propriedade. Os resultados incluem todas as entidades que tenham pelo menos um valor (que pode ser nulo) para cada propriedade nomeada nos filtros e ordens de classificação e que também tenham valores de propriedade que atendam a todos os critérios de filtro especificados. A consulta pode retornar entidades de projeção, inteiras ou apenas chaves de entidade.

Uma consulta típica inclui:

Quando executada, a consulta recupera todas as entidades do tipo determinado que satisfaçam a todos os filtros fornecidos, classificadas na ordem especificada. As consultas são executadas no modo de somente leitura.

Observação: para economizar memória e melhorar o desempenho, uma consulta deve especificar sempre que possível um limite para o número de resultados retornados.

Uma consulta também pode incluir um filtro de ancestral, que limita os resultados apenas ao grupo de entidades descendentes de um ancestral especificado. Essa consulta é conhecida como consulta de ancestral. Por padrão, ela retorna resultados com consistência forte (em inglês), com garantia de atualização de acordo com as alterações mais recentes dos dados. Por outro lado, as consultas que não são de ancestral podem abranger todo o Datastore, em vez de um único grupo de entidades, mas têm apenas consistência posterior e talvez retornem resultados desatualizados. Caso a consistência forte seja importante para o aplicativo, leve isso em conta ao estruturar os dados. Coloque entidades relacionadas no mesmo grupo, assim elas serão recuperadas com uma consulta de ancestral em vez de não ancestral. Consulte Como estruturar dados para consistência forte para receber mais informações.

O App Engine predefine um índice simples em cada propriedade de uma entidade. Um aplicativo do App Engine pode definir outros índices personalizados em um arquivo de configuração de índice chamado index.yaml. O servidor de desenvolvimento adiciona sugestões a esse arquivo automaticamente quando encontra consultas não executáveis com os índices atuais. É possível ajustar os índices manualmente editando o arquivo antes de fazer upload do aplicativo.

Observação: o mecanismo de consulta baseado em índice é compatível com uma grande variedade de consultas e adequado à maioria dos aplicativos. No entanto, ele não aceita alguns tipos de consulta comuns em outras tecnologias de banco de dados. Especificamente, mesclagens e consultas agregadas não são compatíveis com o mecanismo de consulta do Datastore. Para conhecer as limitações das consultas do Datastore, consulte a página Consultas do Datastore.

Transações

Toda tentativa de inserir, atualizar ou excluir uma entidade ocorre no contexto de uma transação. Uma única transação pode incluir qualquer número dessas operações. Para manter a consistência dos dados, a transação assegura que todas as operações que ela contém sejam aplicadas ao Datastore como uma unidade ou, se alguma das operações falhar, que nenhuma delas seja aplicada.

É possível executar diversas ações em uma entidade dentro de uma única transação. Por exemplo, para incrementar um campo de contador em um objeto, é preciso ler o valor do contador, calcular o novo valor e armazená-lo novamente. Sem uma transação, é possível que outro processo incremente o contador entre o momento em que o valor é lido e o momento em que ele é atualizado, fazendo com que o aplicativo acabe substituindo o valor atualizado. Fazer leitura, cálculo e gravação em uma única transação garante que nenhum outro processo interfira no incremento.

Transações e grupos de entidades

Somente consultas de ancestrais são permitidas dentro de uma transação, ou seja, cada consulta transacional é limitada a um único grupo de entidades. A própria transação é aplicada a várias entidades pertencentes a um único grupo ou, no caso de uma transação entre grupos, a até 25 grupos diferentes de entidades.

O Datastore usa simultaneidade otimista para gerenciar transações. Quando duas ou mais transações tentam alterar o mesmo grupo de entidades ao mesmo tempo, seja atualizando entidades existentes ou criando novas, a primeira transação a ser confirmada será bem-sucedida e as demais falharão. Em seguida, essas outras transações poderão ser repetidas nos dados atualizados. Observe que isso limita o número de gravações simultâneas a serem feitas em qualquer entidade em um determinado grupo.

Transações entre grupos

Uma transação em entidades que pertençam a diferentes grupos é chamada de transação entre grupos (XG, na sigla em inglês). A transação pode ser aplicada no máximo em 25 grupos de entidades e será bem-sucedida desde que nenhuma transação simultânea interfira em qualquer um dos grupos de entidades aos quais ela se aplica. Isso torna a organização de dados mais flexível, porque você não tem obrigação de colocar dados diferentes no mesmo ancestral apenas para executar gravações atômicas neles.

Assim como em uma transação de um único grupo, não é possível executar uma consulta não ancestral em uma transação XG. No entanto, é possível realizar consultas ancestrais em grupos de entidades distintos. As consultas não transacionais (não ancestrais) poderão ver todos, alguns ou nenhum resultado de uma transação confirmada anteriormente. Para mais informações sobre esse problema, consulte Gravações do Datastore e visibilidade de dados. No entanto, as chances de essas consultas não transacionais retornarem os resultados de uma transação XG parcialmente confirmada são maiores do que as de uma transação parcialmente confirmada de um único grupo.

Uma transação XG que interfira em um único grupo de entidades tem exatamente o mesmo desempenho e custo de uma transação não XG de um único grupo. Em uma transação XG que interfira em diversos grupos de entidades, as operações têm o mesmo custo de uma transação não XG, mas podem sofrer maior latência.

Gravações no Datastore e visibilidade de dados

Os dados são gravados no Datastore em duas fases:

  1. Na fase de Commit, o registro dos dados da entidade é feito nos registros de transação da maioria das réplicas. As réplicas em que o registro não foi feito são marcadas como não tendo registros atualizados.
  2. Na fase de Apply, que ocorre de maneira independente em cada réplica, duas ações são realizadas em paralelo:
    • Os dados da entidade são gravados nessa réplica.
    • As linhas de índice para a entidade são gravadas nessa réplica. Isso pode levar mais tempo do que para gravar os dados propriamente ditos.

A operação de gravação retorna imediatamente após a fase de Commit e a fase de Apply ocorre de maneira assíncrona, possivelmente tanto em momentos diferentes em cada réplica quanto com atrasos de algumas centenas de milissegundos ou mais, após a conclusão da fase de Commit. Se ocorrer uma falha durante a fase de confirmação, novas tentativas automáticas serão realizadas. Se as falhas persistirem, o Datastore retornará uma mensagem de erro que o aplicativo receberá como uma exceção. Quando há êxito na fase de Commit e falha na fase de Apply em uma determinada réplica, a Apply é enviada para conclusão dessa réplica em uma das situações abaixo:

  • As varreduras periódicas do Datastore verificam se há jobs de confirmação incompletos e os aplicam.
  • Determinadas operações, como get, put, delete e consultas de ancestrais, que usam o grupo de entidades afetadas, fazem com que as alterações do commit que ainda não tiverem sido aplicadas sejam concluídas na réplica em que estão sendo executadas antes de prosseguir com a nova operação.

Este comportamento de gravação tem várias implicações sobre como e quando os dados estarão visíveis para o aplicativo em diferentes partes das fases de Commit e Apply:

  • Caso uma operação de gravação relate um erro de tempo limite, não será possível determinar o sucesso ou a falha da operação sem tentar ler os dados.
  • Como a função get e as consultas de ancestrais do Datastore aplicam modificações pendentes à réplica em que estão sendo executadas, essas operações sempre têm uma visão consistente de todas as transações anteriores bem-sucedidas. Isso significa que uma operação get que procura uma entidade atualizada pela chave correspondente tem a garantia de ver a versão mais recente dessa entidade.
  • As consultas não ancestrais retornam resultados desatualizados porque podem estar em execução em uma réplica em que as transações mais recentes ainda não foram aplicadas. Isso ocorre mesmo que tenha sido realizada uma operação com garantia de aplicação das transações pendentes, porque a consulta pode estar em execução em uma réplica diferente da operação anterior.
  • O tempo das alterações simultâneas afeta os resultados das consultas não ancestrais. Se a princípio uma entidade satisfaz uma consulta, mas depois é alterada para que não satisfaça mais, ela ainda estará incluída no conjunto de resultados da consulta caso as alterações não tenham sido aplicadas aos índices na réplica em que a consulta foi executada.=

Estatísticas do Datastore

O Datastore mantém estatísticas sobre os dados armazenados de um aplicativo, como o número de determinados tipos de entidades existentes ou o espaço usado por certos tipos de valores de propriedade. É possível ver essas estatísticas na página Painel do Datastore do console do Google Cloud. Você também pode usar a API Datastore para acessar esses valores de maneira programática no aplicativo, consultando entidades especialmente nomeadas. Consulte Estatísticas do Datastore no Python 2 para mais informações.

Exemplo do Datastore em Python 2

Na API do Python, um modelo descreve um tipo de entidade, inclusive os tipos e a configuração das propriedades. Um aplicativo define um modelo que usa uma classe do Python, com atributos de classe que descrevem as propriedades. O nome da classe se torna o nome do tipo de entidade. As entidades do tipo indicado são representadas por instâncias da classe de modelo, com atributos de instância representando os valores da propriedade. Para criar uma nova entidade, gere um objeto da classe pretendida, defina os atributos e chame um método, como put() para salvá-lo:

import datetime
from google.appengine.ext import db
from google.appengine.api import users

class Employee(db.Model):
  name = db.StringProperty(required=True)
  role = db.StringProperty(required=True,
                           choices=set(["executive", "manager", "producer"]))
  hire_date = db.DateProperty()
  new_hire_training_completed = db.BooleanProperty(indexed=False)
  email = db.StringProperty()

e = Employee(name="John",
             role="manager",
             email=users.get_current_user().email())
e.hire_date = datetime.datetime.now().date()
e.put()

A API Datastore oferece duas interfaces para consultas: uma interface de objeto de consulta e uma linguagem de consulta semelhante a SQL chamada GQL. Uma consulta retorna entidades na forma de instâncias das classes de modelo:

training_registration_list = ["Alfred.Smith@example.com",
                              "jharrison@example.com",
                              "budnelson@example.com"]
employees_trained = db.GqlQuery("SELECT * FROM Employee WHERE email IN :1",
                                training_registration_list)
for e in employees_trained:
  e.new_hire_training_completed = True
  db.put(e)