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 precise de um índice que você não especificou, o servidor de desenvolvimento a adicionará automaticamente ao index.yaml correspondente. 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 OR booleana e a operação IN, que testa a igualdade em relação a uma lista de valores possíveis (como o operador 'in' 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 é implementada 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 fazem referência 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 tiver certeza de que havia apenas uma Account com esse userid, talvez convenha 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 de 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 pela combinação de 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 de exemplo de consulta de 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 procurar entidades Article que têm valor de tag igual à lista ['python', 'ruby', 'php']: ele procura entidades com o valor tags (consideradas como uma lista) contendo pelo menos um desses valores.

Consultar um valor None em uma propriedade repetida gera 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 realizar esta normalização, é preciso lembrar das regras da lógica booleana e de como os filtros != e IN são efetivamente implementados:

  1. Expanda os operadores != e IN para o formato primitivo, em que != torna-se uma procura pela propriedade < ou > que o valor, e IN torna-se uma procura pela propriedade == ao primeiro valor ou o segundo valor ou ... até o último valor da lista.
  2. Um AND com um OR dentro é 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 tem um operando que é uma operação AND pode incorporar os operandos do AND aninhado no AND de inclusão. 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 de inclusão. 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 nos operadores 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

Você pode usar o método order() para especificar a ordem em que uma consulta retorna 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 respectiva propriedade content. Execuções de entidades consecutivas com a mesma propriedade de conteúdo serão classificadas pelo valor decrescente da respectiva propriedade date. Você pode 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 de 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:

Atributo Tipo Padrão Descrição
kind str None Nome do tipo (geralmente o nome da classe)
ancestor Key None Ancestral especificado para a consulta
filters FilterNode None Expressão de filtro
orders Order 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 por todos os contatos com um endereço que contém a cidade 'Amsterdam' seria assim:

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 contendo a cidade 'Amsterdam' e outro endereço (diferente) com a rua 'Spear St'. No entanto, pelo menos para filtros de igualdade, você pode 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 iguais a None serão ignoradas 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 propriedade country 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 pelo valor None de propriedade de subentidade.

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, será conveniente transformá-las 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 é usada para propriedades dinâmicas:

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

O uso de GenericProperty também funciona quando o modelo não é um Expando, mas se você quiser assegurar o uso apenas de nomes de propriedade definidos, também poderá usar 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 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 utiliza 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 integrada iter() 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, usar o método iter() do objeto Query, permite passar para o iterador opções que afetam o comportamento dele. Por exemplo, para usar uma consulta somente de chaves em uma repetição for, você pode escrever 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 quando uma chamada next() subsequente retorna um resultado, False quando gera StopIteration.

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

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

Gera uma exceção quando nenhum cursor está disponível (especialmente quando a opção de consulta produce_cursors não foi passada).
cursor_after() Retorna um cursor de consulta que representa um ponto logo após o último resultado retornado.

Gera uma exceção quando nenhum cursor está disponível (especialmente quando a opção de consulta produce_cursors não foi passada).
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 típica de usá-los é com o método fetch_page() de uma consulta. Ele funciona semelhante a fetch(), mas retorna um triplo (results, cursor, more). A sinalização more retornada indica que provavelmente há mais resultados. Uma IU pode usá-la, 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 é gerado quando você passa 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 o receba de volta do cliente em uma solicitação posterior.

Observação: o método fetch_page() costuma retornar 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 a sinalização more é implementada usando o método probably_has_next() do iterador, em raras circunstâncias ele 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 usa IN, OR ou !=, então os resultados da consulta não funcionarão com cursores, a menos que ordenados por chave. Se um aplicativo não pedir os resultados por chave e chamar fetch_page(), ele receberá 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" os resultados da consulta, você pode usar o método iter() de uma consulta para receber um cursor em um ponto preciso. Para fazer isso, passe produce_cursors=True para iter(). Quando o iterador estiver no lugar certo, chame o respectivo cursor_after() para receber um cursor que está imediatamente depois dele. Ou, da mesma forma, chame cursor_before() para um cursor que está imediatamente antes dele. Observe que chamar cursor_after() ou cursor_before() pode fazer uma chamada de bloqueio do Datastore, reexecutando 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 é tão grande. Este é um bom lugar para usar gets assíncronos.

GQL

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

Você pode usar o GQL para construir consultas. O processo é semelhante a criar uma consulta com Model.query(), mas usa a sintaxe do GQL para definir o filtro e a ordem da consulta. Para usá-lo:

  • ndb.gql(querystring) retorna um objetoQuery (o mesmo tipo retornado por Model.query()). Todos os métodos usuais estão disponíveis nestes objetos Query: fetch(), map_async(), filter() etc.
  • Model.gql(querystring) é uma abreviação de ndb.gql("SELECT * FROM Model " + querystring). Comumente, querystring é algo como "WHERE prop1 > 0 AND prop2 = TRUE".
  • Para consultar modelos que contêm propriedades estruturadas, você pode usar foo.bar na sintaxe do GQL para fazer referência 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 do modelo modificar o nome dele (por exemplo, foo = StringProperty('bar')), a consulta GQL precisará usar o nome de propriedade modificado (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.

A especificação de um limite ou desvio para o fetch() da consulta modifica o limite ou desvio definido pelas cláusulas OFFSET e LIMIT do GQL. Não combine OFFSET e LIMIT do GQL com fetch_page(). Observe que o máximo de 1.000 resultados imposto pelo App Engine para consultas se aplica ao desvio 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.

Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…

Ambiente padrão do App Engine para Python 2