Una aplicación puede usar consultas para buscar en Datastore entidades que coincidan con los criterios de búsqueda específicos, llamados filtros.
Descripción general
Una aplicación puede usar consultas para buscar en Datastore entidades que coincidan con los criterios de búsqueda específicos, llamados filtros. Por ejemplo, una aplicación que realiza el seguimiento de varios libros de visitas podría utilizar una consulta para recuperar mensajes de un libro de visitas ordenados por fecha:
...
...
Algunas consultas son más complejas que otras, y el almacén de datos necesita índices previamente compilados para esas consultas.
Estos índices compilados con anterioridad se especifican en un archivo de configuración, index.yaml
.
Si ejecutas una consulta en el servidor de desarrollo que necesite un índice que no especificaste, el servidor de desarrollo lo agrega automáticamente a su archivo index.yaml
.
Sin embargo, una consulta que necesita un índice aún no especificado fallaría en tu sitio web.
Por lo tanto, el ciclo de desarrollo típico consta de ejecutar una consulta nueva en el servidor de desarrollo y, luego, actualizar el sitio web para usar el archivo index.yaml
que cambia de forma automática.
Puedes actualizar index.yaml
por separado cuando cargas la aplicación si ejecutas gcloud app deploy index.yaml
.
Si el almacén de datos tiene demasiadas entidades, llevará mucho tiempo crear un índice nuevo para ellas. En ese caso, lo más acertado es actualizar las definiciones del índice antes de subir el código que utiliza el índice nuevo.
Puedes usar la Consola del administrador para saber cuándo se terminaron de compilar los índices.
App Engine Datastore admite de forma nativa filtros para las coincidencias exactas (el operador ==) y las comparaciones (los operadores <, <=, > y >=).
Admite la combinación de varios filtros mediante el uso de una operación booleana AND
, pero con algunas limitaciones (ver a continuación).
Además de los operadores nativos, la API admite el operador !=
, y combina grupos de filtros que usan la operación booleana OR
y la operación IN
, las cuales prueban la igualdad a un valor de una lista de valores posibles (como el operador “in” de Python).
Estas operaciones no asignan una relación de 1:1 con las operaciones nativas de Datastore; por lo tanto, son poco convencionales y relativamente lentas.
Se implementan mediante la combinación en la memoria de transmisiones de resultados. Ten en cuenta que p != v
se implementa como “p < v O p > v”.
(Esto es importante para las propiedades repetidas).
Limitaciones: Datastore aplica algunas restricciones en las consultas. Infringir estas restricciones puede generar excepciones. Por ejemplo, actualmente no está permitido combinar demasiados filtros, usar desigualdades para varias propiedades o combinar una desigualdad con un orden de clasificación en una propiedad diferente. Además, a veces, los filtros que hacen referencia a varias propiedades requieren la configuración de índices secundarios.
No compatible: Datastore no admite directamente coincidencias de subcadena, coincidencias con distinción entre mayúsculas y minúsculas, o la llamada búsqueda de texto completo. Existen maneras de implementar las coincidencias con distinción entre mayúsculas y minúsculas, y hasta la búsqueda de texto completo mediante el uso de propiedades calculadas.
Cómo filtrar por valores de propiedad
Recupera la clase Account de las propiedades de NDB:
Generalmente, no quieres recuperar todas las entidades de un tipo determinado; solo necesitas las que tienen un valor o un rango de valores específico para algunas propiedades.
Los objetos de propiedad sobrecargan algunos operadores para mostrar expresiones de filtro que se pueden utilizar con el fin de controlar una consulta: por ejemplo, para buscar todas las entidades Cuenta cuya propiedad userid tiene el valor exacto de 42, puedes usar la expresión
(Si estás seguro de que solo había una Account
con ese userid
, es posible que prefieras usar userid
como clave.
Account.get_by_id(...)
es más rápido que Account.query(...).get()
).
NDB admite estas operaciones:
property == value
property < value
property <= value
property > value
property >= value
property != value
property.IN([value1, value2])
Para filtrar por desigualdad, puedes usar la sintaxis que se muestra a continuación:
De esta forma, busca todas las entidades de Cuenta con una propiedad userid
mayor o igual que 40.
Dos de estas operaciones, IN y !=, se implementan como combinaciones de las otras y son un poco inestables, como se describe en IN y !=.
Puedes especificar varios filtros:
Esto combina los argumentos de filtro especificados, que muestran todas las entidades Cuenta cuyo valor userid es mayor o igual que 40 o menor que 50.
Nota: Como se mencionó anteriormente, Datastore rechaza las consultas que utilizan el filtro de desigualdades en más de una propiedad.
En vez de especificar un filtro de consulta completo en una sola expresión, es posible que sea más conveniente compilarlo en etapas. Por ejemplo:
query3
es equivalente a la variable query
del ejemplo anterior. Ten en cuenta que los objetos de consulta son inmutables, por lo que la construcción de query2
no afecta a query1
y la construcción de query3
no afecta a query1
ni a query2
.
Las operaciones IN y !=
Recupera la clase Article de las propiedades de NDB:
Las operaciones IN
(membresía) y !=
(no igual) se implementan mediante la combinación de otros filtros que usan la operación OR
. La primera,
property != value
se implementa como
(property < value) OR (property > value)
Por ejemplo,
es equivalente a
Nota: Contrario a lo esperado, esta consulta no busca entidades Article
que no incluyan la etiqueta “perl”.
En cambio, busca todas las entidades con una etiqueta distinta de “perl”.
Por ejemplo, la siguiente entidad se incluiría en los resultados, aunque tenga una etiqueta “perl”:
Sin embargo, esta no se incluirá:
No existe una manera para hacer una consulta a entidades que no tengan una etiqueta igual a “perl”.
Lo mismo sucede en la operación IN
property IN [value1, value2, ...]
que prueba membresías en una lista de valores posibles y se implementa como
(property == value1) OR (property == value2) OR ...
Por ejemplo,
es equivalente a
Nota: Las consultas que usan OR
deduplican sus resultados, es decir, la transmisión de resultados no incluye una entidad más de una vez, incluso si una de ellas coincide con dos o más subconsultas.
Hacer consultas para propiedades repetidas
La clase Article
definida en la sección anterior también sirve como ejemplo de consulta de las propiedades repetidas. En particular, un filtro como
usa un solo valor, aunque Article.tags
sea una propiedad repetida. No puedes comparar propiedades repetidas para mostrar la lista de objetos (Datastore no lo comprendería), y un filtro como
realiza algo completamente diferente de la búsqueda de entidades Article
, cuyo valor de etiquetas es la lista ['python', 'ruby', 'php']
: busca entidades cuyo valor tags
(considerado como una lista) contenga por lo menos uno de esos valores.
La consulta de un valor de None
en una propiedad repetida tiene un comportamiento indefinido. No hagas esto.
Combinar operaciones AND y OR
Puedes anidar operaciones AND
y OR
de manera arbitraria.
Por ejemplo:
Sin embargo, debido a la implementación de OR
, una consulta con esta forma que es muy compleja puede fallar con una excepción. La manera más segura es normalizar estos filtros para que haya (como máximo) una operación OR
en la parte superior del árbol de expresiones, y un solo nivel de operaciones AND
debajo.
Para realizar esta normalización, debes recordar las reglas de la lógica booleana y la manera en la que se implementan los filtros !=
y IN
:
- Expande los operadores
!=
yIN
a su forma original, en la que!=
se convierte en una verificación de si la propiedad es menor o mayor que el valor, mientras queIN
se convierte en una verificación de si la propiedad es igual al primer valor, al segundo valor, y así hasta el último valor de la lista. - Un
AND
que contiene unOR
es equivalente a unOR
de variosAND
aplicados a los operandosAND
originales, con un solo operandoOR
sustituido por elOR
original. Por ejemplo,AND(a, b, OR(c, d))
es equivalente aOR(AND(a, b, c), AND(a, b, d))
. - Una operación
AND
que tiene un operando que es una operaciónAND
en sí mismo puede incorporar los operandos deAND
anidada enAND
entre paréntesis. Por ejemplo,AND(a, b, AND(c, d))
es equivalente aAND(a, b, c, d)
. - Una operación
OR
que tiene un operando que es una operaciónOR
en sí mismo puede incorporar los operandos deOR
anidada enOR
entre paréntesis. Por ejemplo,OR(a, b, OR(c, d))
es equivalente aOR(a, b, c, d)
.
Si aplicamos estas transformaciones en etapas al filtro de ejemplo, con una notación más simple que Python, obtienes lo siguiente:
- Mediante el uso de la regla n.º 1 en los operadores
!=
yIN
:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', OR(tags < 'perl', tags > 'perl'))))
- Mediante el uso de la regla n.º 2 en la operación
OR
más interna anidada dentro deAND
:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', OR(AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl'))))
- Mediante el uso de la regla n.º 4 en
OR
anidada dentro de otraOR
:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl')))
- Mediante el uso de la regla n.º 2 en
OR
restante anidada dentro deAND
: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')))
- Mediante el uso de la regla n.º 3 para contraer las
AND
anidadas 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'))
Precaución: En algunos filtros, esta normalización puede generar una explosión combinatoria. Considera la operación AND
de 3 cláusulas OR
con 2 cláusulas básicas cada una.
En la normalización, esto se convierte en OR
de 8 cláusulas AND
con 3 cláusulas básicas cada una: es decir, 6 términos se convierten en 24.
Especificar órdenes de clasificación
Puedes usar el método order()
para especificar el orden en el que una consulta muestra sus resultados. Este método toma una lista de argumentos, en la que cada uno es un objeto de propiedad (para clasificarse en orden ascendente) o su negación (denota clasificación descendente). Por ejemplo:
Esto recupera todas las entidades Greeting
, ordenadas por el valor ascendente de su propiedad content
.
Las ejecuciones de entidades consecutivas con la misma propiedad de contenido se clasificarán por el valor descendente de su propiedad date
.
Puedes usar varias llamadas order()
con el mismo propósito:
Nota: Cuando combinas filtros con order()
, Datastore rechaza ciertas combinaciones.
Particularmente, cuando utilices un filtro de desigualdad, el primer orden de clasificación (si existe) debe especificar la misma propiedad que el filtro.
Además, a veces, deberás configurar un índice secundario.
Consultas principales
Las consultas principales te permiten realizar consultas de coherencia sólida al almacén de datos. Sin embargo, las entidades con la misma consulta principal están limitadas a 1 escritura por segundo. A continuación, se muestra una comparación simple de la compensación y estructura entre una consulta principal y una no principal mediante clientes y las compras asociadas en el almacén de datos.
En el siguiente ejemplo de una consulta no principal, hay una entidad en el almacén de datos para cada Customer
y una entidad en el almacén de datos para cada Purchase
, con un KeyProperty
que apunta al cliente.
Para buscar todas las compras que pertenecen al cliente, puedes usar la siguiente consulta:
En este caso, el almacén de datos ofrece una capacidad de procesamiento de escritura alta, pero solo coherencia eventual. Si se agregó una compra nueva, es posible que obtengas datos obsoletos. Puedes erradicar este comportamiento con consultas principales.
Para clientes y compras con consultas principales, sigues teniendo la misma estructura con dos entidades separadas. La parte de cliente es la misma. Sin embargo, cuando creas compras, ya no necesitas especificar la KeyProperty()
de las compras. Esto se debe a que cuando utilizas consultas principales, llamas a la clave de entidad del cliente mediante la creación de la de entidad compra.
Cada compra y cliente tienen sus propias claves. Sin embargo, cada clave de compra incorporará una clave de customer_entity. Recuerda que esto se limitará a una escritura por consulta principal por segundo. A continuación, se crea una entidad con una consulta principal:
Para obtener información sobre las compras de un cliente dado, utiliza la siguiente consulta:
Atributos de consultas
Los objetos de las consultas tienen los siguientes atributos de datos de solo lectura:
Atributo | Tipo | Predeterminado | Descripción |
---|---|---|---|
tipo | str | None | Tipo de nombre (generalmente nombre de clase) |
principal | Key | None | Clave principal especificada para la consulta |
filtros | FilterNode | None | Expresión de filtro |
órdenes | Order | None | Órdenes de clasificación |
Imprimir un objeto de consulta (o llamar str()
o repr()
en esta) produce una representación de string con un buen formato:
Filtra por valores de propiedad estructurados
Una consulta puede filtrar directamente por los valores de campo de las propiedades estructuradas.
Por ejemplo, una consulta para todos los contactos con una dirección cuya ciudad es 'Amsterdam'
se vería de esta forma:
Si combinas varios de estos filtros, es posible que los filtros coincidan con diferentes subentidades Address
dentro de la misma entidad de contacto.
Por ejemplo:
Es posible que encuentre contactos con una dirección cuya ciudad es 'Amsterdam'
y otra dirección (diferente) cuya calle es 'Spear St'
. Sin embargo, al menos para los filtros de igualdad, puedes crear una consulta que muestre solo los resultados con varios valores en una sola subentidad:
Si utilizas esta técnica, las propiedades de la subentidad igual a None
se ignoran en la consulta.
Si una propiedad tiene un valor predeterminado, debes configurarlo de forma explícita como None
para ignorarlo en la consulta. De lo contrario, la consulta incluirá un filtro en el que el valor de propiedad será igual al predeterminado.
Por ejemplo, si el modelo Address
tuviera una propiedad country
con default='us'
, el ejemplo anterior solo mostraría contactos cuyo país fuera igual a 'us'
. Para tener en cuenta a los contactos con otros valores de países, deberás filtrar por Address(city='San Francisco', street='Spear St',
country=None)
.
Si una subentidad tiene valores de propiedad iguales a None
, se ignorarán. Por lo tanto, no tiene sentido filtrar por un valor de propiedad de subentidad de None
.
Usar propiedades nombradas por cadenas
A veces, quieres ordenar o filtrar una consulta con base en una propiedad, cuyo nombre está especificado por una cadena. Por ejemplo, si permites que el usuario ingrese búsquedas, como tags:python
, sería conveniente convertirlo de algún modo en una consulta, como la siguiente:
Article.query(Article."tags" == "python") # does NOT work
Si tu modelo es un Expando
, tu filtro puede usar GenericProperty
, la clase que Expando
usa para las propiedades dinámicas:
El uso de GenericProperty
también funciona si tu modelo no es un Expando
, pero si quieres asegurarte de solo usar nombres definidos de propiedades, también puedes usar el atributo de clase _properties
.
También puedes utilizar getattr()
para obtenerlo de la clase:
La diferencia es que getattr()
utiliza el "nombre de Python" de la propiedad, mientras que _properties
se indexa por el "nombre del almacén de datos" de la propiedad. Esto solo difiere cuando la propiedad fue declarada con algo como
Aquí, el nombre de Python es title
, pero el nombre del almacén de datos es t
.
Estos enfoques también funcionan para ordenar resultados de consultas:
Iteradores de consulta
Mientras una consulta está en curso, su estado se encuentra en un objeto iterador. (La mayoría de las aplicaciones no las usarán directamente; por lo general, llamar a fetch(20)
es más directo que manipular el objeto iterador).
Existen dos maneras básicas de obtener ese objeto:
- Mediante el uso de la función
iter()
integrada de Python en un objetoQuery
. - Mediante una llamada al método
iter()
del objetoQuery
.
La primera opción admite el uso de un for
de Python (que implícitamente llama a la función iter()
) para aplicar el bucle en una consulta.
La segunda opción, que usa el método iter()
del objeto Query
, te permite pasar opciones al iterador a fin de modificar su comportamiento. Por ejemplo, para usar una consulta de solo clave en un bucle for
, puedes escribir lo siguiente:
Los iteradores de consulta tienen otros métodos útiles:
Método | Descripción |
---|---|
__iter__()
| Parte del protocolo de iterador de Python. |
next()
| Muestra el siguiente resultado o genera una excepción StopIteration si no existe uno. |
has_next()
| Muestra True si una llamada posterior a next() mostrará un resultado y False si generará StopIteration .Se bloquea hasta que se conozca la respuesta a esta pregunta y almacena el resultado (si existe) hasta que lo recuperes con next() .
|
probably_has_next()
| Es como has_next() , pero usa un acceso directo más rápido (y, a veces, impreciso).Puede mostrar un falso positivo ( True cuando next() generaría StopIteration ), pero nunca un falso negativo (False cuando next() generaría un resultado).
|
cursor_before()
| Muestra un cursor de consulta que representa un punto justo antes del último resultado mostrado. Genera una excepción si no hay un cursor disponible (en particular, si no se pasó la opción de consulta produce_cursors ).
|
cursor_after()
| Muestra un cursor de consulta que representa un punto justo después del último resultado mostrado. Genera una excepción si no hay un cursor disponible (en particular, si no se pasó la opción de consulta produce_cursors ).
|
index_list()
| Muestra una lista de índices usados por una consulta ejecutada, incluidos los índices principales, compuestos, de tipo y con una sola propiedad. |
Cursores de consulta
Un cursor de consulta es una pequeña estructura de datos opacos que representa un punto de reanudación en una consulta. Es útil para mostrarle al usuario una página de resultados a la vez. Además, permite manejar trabajos largos que se deben detener y reanudar.
Una forma típica de usarlos es con el método fetch_page()
de una consulta.
Funciona en cierta forma como fetch()
, pero muestra un (results, cursor, more)
triple.
La marca more
que se muestra indica que probablemente haya más resultados. Una IU puede usar esto, por ejemplo, para suprimir un vínculo o un botón de "Página siguiente".
Para solicitar las páginas siguientes, pasa el cursor que mostró una llamada fetch_page()
a la siguiente. Se genera un BadArgumentError
si pasas un cursor que no sea válido. Ten en cuenta que la validación solo verifica si el valor es codificación base64. Tendrás que hacer las validaciones adicionales que sean necesarias.
Por lo tanto, para que el usuario pueda ver todas las entidades que coinciden con una consulta, de a una página a la vez, el código debe ser de la siguiente manera:
...
Ten en cuenta el uso de urlsafe()
y Cursor(urlsafe=s)
para serializar y deserializar el cursor.
Esto te permite pasar un cursor a un cliente en la Web como respuesta a una solicitud y volver a recibirlo del cliente en otra consulta.
Nota: Por lo general, el método fetch_page()
muestra un cursor incluso si no hay más resultados, pero esto no está garantizado debido a que el valor del cursor que se muestra puede ser None
. Ten en cuenta también que, debido a que la marca more
se implementa mediante el método probably_has_next()
del iterador, en circunstancias excepcionales puede mostrar True
aunque la página siguiente esté vacía.
Algunas consultas de NDB no admiten cursores de consulta, pero puedes corregirlos.
Si una consulta usa IN
, OR
o !=
, los resultados de la consulta no funcionarán con los cursores, a menos que se ordenen por clave.
Si una aplicación no ordena los resultados por clave y llama a fetch_page()
, obtiene un BadArgumentError
.
Si
User.query(User.name.IN(['Joe', 'Jane'])).order(User.name).fetch_page(N)
recibe un error, cámbialo a
User.query(User.name.IN(['Joe', 'Jane'])).order(User.name, User.key).fetch_page(N)
.
En vez de “paginar” los resultados de consulta, puedes usar el método iter()
de una consulta a fin de obtener un cursor en un punto preciso.
Para ello, pasa produce_cursors=True
a iter()
; cuando el iterador esté en el lugar correcto, llama a su cursor_after()
a fin de obtener un cursor que está justo después de eso. (De la misma manera, llama a cursor_before()
para obtener el cursor anterior).
Ten en cuenta que si llamas a cursor_after()
o a cursor_before()
, puede generar una llamada de bloqueo de Datastore, lo que volvería a ejecutar parte de la consulta con el fin de extraer un cursor que apunte al centro de un lote.
Con el fin de utilizar un cursor para ir a la página anterior a través de los resultados de la consulta, crea una consulta inversa:
Llamar a una función para cada entidad ("Asignar")
Supongamos que necesitas obtener las entidades Account
correspondientes a las entidades Message
que muestra una consulta.
Puedes escribir lo siguiente:
Sin embargo, no es tan eficiente. Espera para recuperar una entidad; luego, la utiliza; después, espera a la siguiente entidad y la utiliza. Hay mucho tiempo de espera. Otra forma es escribir una función de devolución de llamada que está asignada a los resultados de la consulta:
Esta versión se ejecutará más rápido que el bucle for
anterior, ya que es posible alguna concurrencia.
Sin embargo, dado que la llamada get()
en callback()
sigue siendo síncrona, la ganancia no es demasiada.
Este es un muy buen lugar para usar gets asíncronos.
GQL
GQL es un lenguaje similar a SQL para recuperar entidades o claves desde App Engine Datastore. Si bien las funciones de GQL son diferentes de las de un lenguaje de consulta para una base de datos relacional tradicional, su sintaxis es similar a la de SQL. La sintaxis de GQL se describe en la Referencia de GQL.
Puedes usar GQL para crear consultas. Esto es similar a crear una consulta con Model.query()
, pero con la sintaxis de GQL para definir el orden y el filtro de la consulta. Para usarlo, realiza los pasos siguientes:
ndb.gql(querystring)
muestra un objetoQuery
(el mismo tipo que muestraModel.query()
). Todos los métodos habituales están disponibles en estos objetosQuery
:fetch()
,map_async()
,filter()
y demás.Model.gql(querystring)
es una abreviatura dendb.gql("SELECT * FROM Model " + querystring)
. Por lo general, querystring es similar a"WHERE prop1 > 0 AND prop2 = TRUE"
.- Si deseas consultar modelos que contienen propiedades estructuradas, puedes usar
foo.bar
en la sintaxis de GQL para hacer referencia a estas subpropiedades. - GQL admite vinculaciones de parámetros similares a los de SQL. Una aplicación puede definir una consulta y, luego, vincular valores en ella:
Llamar a una función de consulta
bind()
muestra una consulta nueva; no modifica la original. - Si tu clase de modelo anula el método de clase
_get_kind()
, la consulta de GQL debe utilizar el tipo que muestra esa función, no el nombre de la clase. - Si una propiedad del modelo anula el nombre (p. ej.,
foo = StringProperty('bar')
), la consulta de GQL debe usar el nombre de propiedad anulado (en el ejemplo,bar
).
Siempre utiliza la función de vinculación de parámetros si algunos valores de la consulta son variables suministradas por el usuario. Esto evita los ataques basados en hackeos sintácticos.
Sería un error realizar una consulta para un modelo que no fue importado (o que no fue definido).
Sería un error usar un nombre de propiedad que no esté definido por la clase de modelo, a menos que el modelo sea un Expando.
Especificar un límite o compensación para fetch()
de la consulta anula el límite o la compensación que establece las cláusulas de OFFSET
y LIMIT
de GQL. No combines OFFSET
y LIMIT
de GQL con fetch_page()
. Ten en cuenta que el máximo de 1,000 resultados que impone App Engine en las consultas se aplica al desplazamiento y al límite.
Si estás acostumbrado a usar SQL, ten cuidado con las suposiciones falsas cuando uses GQL. Este se traduce a la API de consulta nativa de NDB. Es diferente a un asignador relacional de objetos típico (como la asistencia de base de datos de SQLAlchemy o Django), en el que las llamadas a la API se traducen a SQL antes de transmitirse al servidor de base de datos. GQL no admite las modificaciones de Datastore (inserciones, operaciones de borrado o actualizaciones); solo admite consultas.