Consultas NDB

Uma aplicação pode usar consultas para pesquisar no Datastore entidades que correspondam a critérios de pesquisa específicos denominados filtros.

Vista geral

Uma aplicação pode usar consultas para pesquisar no Datastore entidades que correspondam a critérios de pesquisa específicos denominados filtros. Por exemplo, uma aplicação que monitoriza vários livros de visitas pode usar uma consulta para obter mensagens de 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 do que outras. O arquivo de dados precisa de índices pré-criados para estas. Estes índices pré-criados são especificados num ficheiro de configuração, index.yaml. No servidor de programação, se executar uma consulta que precise de um índice que não especificou, o servidor de programação adiciona-o automaticamente ao respetivo index.yaml. No entanto, no seu Website, uma consulta que precisa de um índice ainda não especificado falha. Assim, o ciclo de desenvolvimento típico consiste em experimentar uma nova consulta no servidor de desenvolvimento e, em seguida, atualizar o Website para usar o index.yaml alterado automaticamente. Pode atualizar o index.yaml separadamente do carregamento da aplicação executando gcloud app deploy index.yaml. Se o seu arquivo de dados tiver muitas entidades, demora muito tempo a criar um novo índice para as mesmas. Neste caso, é aconselhável atualizar as definições de índice antes de carregar código que use o novo índice. Pode usar a consola de administração para saber quando os índices terminaram de ser criados.

O App Engine Datastore suporta nativamente filtros para: Correspondências exatas (o operador ==) e Comparações (os operadores <, <=, > e >=). Suporta a combinação de vários filtros através de uma operação booleana AND, com algumas limitações (veja abaixo).

Além dos operadores nativos, a API suporta o operador !=, a combinação de grupos de filtros através da operação booleana OR, e a operação IN, que testa a igualdade a um de uma lista de valores possíveis (como o operador "in" do Python). Estas operações não são mapeadas 1:1 para as operações nativas do Datastore. Por isso, são um pouco estranhas e lentas, relativamente. São implementadas através da união na memória de streams de resultados. Tenha em atenção que p != v é implementado como "p < v OR p > v". (Isto é importante para propriedades repetidas.)

Limitações: o Datastore aplica algumas restrições às consultas. A violação destas regras faz com que sejam geradas exceções. Por exemplo, a combinação de demasiados filtros, a utilização de desigualdades para várias propriedades ou a combinação de uma desigualdade com uma ordem de classificação numa propriedade diferente estão atualmente proibidas. Além disso, os filtros que fazem referência a várias propriedades requerem, por vezes, a configuração de índices secundários.

Não suportado: o Datastore não suporta diretamente correspondências de subcadeias de carateres, correspondências não sensíveis a maiúsculas e minúsculas nem a chamada pesquisa de texto integral. Existem formas de implementar correspondências não sensíveis a maiúsculas e minúsculas e até mesmo uma pesquisa de texto completo usando propriedades calculadas.

Filtrar por valores de propriedades

Recorde a classe Account de NDB Properties:

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

Normalmente, não quer obter todas as entidades de um determinado tipo; quer apenas as que têm um valor específico ou um intervalo de valores para alguma propriedade.

Os objetos de propriedade sobrecarregam alguns operadores para devolver expressões de filtro que podem ser usadas para controlar uma consulta: por exemplo, para encontrar todas as entidades Account cuja propriedade userid tem o valor exato 42, pode usar a expressão

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

(Se tiver a certeza de que existiu apenas um Account com esse userid, pode preferir usar userid como chave. Account.get_by_id(...) é mais rápido do que Account.query(...).get().)

O NDB suporta as seguintes operações:

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

Para filtrar uma desigualdade, pode usar uma sintaxe como a seguinte:

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

Esta consulta encontra todas as entidades Account cuja propriedade userid é superior ou igual a 40.

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

Pode especificar vários filtros:

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

Esta função combina os argumentos de filtro especificados, devolvendo todas as entidades Account cujo valor de userid é superior ou igual a 40 e inferior a 50.

Nota: Conforme mencionado anteriormente, o Datastore rejeita consultas que usam a filtragem de desigualdade em mais de uma propriedade.

