Consultas NDB

Um aplicativo pode usar consultas para pesquisar o Datastore em busca de entidades que correspondam a critérios de pesquisa específicos conhecidos como filtros.

Visão geral

Um aplicativo pode usar consultas para pesquisar o Datastore em busca de entidades que correspondam a critérios de pesquisa específicos conhecidos como filtros. Por exemplo, um aplicativo que rastreia vários livros de visitas poderia usar uma consulta para recuperar mensagens de apenas um livro de visitas, ordenadas por data:

from google.appengine.ext import ndb
...
class Greeting(ndb.Model):
    """Models an individual Guestbook entry with content and date."""
    content = ndb.StringProperty()
    date = ndb.DateTimeProperty(auto_now_add=True)

    @classmethod
    def query_book(cls, ancestor_key):
        return cls.query(ancestor=ancestor_key).order(-cls.date)
...
class MainPage(webapp2.RequestHandler):
    GREETINGS_PER_PAGE = 20

    def get(self):
        guestbook_name = self.request.get('guestbook_name')
        ancestor_key = ndb.Key('Book', guestbook_name or '*notitle*')
        greetings = Greeting.query_book(ancestor_key).fetch(
            self.GREETINGS_PER_PAGE)

        self.response.out.write('<html><body>')

        for greeting in greetings:
            self.response.out.write(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        self.response.out.write('</body></html>')

Algumas consultas são mais complexas que outras. O armazenamento de dados precisa de índices pré-criados para elas. Esses índices pré-criados são especificados em um arquivo de configuração, index.yaml. No servidor de desenvolvimento, se você executar uma consulta que precisa de um índice que não foi especificado, o servidor de desenvolvimento a adicionará automaticamente ao index.yaml. Mas no site, uma consulta que precisa de um índice ainda não especificado falha. Assim, o ciclo de desenvolvimento típico é tentar uma nova consulta no servidor de desenvolvimento e, em seguida, atualizar o site para usar o index.yaml alterado automaticamente. Você pode atualizar index.yaml separadamente do upload do aplicativo executando gcloud app deploy index.yaml. Se o armazenamento de dados tiver muitas entidades, levará muito tempo para criar um novo índice para elas. Nesse caso, é aconselhável atualizar as definições de índice antes de fazer o upload do código que usa o novo índice. Você pode usar o console de administração para descobrir quando a criação dos índices for concluída.

O App Engine Datastore é nativamente compatível com filtros de correspondências exatas (o operador ==) e comparações (os operadores <, <=, > e >=). Ele aceita a combinação de vários filtros usando uma operação AND booleana, com algumas limitações (consulte abaixo).

Além dos operadores nativos, a API é compatível com o operador !=, combinando grupos de filtros usando a operação booleana OR e a operação IN, que testam a igualdade de um de uma lista de valores possíveis. (como o operador "em" do Python). Essas operações não se comparam exatamente às operações nativas do Datastore, sendo um pouco peculiares e lentas em relação a elas. Elas são implementadas usando a mesclagem na memória dos fluxos de resultados. Observe que p != v é implementado como "p < v OR p > v". Isso é relevante para propriedades repetidas.

Limitações: o Datastore impõe algumas restrições às consultas. Violá-las faz com que ele gere exceções. Por exemplo, combinar muitos filtros, usar desigualdades de várias propriedades ou combinar uma desigualdade com uma ordem de classificação em uma propriedade diferente não é permitido no momento. Além disso, os filtros que referir-se a várias propriedades às vezes exigem que os índices secundários sejam configurados.

Incompatibilidade: o Datastore não é diretamente compatível com correspondências de substring, correspondências que não diferenciam maiúsculas de minúsculas ou pesquisa de texto completo. Há maneiras de implementar correspondências que não diferenciam maiúsculas de minúsculas e até mesmo pesquisa de texto completo usando propriedades calculadas.

Como filtrar por valores de propriedade

Chame novamente a classe Account das Propriedades do NDB:

class Account(ndb.Model):
    username = ndb.StringProperty()
    userid = ndb.IntegerProperty()
    email = ndb.StringProperty()

Normalmente não convém recuperar todas as entidades de um determinado tipo, mas apenas aquelas com um valor específico ou um intervalo de valores de alguma propriedade.

Os objetos de propriedade sobrecarregam alguns operadores para retornar expressões de filtro que podem ser usadas para controlar uma consulta. Por exemplo, para localizar todas as entidades Account que tenham a propriedade userid com o valor 42 exato, você pode usar a expressão

query = Account.query(Account.userid == 42)

Se você tiver certeza de que havia apenas um Account com esse userid, talvez prefira usar userid como chave. Account.get_by_id(...) é mais rápido que Account.query(...).get().

O NDB aceita estas operações:

property == value
property < value
property <= value
property > value
property >= value
property != value
property.IN([value1, value2])

Para filtrar por uma desigualdade, você pode usar uma sintaxe como esta:

query = Account.query(Account.userid >= 40)

Ela localiza todas as entidades Account com a propriedade userid maior ou igual a 40.

Duas dessas operações, != e IN, são implementadas como combinações das outras e são um pouco peculiares, como descrito em != e IN.

Você pode especificar vários filtros:

query = Account.query(Account.userid >= 40, Account.userid < 50)

Esse combina os argumentos de filtro especificados, retornando todas as entidades Account com valor de userid maior ou igual a 40 e menor que 50.

Observação: conforme mencionado anteriormente, o Datastore rejeita consultas usando a filtragem de desigualdade em mais de uma propriedade.

Em vez de especificar um filtro de consulta inteiro em uma única expressão, talvez convenha criá-lo em etapas. Por exemplo:

query1 = Account.query()  # Retrieve all Account entitites
query2 = query1.filter(Account.userid >= 40)  # Filter on userid >= 40
query3 = query2.filter(Account.userid < 50)  # Filter on userid < 50 too

query3 é equivalente à variável query do exemplo anterior. Observe que os objetos de consulta são imutáveis, portanto, a construção de query2 não afeta query1 e a construção de query3 não afeta query1 ou query2.

As operações!= e IN

Chame novamente a classe Article das Propriedades do NDB:

class Article(ndb.Model):
    title = ndb.StringProperty()
    stars = ndb.IntegerProperty()
    tags = ndb.StringProperty(repeated=True)

As operações != (não igual) e IN (associação) são implementadas combinando outros filtros usando a operação OR. A primeira delas

property != value

é implementada como

(property < value) OR (property > value)

Por exemplo:

query = Article.query(Article.tags != 'perl')

é equivalente a:

query = Article.query(ndb.OR(Article.tags < 'perl',
                             Article.tags > 'perl'))

Observação: talvez seja algo surpreendente, mas essa consulta não procura as entidades Article que não incluam 'perl' como tag. Em vez disso, ela encontra todas as entidades com pelo menos uma tag diferente de 'perl'. Por exemplo, a seguinte entidade seria incluída nos resultados, mesmo que tivesse 'perl' como uma das tags:

Article(title='Perl + Python = Parrot',
        stars=5,
        tags=['python', 'perl'])

No entanto, esta não seria incluída:

Article(title='Introduction to Perl',
        stars=3,
        tags=['perl'])

Não há como fazer consulta de entidades que não incluam uma tag igual a 'perl'.

Da mesma forma, a operação IN

property IN [value1, value2, ...]

que testa a participação em uma lista de valores possíveis, é implementada como

(property == value1) OR (property == value2) OR ...

Por exemplo:

query = Article.query(Article.tags.IN(['python', 'ruby', 'php']))

é equivalente a:

query = Article.query(ndb.OR(Article.tags == 'python',
                             Article.tags == 'ruby',
                             Article.tags == 'php'))

Observação: consultas usando OR duplicam os resultados: o fluxo de resultado não inclui a entidade mais de uma vez, mesmo que uma entidade corresponda a duas ou mais subconsultas.

Como fazer consulta de propriedades repetidas

A classe Article definida na seção anterior também serve como um exemplo de consulta para propriedades repetidas. Notavelmente, um filtro como

usa um único valor, mesmo que Article.tags seja uma propriedade repetida. Não é possível comparar propriedades repetidas para listar objetos (o Datastore não entenderá), e um filtro como

Article.tags.IN(['python', 'ruby', 'php'])

faz algo completamente diferente de pesquisar entidades Article cujo valor de tags é a lista ['python', 'ruby', 'php']: ele pesquisa entidades cujo valor tags (considerado uma lista) contém pelo menos um desses valores.

Consultar um valor de None em uma propriedade repetida tem um comportamento indefinido. não faça isso.

Como combinar as operações AND e OR

Você pode aninhar as operações AND e OR arbitrariamente. Exemplo:

query = Article.query(ndb.AND(Article.tags == 'python',
                              ndb.OR(Article.tags.IN(['ruby', 'jruby']),
                                     ndb.AND(Article.tags == 'php',
                                             Article.tags != 'perl'))))

No entanto, devido à implementação de OR, uma consulta desse formato, que é muito complexa, pode falhar com uma exceção. É mais seguro normalizar esses filtros de modo que haja (no máximo) uma única operação OR na parte superior da árvore da expressão e um único nível de operações AND abaixo dela.

Para executar essa normalização, você precisa se lembrar de ambas as regras da lógica booleana e de como os filtros != e IN são realmente implementados:

  1. Expanda os operadores != e IN para a forma primitiva, em que != se torna uma verificação para a propriedade que é <or> do que o valor e IN se torna uma verificação para a propriedade que está sendo == ao primeiro ou ao segundo valor ou até o último valor da lista.
  2. Um AND com OR dentro dele é equivalente a OR de vários AND s aplicados aos operandos AND originais, com um único operando OR substituído pelo original OR. Por exemplo, AND(a, b, OR(c, d)) é equivalente a OR(AND(a, b, c), AND(a, b, d)).
  3. Um AND que tem um operando que é uma operação AND pode incorporar os operandos do AND aninhado no AND delimitador. Por exemplo, AND(a, b, AND(c, d)) é equivalente a AND(a, b, c, d).
  4. Um OR que tem um operando que é uma operação OR pode incorporar os operandos do OR aninhado no OR delimitador. Por exemplo, OR(a, b, OR(c, d)) é equivalente a OR(a, b, c, d).

Se aplicarmos essas transformações em estágios ao filtro de exemplo, ao usar uma notação mais simples que o Python, você consegue:

  1. Usando a regra nº 1 em IN e !=:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php',
             OR(tags < 'perl', tags > 'perl'))))
  2. Usando a regra nº 2 no OR mais interno aninhado em um AND:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         OR(AND(tags == 'php', tags < 'perl'),
            AND(tags == 'php', tags > 'perl'))))
  3. Usando a regra nº 4 no OR aninhado em outro OR:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php', tags < 'perl'),
         AND(tags == 'php', tags > 'perl')))
  4. Usando a regra nº 2 no restante do OR aninhado em um AND:
    OR(AND(tags == 'python', tags == 'ruby'),
       AND(tags == 'python', tags == 'jruby'),
       AND(tags == 'python', AND(tags == 'php', tags < 'perl')),
       AND(tags == 'python', AND(tags == 'php', tags > 'perl')))
  5. Usando a regra nº 3 para recolher os AND aninhados restantes:
    OR(AND(tags == 'python', tags == 'ruby'),
       AND(tags == 'python', tags == 'jruby'),
       AND(tags == 'python', tags == 'php', tags < 'perl'),
       AND(tags == 'python', tags == 'php', tags > 'perl'))

