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.
Informações gerais
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:
...
...
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:
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
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:
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:
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:
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:
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,
é equivalente a
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:
No entanto, esta não seria incluída:
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,
é equivalente a
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
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:
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:
- Expanda os operadores
!=
eIN
para a forma primitiva, em que!=
se torna uma verificação para a propriedade que é <or> do que o valor eIN
se torna uma verificação para a propriedade que está sendo == ao primeiro ou ao segundo valor ou até o último valor da lista. - Um
AND
comOR
dentro dele é equivalente aOR
de váriosAND
s aplicados aos operandosAND
originais, com um único operandoOR
substituído pelo originalOR
. Por exemplo,AND(a, b, OR(c, d))
é equivalente aOR(AND(a, b, c), AND(a, b, d))
. - Um
AND
que tem um operando que é uma operaçãoAND
pode incorporar os operandos doAND
aninhado noAND
delimitador. Por exemplo,AND(a, b, AND(c, d))
é equivalente aAND(a, b, c, d)
. - Um
OR
que tem um operando que é uma operaçãoOR
pode incorporar os operandos doOR
aninhado noOR
delimitador. Por exemplo,OR(a, b, OR(c, d))
é equivalente aOR(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:
- Usando a regra nº 1 em
IN
e!=
:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', OR(tags < 'perl', tags > 'perl'))))
- Usando a regra nº 2 no
OR
mais interno aninhado em umAND
:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', OR(AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl'))))
- Usando a regra nº 4 no
OR
aninhado em outroOR
:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl')))
- Usando a regra nº 2 no restante do
OR
aninhado em umAND
: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')))
- 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:
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:
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.
Para encontrar todas as compras pertencentes ao cliente, você pode usar a seguinte consulta:
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.
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:
Para consultar as compras de um determinado cliente, use a consulta a seguir.
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:
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
Se você combinar vários desses filtros, eles poderão corresponder a diferentes subentidades de Address
na mesma entidade Contact.
Exemplo:
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:
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:
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
.
ou use getattr()
para recebê-lo da classe:
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:
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:
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 objetoQuery
; - chamando o método
iter()
do objetoQuery
.
O primeiro aceita o uso de uma repetição for
do Python (que implicitamente chama a função iter()
) para repetir uma consulta.
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:
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:
...
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:
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:
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:
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 objetoQuery
(o mesmo tipo retornado porModel.query()
). Todos os métodos comuns estão disponíveis nesses objetosQuery
:fetch()
,map_async()
,filter()
etc.Model.gql(querystring)
é uma forma abreviada dendb.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:
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.