Em vez de especificar um filtro de consulta completo numa única expressão, pode ser mais conveniente criá-lo passo a passo. 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. Tenha em atenção que os objetos de consulta são imutáveis, pelo que a construção de query2 não afeta query1 e a construção de query3 não afeta query1 nem query2.

As operações != e IN

Recupere a classe Article de NDB Properties:

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

As operações != (diferente de) e IN (pertence) são implementadas combinando outros filtros através da operação OR. O primeiro destes,

property != value

é implementado 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'))

Nota: Surpreendentemente, esta consulta não pesquisa entidades Article que não incluem "perl" como etiqueta! Em vez disso, encontra todas as entidades com, pelo menos, uma etiqueta diferente de "perl". Por exemplo, a seguinte entidade seria incluída nos resultados, apesar de ter "perl" como uma das respetivas etiquetas:

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

No entanto, este não seria incluído:

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

Não existe forma de consultar entidades que não incluam uma etiqueta igual a "perl".

Da mesma forma, a operação IN

property IN [value1, value2, ...]

que testa a associação numa lista de possíveis valores, é implementado 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'))

Nota: As consultas que usam OR removem duplicados dos respetivos resultados: o fluxo de resultados não inclui a entidade mais do que uma vez, mesmo que uma entidade corresponda a duas ou mais subconsultas.

Consultar propriedades repetidas

A classe Article definida na secção anterior também serve como exemplo de consulta de propriedades repetidas. Em particular, um filtro como

Article.tags == 'python'

usa um único valor, embora Article.tags seja uma propriedade repetida. Não pode comparar propriedades repetidas com objetos de lista (o Datastore não o compreende) e um filtro como

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

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

A consulta de um valor de None numa propriedade repetida tem um comportamento indefinido. Não o faça.

Combinar operações AND e OR

Pode aninhar operações AND e OR arbitrariamente. Por 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 do OR, uma consulta deste formulário que seja demasiado complexa pode falhar com uma exceção. É mais seguro normalizar estes filtros para que exista (no máximo) uma única operação OR na parte superior da árvore de expressões e um único nível de operações AND abaixo.

Para realizar esta normalização, tem de se lembrar das regras de lógica booleana e de como os filtros != e IN são efetivamente implementados:

  1. Expanda os operadores != e IN para a respetiva forma primitiva, em que != torna-se uma verificação da propriedade que é < ou > do que o valor e IN torna-se uma verificação da propriedade que é == ao primeiro valor ou ao segundo valor ou...até ao último valor na lista.
  2. Um AND com um OR no interior é equivalente a um OR de vários ANDs aplicados aos operandos AND originais, com um único operando OR substituído pelo OR original. Por exemplo, AND(a, b, OR(c, d)) é equivalente a OR(AND(a, b, c), AND(a, b, d))
  3. Um AND que tenha um operando que seja, em si mesmo, uma operação AND pode incorporar os operandos do AND aninhado no AND envolvente. Por exemplo, AND(a, b, AND(c, d)) é equivalente a AND(a, b, c, d)
  4. Um OR que tenha um operando que seja, em si mesmo, uma operação OR pode incorporar os operandos do OR aninhado no OR de inclusão. Por exemplo, OR(a, b, OR(c, d)) é equivalente a OR(a, b, c, d)

Se aplicarmos estas transformações em fases ao filtro de exemplo, usando uma notação mais simples do que a linguagem Python, obtemos:

  1. Usando a regra n.º 1 nos operadores IN e !=:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php',
             OR(tags < 'perl', tags > 'perl'))))
  2. Usar a regra n.º 2 no elemento OR mais interior aninhado num elemento AND:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         OR(AND(tags == 'php', tags < 'perl'),
            AND(tags == 'php', tags > 'perl'))))
  3. Usar a regra n.º 4 no elemento OR aninhado noutro elemento OR:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php', tags < 'perl'),
         AND(tags == 'php', tags > 'perl')))
  4. Usar a regra n.º 2 nos restantes OR aninhados num 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. Usar a regra n.º 3 para reduzir os restantes ANDs aninhados:
    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'))

Atenção: para alguns filtros, esta normalização pode causar uma explosão combinatória. Considere o AND de 3 OR cláusulas com 2 cláusulas básicas cada. Quando normalizado, isto torna-se um OR de 8 AND cláusulas com 3 cláusulas básicas cada: ou seja, 6 termos tornam-se 24.