Cuidado: para alguns filtros, essa normalização pode causar uma explosão combinatória. Considere o AND de três cláusulas OR com duas cláusulas básicas cada. Quando normalizado, isso se torna um OR de oito cláusulas AND com três cláusulas básicas cada, ou seja, seis termos tornam-se 24.

Como especificar ordens de classificação

Use o método order() para especificar a ordem em que uma consulta retorna os resultados. Esse método usa uma lista de argumentos, cada um sendo um objeto de propriedade (a ser classificado em ordem crescente) ou a respectiva negação (denotando ordem decrescente). Exemplo:

query = Greeting.query().order(Greeting.content, -Greeting.date)

Isso recupera todas as entidades Greeting, classificadas pelo valor crescente da propriedade content. As execuções de entidades consecutivas com a mesma propriedade de conteúdo serão classificadas pelo valor decrescente da propriedade date. É possível usar várias chamadas order() para o mesmo efeito:

query = Greeting.query().order(Greeting.content).order(-Greeting.date)

Observação: quando você combina filtros com order(), o Datastore rejeita certas combinações. Particularmente, quando você usa um filtro de desigualdade, a primeira ordem de classificação (se houver) precisa especificar a mesma propriedade do filtro. Além disso, às vezes você precisa configurar um índice secundário.

