Consultas de NDB

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:

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

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:

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

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

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

(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:

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

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:

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

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:

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 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:

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

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:

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

es equivalente a

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

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”:

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

Sin embargo, esta no se incluirá:

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

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:

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

es equivalente a

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

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

Article.tags == 'python'

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

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

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:

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

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:

  1. Expande los operadores != y INa su forma original, en la que !=se convierte en una verificación de si la propiedad es menor o mayor que el valor, mientras que IN 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.
  2. Un AND que contiene un OR es equivalente a un OR de varios AND aplicados a los operandos AND originales, con un solo operando OR sustituido por el OR original. Por ejemplo, AND(a, b, OR(c, d)) es equivalente a OR(AND(a, b, c), AND(a, b, d)).
  3. Una operación AND que tiene un operando que es una operación AND en sí mismo puede incorporar los operandos de AND anidada en AND entre paréntesis. Por ejemplo, AND(a, b, AND(c, d)) es equivalente a AND(a, b, c, d).
  4. Una operación OR que tiene un operando que es una operación OR en sí mismo puede incorporar los operandos de OR anidada en OR entre paréntesis. Por ejemplo, OR(a, b, OR(c, d)) es equivalente a OR(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:

  1. Mediante el uso de la regla n.º 1 en los operadores != y IN:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php',
             OR(tags < 'perl', tags > 'perl'))))
  2. Mediante el uso de la regla n.º 2 en la operación OR más interna anidada dentro de AND:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         OR(AND(tags == 'php', tags < 'perl'),
            AND(tags == 'php', tags > 'perl'))))
  3. Mediante el uso de la regla n.º 4 en OR anidada dentro de otra OR:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php', tags < 'perl'),
         AND(tags == 'php', tags > 'perl')))
  4. Mediante el uso de la regla n.º 2 en OR restante anidada dentro de 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. 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:

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

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:

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

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.

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

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

Para buscar todas las compras que pertenecen al cliente, puedes usar la siguiente consulta:

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

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.

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

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

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:

purchase = Purchase(parent=customer_entity.key)

Para obtener información sobre las compras de un cliente dado, utiliza la siguiente consulta:

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

Atributos de consultas

Los objetos de las consultas tienen los siguientes atributos de datos de solo lectura:

AtributoTipoPredeterminadoDescripción
tipostr None Tipo de nombre (generalmente nombre de clase)
principalKey None Clave principal especificada para la consulta
filtrosFilterNode None Expresión de filtro
órdenesOrder 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:

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

Filtrar 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:

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

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:

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

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:

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

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:

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

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.

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

También puedes utilizar getattr() para obtenerlo de la clase:

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

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

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

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:

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

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

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 objeto Query.
  • Mediante una llamada al método iter() del objeto Query.

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.

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

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:

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

Los iteradores de consulta tienen otros métodos útiles:

MétodoDescripció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:

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

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:

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

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:

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

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:

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 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 objeto Query (el mismo tipo que muestra Model.query()). Todos los métodos habituales están disponibles en estos objetos Query: fetch(), map_async(), filter() y demás.
  • Model.gql(querystring) es una abreviatura de ndb.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:
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1")
    query2 = query.bind(3)
    
    o
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1", 3)

    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.