Especificar ordens de ordenação

Pode usar o método order() para especificar a ordem em que uma consulta devolve os respetivos resultados. Este método recebe uma lista de argumentos, cada um dos quais é um objeto de propriedade (a ser ordenado por ordem ascendente) ou a sua negação (que indica a ordem descendente). Por exemplo:

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

Isto obtém todas as entidades Greeting, ordenadas pelo valor ascendente da respetiva propriedade content. As execuções de entidades consecutivas com a mesma propriedade de conteúdo são ordenadas pelo valor descendente da respetiva propriedade date. Pode usar várias chamadas order() para o mesmo efeito:

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

Nota: quando combina filtros com order(), o Datastore rejeita determinadas combinações. Em particular, quando usa um filtro de desigualdade, a primeira ordem de ordenação (se existir) tem de especificar a mesma propriedade que o filtro. Além disso, por vezes, tem de configurar um índice secundário.

Consultas predecessoras

As consultas de antecessores permitem-lhe fazer consultas fortemente consistentes ao arquivo de dados. No entanto, as entidades com o mesmo antecessor estão limitadas a 1 gravação por segundo. Segue-se uma comparação simples das concessões e da estrutura entre uma consulta de antepassados e não antepassados que usa clientes e as respetivas compras associadas no arquivo de dados.

No exemplo não ancestral seguinte, existe uma entidade no arquivo de dados para cada Customer e uma entidade no arquivo 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 que pertencem ao cliente, pode usar a seguinte consulta:

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

Neste caso, o arquivo de dados oferece um elevado débito de gravação, mas apenas consistência eventual. Se foi adicionada uma nova compra, pode receber dados desatualizados. Pode eliminar este comportamento através de consultas de antecessores.

Para clientes e compras com consultas de antepassados, continua a ter a mesma estrutura com duas entidades separadas. A parte do cliente é a mesma. No entanto, quando cria compras, já não precisa de especificar o KeyProperty() para compras. Isto acontece porque, quando usa consultas de hierarquia, chama a chave da entidade de cliente quando cria 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 sua própria chave. No entanto, cada chave de compra tem a chave da entidade de cliente incorporada. Lembre-se de que isto está limitado a uma gravação por antepassado por segundo. O seguinte cria uma entidade com um antecessor:

purchase = Purchase(parent=customer_entity.key)

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

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

Atributos de consulta

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

Atributo Tipo Predefinição Descrição
gentil str None Nome do tipo (normalmente, o nome da classe)
predecessor Key None Predecessor especificado para consulta
filtros FilterNode None Expressão de filtro
encomendas Order None Ordene encomendas

A impressão de um objeto de consulta (ou a chamada de str() ou repr() no mesmo) produz uma representação de string com formatação adequada:

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

Filtragem de valores de propriedades estruturadas

Uma consulta pode filtrar diretamente os valores dos campos de propriedades estruturadas. Por exemplo, uma consulta para todos os contactos com uma morada cuja cidade seja 'Amsterdam' teria o seguinte aspeto

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

Se combinar vários filtros deste tipo, os filtros podem corresponder a subentidades diferentes Address na mesma entidade de contacto. Por exemplo:

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

pode encontrar contactos com uma morada cuja cidade seja 'Amsterdam' e outra morada (diferente) cuja rua seja 'Spear St'. No entanto, pelo menos para filtros de igualdade, pode criar uma consulta que devolva apenas resultados com vários valores numa única subentidade:

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

Se usar esta técnica, as propriedades da subentidade iguais a None são ignoradas na consulta. Se uma propriedade tiver um valor predefinido, tem de a definir explicitamente como None para a ignorar na consulta. Caso contrário, a consulta inclui um filtro que exige que o valor da propriedade seja igual ao predefinido. Por exemplo, se o modelo Address tiver uma propriedade country com default='us', o exemplo acima só devolve contactos com o país igual a 'us'. Para considerar contactos com outros valores de país, tem de filtrar por Address(city='San Francisco', street='Spear St', country=None).

Se uma subentidade tiver valores de propriedade iguais a None, são ignorados. Assim, não faz sentido filtrar por um valor da propriedade da subentidade de None.

Usar propriedades com nome por string