Consultas de ancestral

As consultas de ancestral permitem que você faça consultas de consistência forte ao armazenamento de dados, no entanto, entidades com o mesmo ancestral estão limitadas a uma gravação por segundo. Aqui está uma comparação simples dos dilemas e da estrutura entre uma consulta de ancestral e de não ancestral usando clientes e respectivas compras associadas no armazenamento de dados.

No exemplo não ancestral a seguir, há uma entidade no armazenamento de dados para cada Customer e uma entidade no armazenamento de dados para cada Purchase, com um KeyProperty que aponta para o cliente.

class Customer(ndb.Model):
    name = ndb.StringProperty()

class Purchase(ndb.Model):
    customer = ndb.KeyProperty(kind=Customer)
    price = ndb.IntegerProperty()

Para encontrar todas as compras pertencentes ao cliente, você pode usar a seguinte consulta:

purchases = Purchase.query(
    Purchase.customer == customer_entity.key).fetch()

Nesse caso, o armazenamento de dados oferece alta capacidade de gravação, mas apenas consistência eventual. Se uma nova compra foi adicionada, você pode receber dados desatualizados. Elimine esse comportamento usando consultas de ancestral.

Para clientes e compras com consultas de ancestral, você ainda tem a mesma estrutura com duas entidades separadas. A parte do cliente é igual. No entanto, quando você cria compras, não precisa mais especificar KeyProperty() para compras. Isso ocorre porque, quando usa consultas de ancestral, você chama a chave da entidade do cliente ao criar uma entidade de compra.

class Customer(ndb.Model):
    name = ndb.StringProperty()

class Purchase(ndb.Model):
    price = ndb.IntegerProperty()

Cada compra tem uma chave e o cliente também tem a própria chave. No entanto, cada chave de compra terá a chave de customer_entity incorporada. Lembre-se, isso será limitado a uma gravação por ancestral por segundo. O seguinte código cria uma entidade com um ancestral:

purchase = Purchase(parent=customer_entity.key)

Para consultar as compras de um determinado cliente, use a consulta a seguir.

purchases = Purchase.query(ancestor=customer_entity.key).fetch()

Atributos de consulta

Os objetos de consulta têm os seguintes atributos de dados somente leitura:

AtributoTipoPadrãoDescrição
kindstr None Nome do tipo (geralmente o nome da classe)
ancestorKey None Ancestral especificado para a consulta
filtersFilterNode None Expressão de filtro
ordersOrder None Ordens de classificação

Imprimir um objeto de consulta (ou chamar str() ou repr() nele) produz uma representação de string bem formatada:

print(Employee.query())
# -> Query(kind='Employee')
print(Employee.query(ancestor=ndb.Key(Manager, 1)))
# -> Query(kind='Employee', ancestor=Key('Manager', 1))

Como filtrar por valores de propriedades estruturados

Uma consulta pode filtrar diretamente pelos valores de campo de propriedades estruturadas. Por exemplo, uma consulta para todos os contatos com um endereço cuja cidade seja 'Amsterdam' seria semelhante a

query = Contact.query(Contact.addresses.city == 'Amsterdam')

Se você combinar vários desses filtros, eles poderão corresponder a diferentes subentidades de Address na mesma entidade Contact. Exemplo:

query = Contact.query(Contact.addresses.city == 'Amsterdam',  # Beware!
                      Contact.addresses.street == 'Spear St')

Pode encontrar contatos com um endereço cuja cidade é 'Amsterdam' e outro endereço (diferente) cuja rua seja 'Spear St'. No entanto, pelo menos para filtros de igualdade, é possível criar uma consulta que retorna apenas resultados com vários valores em uma única subentidade:

query = Contact.query(Contact.addresses == Address(city='San Francisco',
                                                   street='Spear St'))

Se você usar essa técnica, as propriedades da subentidade serão iguais a None são ignorados na consulta. Se uma propriedade tiver um valor padrão, você precisará defini-la explicitamente como None para ignorá-la na consulta, caso contrário, a consulta incluirá um filtro que exige que o valor da propriedade seja igual ao padrão. Por exemplo, se o modelo Address tiver uma propriedadecountry com default='us', e exemplo acima só retornará contatos com o país igual a 'us'. Para considerar contatos com outros valores de país, seria necessário filtrar por Address(city='San Francisco', street='Spear St', country=None).

Se uma subentidade tiver algum valor de propriedade igual a None, ele será ignorado. Portanto, não faz sentido filtrar um valor de propriedade de subentidade de None.

Como usar propriedades nomeadas por string

Às vezes, convém filtrar ou pedir uma consulta com base em uma propriedade que tenha o nome especificado por string. Por exemplo, se você permitir que o usuário insira consultas de pesquisa como tags:python, seria conveniente transformar isso em uma consulta como

Article.query(Article."tags" == "python") # does NOT work

Se o modelo for um Expando, o filtro poderá usar GenericProperty, a classe Expando usará para propriedades dinâmicas:

property_to_query = 'location'
query = FlexEmployee.query(ndb.GenericProperty(property_to_query) == 'SF')

O uso de GenericProperty também funciona se o modelo não for um Expando, mas se você quiser garantir que esteja usando apenas nomes de propriedade definidos, use o atributo de classe _properties.

query = Article.query(Article._properties[keyword] == value)

ou use getattr() para recebê-lo da classe:

query = Article.query(getattr(Article, keyword) == value)

A diferença é que getattr() usa o "nome Python" da propriedade, enquanto _properties é indexado pelo "nome do armazenamento de dados" da propriedade. Eles só diferem quando a propriedade tiver sido declarada com algo assim:

class ArticleWithDifferentDatastoreName(ndb.Model):
    title = ndb.StringProperty('t')

Aqui, o nome do Python é title, mas o nome do armazenamento de dados é t.

Essas abordagens também funcionam para ordenar os resultados da consulta:

expando_query = FlexEmployee.query().order(ndb.GenericProperty('location'))

property_query = Article.query().order(Article._properties[keyword])

Iteradores de consulta

Enquanto uma consulta está em andamento, o estado dela é mantido em um objeto iterador. A maioria dos aplicativos não os usa diretamente. Normalmente, é mais simples chamar fetch(20) do que manipular o objeto iterador. Há duas maneiras básicas de receber tal objeto:

  • usando a função iter() integrada do Python em um objeto Query;
  • chamando o método iter() do objeto Query.

O primeiro aceita o uso de uma repetição for do Python (que implicitamente chama a função iter()) para repetir uma consulta.