Por vezes, quer filtrar ou ordenar uma consulta com base numa propriedade cujo nome é especificado por uma string. Por exemplo, se permitir que o utilizador introduza consultas de pesquisa como tags:python, seria conveniente transformar isso numa consulta como

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

Se o seu modelo for um Expando, o seu filtro pode usar GenericProperty, a classe Expando usa para propriedades dinâmicas:

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

A utilização de GenericProperty também funciona se o seu modelo não for um Expando, mas se quiser garantir que está a usar apenas nomes de propriedades definidos, também pode usar o atributo de classe _properties

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

ou use getattr() para o obter da turma:

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

A diferença é que getattr() usa o "nome Python" da propriedade, enquanto _properties é indexado pelo "nome da base de dados" da propriedade. Estas diferem apenas quando a propriedade foi declarada com algo como

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

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

Estas 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 consultas

Enquanto uma consulta está em curso, o respetivo estado é mantido num objeto iterador. (A maioria das aplicações não as usa diretamente; normalmente, é mais simples chamar fetch(20) do que manipular o objeto iterador.) Existem duas formas básicas de obter um objeto deste tipo:

  • usando a função iter() integrada do Python num objeto Query
  • chamar o método iter() do objeto Query

O primeiro suporta a utilização de um ciclo forPython (que chama implicitamente a função iter()) para percorrer uma consulta.

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

A segunda forma, através do método Query do objeto iter(), permite-lhe transmitir opções ao iterador para afetar o respetivo comportamento. Por exemplo, para usar uma consulta apenas de chaves num ciclo for, pode escrever o seguinte:

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

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

Método Descrição
__iter__() Parte do protocolo de iteradores do Python.
next() Devolve o resultado seguinte ou gera a exceção StopIteration se não existir nenhum.

has_next() Devolve True se uma chamada next() subsequente devolver um resultado, False se gerar StopIteration.

Bloqueia até que a resposta a esta pergunta seja conhecida e armazena o resultado em buffer (se existir) até o obter com next().
probably_has_next() Semelhante ao has_next(), mas usa um atalho mais rápido (e, por vezes, impreciso).

Pode devolver um falso positivo (True quando next() geraria realmente StopIteration), mas nunca um falso negativo (False quando next() devolveria realmente um resultado).
cursor_before() Devolve um cursor de consulta que representa um ponto imediatamente antes do último resultado devolvido.

Gera uma exceção se não estiver disponível nenhum cursor (em particular, se a opção de consulta produce_cursors não tiver sido transmitida).
cursor_after() Devolve um cursor de consulta que representa um ponto imediatamente após o último resultado devolvido.

Gera uma exceção se não estiver disponível nenhum cursor (em particular, se a opção de consulta produce_cursors não tiver sido transmitida).
index_list() Devolve 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 retoma numa consulta. Isto é útil para mostrar ao utilizador uma página de resultados de cada vez. Também é útil para processar tarefas longas que podem ter de ser interrompidas e retomadas. Uma forma típica de os usar é com o método fetch_page() de uma consulta. Funciona de forma semelhante a fetch(), mas devolve um triplo (results, cursor, more). O indicador more devolvido indica que existem provavelmente mais resultados. Uma IU pode usar isto, por exemplo, para suprimir um botão ou um link "Página seguinte". Para pedir páginas subsequentes, transmita o cursor devolvido por uma chamada fetch_page() para a seguinte. É gerado um BadArgumentError se transmitir um cursor inválido. Tenha em atenção que a validação apenas verifica se o valor está codificado em base64. Tem de fazer qualquer validação adicional necessária.

Assim, para permitir que o utilizador veja todas as entidades que correspondem a uma consulta, obtendo-as uma página de cada vez, o seu código pode ter o seguinte aspeto:

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>')

Tenha em atenção a utilização de urlsafe() e Cursor(urlsafe=s) para serializar e desserializar o cursor. Isto permite-lhe transmitir um cursor a um cliente na Web na resposta a um pedido e recebê-lo de volta do cliente num pedido posterior.

Nota: Normalmente, o método fetch_page() devolve um cursor mesmo que não existam mais resultados, mas isto não é garantido: o valor do cursor devolvido pode ser None. Tenha também em atenção que, como a flag more é implementada através do método probably_has_next() do iterador, em circunstâncias raras, pode devolver True, mesmo que a página seguinte esteja vazia.