for greeting in greetings:
    self.response.out.write(
        '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

A segunda maneira, usando o método iter() do objeto Query, permite passar opções ao iterador para afetar o comportamento dele. Por exemplo, para usar uma consulta somente de chaves em um loop for, escreva isto:

for key in query.iter(keys_only=True):
    print(key)

Os iteradores de consulta têm outros métodos úteis:

Método Descrição
__iter__() Parte do protocolo do iterador do Python.
next() Retorna o próximo resultado ou gera a exceção StopIteration, se não houver nenhuma.

has_next() Retorna True se uma chamada next() posterior retornará um resultado, False se ele aumentar StopIteration.

Bloqueia até que a resposta a esta pergunta seja conhecida e armazena o resultado em buffer (se houver) até que você o recupere com next().
probably_has_next() Como has_next(), mas usa um atalho mais rápido (e às vezes impreciso).

Pode retornar um falso positivo (True quando next() aumentaria StopIteration), mas nunca um falso negativo (False quando next() retornaria um resultado).
cursor_before() Retorna um cursor de consulta representando um ponto imediatamente antes do último resultado retornado.

Gera uma exceção se nenhum cursor estiver disponível (principalmente se a opção de consulta produce_cursors não tiver sido transmitida).
cursor_after() Retorna um cursor de consulta que representa um ponto logo após o último resultado retornado.

Gera uma exceção se nenhum cursor estiver disponível (principalmente se a opção de consulta produce_cursors não tiver sido transmitida).
index_list() Retorna uma lista de índices usados por uma consulta executada, incluindo índices principais, compostos, de tipo e de propriedade única.

Cursores de consulta

Um cursor de consulta é uma pequena estrutura de dados opaca que representa um ponto de retomada em uma consulta. Isso é útil para mostrar ao usuário uma página de resultados por vez. Também é útil para processar trabalhos longos que podem precisar parar e retomar. Uma maneira comum de usá-los é com o método fetch_page() de uma consulta. Ele funciona como fetch(), mas retorna um (results, cursor, more) triplo. A sinalização more retornada indica que provavelmente há mais resultados. Uma IU pode usar isso, por exemplo, para suprimir um botão ou link "Próxima página". Para solicitar páginas subsequentes, passe o cursor retornado por uma chamada fetch_page() para a próxima. Um BadArgumentError será gerado se você passar um cursor inválido. Observe que a validação só verifica se o valor tem codificação base64. Você terá que fazer qualquer validação adicional necessária.

Assim, para permitir que o usuário veja todas as entidades correspondentes a uma consulta, buscando uma página por vez, seu código seria semelhante a este:

from google.appengine.datastore.datastore_query import Cursor
...
class List(webapp2.RequestHandler):
    GREETINGS_PER_PAGE = 10

    def get(self):
        """Handles requests like /list?cursor=1234567."""
        cursor = Cursor(urlsafe=self.request.get('cursor'))
        greets, next_cursor, more = Greeting.query().fetch_page(
            self.GREETINGS_PER_PAGE, start_cursor=cursor)

        self.response.out.write('<html><body>')

        for greeting in greets:
            self.response.out.write(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        if more and next_cursor:
            self.response.out.write('<a href="/list?cursor=%s">More...</a>' %
                                    next_cursor.urlsafe())

        self.response.out.write('</body></html>')

Observe o uso de urlsafe() e Cursor(urlsafe=s) para serializar e desserializar o cursor. Isso permite passar um cursor para um cliente na Web na resposta a uma solicitação e a receber de volta do cliente em uma solicitação posterior.

Observação: o método fetch_page() normalmente retorna um cursor mesmo que não haja mais resultados, mas isso não é garantido: o valor do cursor retornado pode ser None. Observe também que, como o more é implementado usando o método do iterador probably_has_next() em circunstâncias raras, pode retornar True mesmo que a próxima página esteja vazia.

Algumas consultas do NDB não aceitam cursores de consulta, mas você pode corrigi-las. Se uma consulta usar IN, OR ou !=, os resultados da consulta não funcionarão com cursores a menos que sejam ordenados por chave. Se um aplicativo não ordenar os resultados por chave e chamadas fetch_page(), ele recebe um BadArgumentError. Se User.query(User.name.IN(['Joe', 'Jane'])).order(User.name).fetch_page(N) receber um erro, altere-o para User.query(User.name.IN(['Joe', 'Jane'])).order(User.name, User.key).fetch_page(N)

Em vez de "paginar" pelos resultados da consulta, você pode usar o método iter() de uma consulta para receber um cursor em um ponto preciso. Para fazer isso, transmita produce_cursors=True para iter(); Quando o iterador estiver no lugar certo, chame o cursor_after() dele para receber um cursor logo depois disso. Ou, da mesma forma, chame cursor_before() para um cursor logo antes. Observe que chamar cursor_after() ou cursor_before() pode fazer uma chamada de Datastore de bloqueio, repetindo parte da consulta para extrair um cursor que aponte para o meio de um lote.

Para usar um cursor visando retroceder páginas dos resultados da consulta, crie uma consulta inversa:

# Set up.
q = Bar.query()
q_forward = q.order(Bar.key)
q_reverse = q.order(-Bar.key)

# Fetch a page going forward.
bars, cursor, more = q_forward.fetch_page(10)

# Fetch the same page going backward.
r_bars, r_cursor, r_more = q_reverse.fetch_page(10, start_cursor=cursor)

Como chamar uma função para cada entidade ("mapeamento")

Suponha que você precise receber as entidades Account correspondentes às entidades Message retornadas por uma consulta. Você poderia escrever algo assim:

message_account_pairs = []
for message in message_query:
    key = ndb.Key('Account', message.userid)
    account = key.get()
    message_account_pairs.append((message, account))

No entanto, isso é bastante ineficiente: ele aguarda para buscar uma entidade e a utiliza, depois aguarda a próxima entidade e a utiliza. Há muito tempo de espera. Outra maneira é escrever uma função de retorno de chamada que é mapeada nos resultados da consulta:

def callback(message):
    key = ndb.Key('Account', message.userid)
    account = key.get()
    return message, account

message_account_pairs = message_query.map(callback)
# Now message_account_pairs is a list of (message, account) tuples.

Essa versão será executada um pouco mais rápido do que a repetição simples de for acima, porque permite alguma simultaneidade. No entanto, como a chamada get() em callback() ainda é síncrona, o ganho não é grande. Este é um bom lugar para usar o asynchronous gets.

GQL

O GQL é uma linguagem semelhante a SQL para recuperação de entidades ou chaves do App Engine Datastore. Embora os recursos de GQL sejam diferentes dos de uma linguagem de consulta de banco de dados relacional tradicional, a sintaxe de GQL é semelhante a SQL. A sintaxe do GQL é descrita na Referência do GQL.

É possível usar o GQL para construir consultas. Isso é semelhante à criação de uma consulta com Model.query(), mas usa a sintaxe GQL para definir o filtro e a ordem da consulta. Para usá-lo:

  • ndb.gql(querystring) retorna um objeto Query (o mesmo tipo retornado por Model.query()). Todos os métodos comuns estão disponíveis nesses objetos Query: fetch(), map_async(), filter() etc.
  • Model.gql(querystring) é uma forma abreviada de ndb.gql("SELECT * FROM Model " + querystring). Normalmente, querystring é algo como "WHERE prop1 > 0 AND prop2 = TRUE".
  • Para consultar modelos que contêm propriedades estruturadas, use foo.bar na sintaxe do GQL para referir-se a subpropriedades.
  • O GQL aceita ligações de parâmetros do tipo SQL. Um aplicativo pode definir uma consulta e, em seguida, ligar valores a ela:
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1")
    query2 = query.bind(3)
    
    ou
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1", 3)

    Chamar a função bind() de uma consulta retorna uma nova consulta, o que não altera a original.

  • Se a classe de modelo modifica o método de classe _get_kind(), a consulta GQL precisará usar o tipo retornado por essa função, não o nome da classe.
  • Se uma propriedade em seu modelo modificar seu nome (por exemplo, foo = StringProperty('bar')) sua consulta GQL deve usar o nome da propriedade modificada (no exemplo, bar).

Sempre use o recurso de ligação de parâmetro se alguns valores da consulta forem variáveis fornecidas pelo usuário. Isso evita ataques baseados em hacks de sintaxe.

É um erro consultar um modelo que não tenha sido importado (ou, de maneira mais geral, definido).

É um erro usar um nome de propriedade que não seja definido pela classe de modelo, a menos que esse modelo seja um Expando.

Especificar um limite ou deslocamento para fetch() da consulta substitui o limite ou o deslocamento definido pelas cláusulas OFFSET e LIMIT de GQL. Não combine OFFSET e LIMIT do GQL com fetch_page(). Observe que o máximo de 1.000 resultados impostos pelo App Engine se aplica ao deslocamento e ao limite.

Se você está acostumado com o SQL, tenha cuidado com as falsas suposições ao usar o GQL. O GQL é traduzido na API de consulta nativa do NDB. Isso é diferente de um mapeador Object-Relational típico (como SQLAlchemy ou a compatibilidade com banco de dados Django), em que as chamadas de API são convertidas em SQL antes de serem transmitidas para o servidor de banco de dados. O GQL não aceita modificações do Datastore (inserções, exclusões ou atualizações), apenas consultas.