Algumas consultas NDB não suportam cursores de consulta, mas pode corrigi-las. Se uma consulta usar IN, OR ou !=, os resultados da consulta não funcionam com cursores a menos que sejam ordenados por chave. Se uma aplicação não ordenar os resultados por chave e chamar fetch_page(), 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 percorrer os resultados da consulta, pode usar o método iter() de uma consulta para obter um cursor num ponto preciso. Para o fazer, passe produce_cursors=True para iter(); quando o iterador estiver no local certo, chame o respetivo cursor_after() para obter um cursor que esteja imediatamente a seguir. (Em alternativa, ligue cursor_before() para um cursor imediatamente antes.) Tenha em atenção que chamar cursor_after() ou cursor_before() pode fazer uma chamada de bloqueio do Datastore, executando novamente parte da consulta para extrair um cursor que aponta para o meio de um lote.

Para usar um cursor para retroceder nas 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)

Chamar uma função para cada entidade ("Mapeamento")

Suponhamos que precisa de obter as entidades Account correspondentes às entidades Message devolvidas por uma consulta. Pode escrever algo como:

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, isto é bastante ineficiente: aguarda a obtenção de uma entidade e, em seguida, usa a entidade; aguarda a próxima entidade e usa a entidade. Existe muito tempo de espera. Outra forma é escrever uma função de chamada de retorno mapeada sobre os 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.

Esta versão é executada um pouco mais rapidamente do que o ciclo simples for acima, porque é possível alguma concorrência. No entanto, uma vez que a chamada get() em callback() continua a ser síncrona, o ganho não é enorme. Este é um bom local para usar obtenções assíncronas.

GQL

O GQL é uma linguagem semelhante a SQL para obter entidades ou chaves do App Engine Datastore. Embora as funcionalidades do GQL sejam diferentes das de uma linguagem de consulta para uma base de dados relacional tradicional, a sintaxe do GQL é semelhante à do SQL. A sintaxe GQL é descrita na Referência GQL.

Pode usar a GQL para criar consultas. Isto é semelhante à criação de uma consulta com Model.query(), mas usa a sintaxe GQL para definir o filtro e a ordem da consulta. Para utilizar a aplicação:

  • ndb.gql(querystring) devolve um objeto Query (o mesmo tipo que o devolvido por Model.query()). Todos os métodos habituais estão disponíveis nesses objetos Query: fetch(), map_async(), filter(), etc.
  • Model.gql(querystring) é uma abreviatura 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, pode usar foo.bar na sintaxe GQL para fazer referência a subpropriedades.
  • O GQL suporta associações de parâmetros semelhantes a SQL. Uma aplicação pode definir uma consulta e, em seguida, associar-lhe valores:
    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 devolve uma nova consulta; não altera a original.

  • Se a classe do modelo substituir o método de classe _get_kind(), a sua consulta GQL deve usar o tipo devolvido por essa função e não o nome da classe.
  • Se uma propriedade no seu modelo substituir o respetivo nome (por exemplo, foo = StringProperty('bar')) a consulta GQL deve usar o nome da propriedade substituído (no exemplo, bar).

Use sempre a funcionalidade de associação de parâmetros se alguns valores na sua consulta forem variáveis fornecidas pelo utilizador. Isto evita ataques baseados em hacks sintáticos.

É um erro consultar um modelo que não foi importado (ou, mais geralmente, definido).

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

A especificação de um limite ou um desvio para o fetch() da consulta substitui o limite ou o desvio definido pelas cláusulas OFFSET e LIMIT do GQL. Não combine OFFSET e LIMIT do GQL com fetch_page() Tenha em atenção que o máximo de 1000 resultados imposto pelo App Engine nas consultas aplica-se ao deslocamento e ao limite.

Se estiver habituado ao SQL, tenha cuidado com as falsas suposições quando usar o GQL. O GQL é traduzido para a API de consulta nativa do NDB. Isto é diferente de um mapeador relacional de objetos típico (como o SQLAlchemy ou o suporte de base de dados do Django), em que as chamadas API são traduzidas em SQL antes de serem transmitidas para o servidor de base de dados. O GQL não suporta modificações do Datastore (inserções, eliminações ou atualizações); apenas suporta consultas.