Consultas de NDB

Una aplicación puede usar consultas para buscar en Datastore entidades que cumplan criterios de búsqueda específicos llamados filtros.

Información general

Una aplicación puede usar consultas para buscar en Datastore entidades que cumplan criterios de búsqueda específicos llamados filtros. Por ejemplo, una aplicación que registra varios libros de visitas podría usar 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, por lo que el almacén de datos necesita índices precompilados. Estos índices precompilados se especifican en un archivo de configuración, index.yaml. En el servidor de desarrollo, si ejecutas una consulta que necesita un índice que no has especificado, el servidor de desarrollo lo añade automáticamente a su index.yaml. Sin embargo, en su sitio web, falla una consulta que necesita un índice que aún no se ha especificado. Por lo tanto, el ciclo de desarrollo habitual consiste en probar una nueva consulta en el servidor de desarrollo y, a continuación, actualizar el sitio web para que use el index.yaml que se ha cambiado automáticamente. Puedes actualizar index.yaml por separado de la subida de la aplicación ejecutando gcloud app deploy index.yaml. Si tu almacén de datos tiene muchas entidades, se tarda mucho tiempo en crear un nuevo índice para ellas. En este caso, es recomendable actualizar las definiciones de índice antes de subir el código que utiliza el nuevo índice. Puedes usar la consola de administración para saber cuándo se han terminado de crear los índices.

App Engine Datastore admite de forma nativa filtros de concordancia exacta (operador ==) y comparaciones (operadores <, <=, > y >=). Permite combinar varios filtros mediante una operación booleana AND, con algunas limitaciones (consulta más abajo).

Además de los operadores nativos, la API admite el operador !=, que combina grupos de filtros mediante la operación booleana OR, y el operador IN, que comprueba si un valor es igual a uno de los valores posibles de una lista (como el operador "in" de Python). Estas operaciones no se corresponden directamente con las operaciones nativas de Datastore, por lo que son un poco peculiares y lentas en comparación. Se implementan mediante la combinación en memoria de flujos de resultados. Ten en cuenta que p != v se implementa como "p < v OR p > v". Esto es importante en el caso de las propiedades repetidas.

Limitaciones: Datastore aplica algunas restricciones a las consultas. Si se infringen, se producirán excepciones. Por ejemplo, actualmente no se permite combinar demasiados filtros, usar desigualdades en varias propiedades o combinar una desigualdad con un orden en una propiedad diferente. Además, los filtros que hacen referencia a varias propiedades a veces requieren que se configuren índices secundarios.

No admitido: Datastore no admite directamente concordancias de subcadenas, concordancias que no distinguen entre mayúsculas y minúsculas ni la llamada búsqueda de texto completo. Hay formas de implementar coincidencias que no distinguen entre mayúsculas y minúsculas, e incluso búsquedas de texto completo, mediante propiedades calculadas.

Filtrar por valores de propiedad

Recuerda la clase Account de Propiedades de NDB:

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

Normalmente, no querrá recuperar todas las entidades de un tipo determinado, sino solo aquellas que tengan un valor o un intervalo de valores específicos para alguna propiedad.

Los objetos de propiedad sobrecargan algunos operadores para devolver expresiones de filtro que se pueden usar para controlar una consulta. Por ejemplo, para buscar todas las entidades Account cuya propiedad userid tenga el valor exacto 42, puedes usar la expresión

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

Si estás seguro de que solo había un Account con ese userid, puedes 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 una desigualdad, puede usar una sintaxis como la siguiente:

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

De esta forma, se encuentran todas las entidades Account cuya propiedad userid es mayor o igual que 40.

Dos de estas operaciones, != e IN, se implementan como combinaciones de las otras y son un poco peculiares, como se describe en != e IN.

Puedes especificar varios filtros:

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

Combina los argumentos de filtro especificados y devuelve todas las entidades Account cuyo valor de userid sea mayor o igual que 40 y menor que 50.

Nota: Como se ha mencionado anteriormente, Datastore rechaza las consultas que usan filtros de desigualdad en más de una propiedad.

En lugar de especificar un filtro de consulta completo en una sola expresión, puede que te resulte más cómodo crearlo por pasos. 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.

Operaciones != e IN

Recupera la clase Article de Propiedades de NDB:

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

Las operaciones != (distinto de) y IN (pertenencia) se implementan combinando otros filtros mediante la operación OR. La primera de ellas,

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: Aunque pueda parecer sorprendente, esta consulta no busca entidades Article que no incluyan "perl" como etiqueta. En su lugar, busca todas las entidades que tengan al menos una etiqueta que no sea "perl". Por ejemplo, la siguiente entidad se incluiría en los resultados, aunque tenga "perl" como una de sus etiquetas:

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

Sin embargo, esta no se incluiría:

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

No hay forma de consultar entidades que no incluyan una etiqueta igual a "perl".

Del mismo modo, la operación IN

property IN [value1, value2, ...]

que comprueba si un valor pertenece a una lista de valores posibles, 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 eliminan los duplicados de sus resultados: el flujo de resultados no incluye una entidad más de una vez, aunque una entidad coincida con dos o más subconsultas.

Consultar propiedades repetidas

La clase Article definida en la sección anterior también sirve como ejemplo de consulta de propiedades repetidas. En concreto, un filtro como

Article.tags == 'python'

usa un solo valor, aunque Article.tags sea una propiedad repetida. No puedes comparar propiedades repetidas con objetos de lista (Datastore no lo entenderá) y un filtro como

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

hace algo completamente diferente a buscar entidades Article cuyo valor de etiqueta sea la lista ['python', 'ruby', 'php']: busca entidades cuyo valor tags (considerado como una lista) contenga al menos uno de esos valores.

Consultar el valor None en una propiedad repetida tiene un comportamiento indefinido, así que no lo hagas.

Combinar operaciones AND y OR

Puedes anidar operaciones AND y OR de forma 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, es posible que una consulta de este tipo que sea demasiado compleja falle y se produzca una excepción. Es más seguro si normalizas estos filtros para que haya (como máximo) una sola operación OR en la parte superior del árbol de expresiones y un solo nivel de operaciones AND por debajo.

Para llevar a cabo esta normalización, debes recordar tanto las reglas de la lógica booleana como la forma en que se implementan los filtros != y IN:

  1. Expande los operadores != y IN a su forma primitiva, donde != se convierte en una comprobación de si la propiedad es < o > que el valor, y IN se convierte en una comprobación de si la propiedad es == al primer valor o al segundo valor o...hasta el último valor de la lista.
  2. Un AND con un OR en su interior equivale a un OR de varios ANDs aplicados a los operandos AND originales, con un único 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. Un AND que tiene un operando que es en sí mismo una operación AND puede incorporar los operandos del AND anidado en el AND envolvente. Por ejemplo, AND(a, b, AND(c, d)) es equivalente a AND(a, b, c, d)
  4. Un OR que tiene un operando que es en sí mismo una operación OR puede incorporar los operandos del OR anidado en el OR envolvente. Por ejemplo, OR(a, b, OR(c, d)) es equivalente a OR(a, b, c, d)

Si aplicamos estas transformaciones por fases al filtro de ejemplo, usando una notación más sencilla que Python, obtenemos lo siguiente:

  1. Uso de la regla 1 en los operadores IN y !=:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php',
             OR(tags < 'perl', tags > 'perl'))))
  2. Aplicando la regla n.º 2 al elemento OR más interno anidado en un elemento AND:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         OR(AND(tags == 'php', tags < 'perl'),
            AND(tags == 'php', tags > 'perl'))))
  3. Usar la regla n.º 4 en el OR anidado en otro OR:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php', tags < 'perl'),
         AND(tags == 'php', tags > 'perl')))
  4. Aplicando la regla n.º 2 a los OR restantes anidados en un 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 la regla n.º 3 para contraer los ANDs anidados 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 provocar una explosión combinatoria. Considera el AND de 3 cláusulas OR con 2 cláusulas básicas cada una. Cuando se normaliza, se convierte en una OR de 8 cláusulas AND con 3 cláusulas básicas cada una, es decir, 6 términos se convierten en 24.

Especificar criterios de ordenación

Puedes usar el método order() para especificar el orden en el que una consulta devuelve sus resultados. Este método toma una lista de argumentos, cada uno de los cuales es un objeto de propiedad (que se va a ordenar en orden ascendente) o su negación (que denota el orden descendente). Por ejemplo:

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

De esta forma, se obtienen todas las entidades Greeting, ordenadas por el valor ascendente de su propiedad content. Las secuencias de entidades consecutivas con la misma propiedad de contenido se ordenarán por el valor descendente de su propiedad date. Puedes usar varias llamadas order() para conseguir el mismo efecto:

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

Nota: Al combinar filtros con order(), Datastore rechaza determinadas combinaciones. En concreto, cuando se usa un filtro de desigualdad, el primer orden de clasificación (si lo hay) debe especificar la misma propiedad que el filtro. Además, a veces es necesario configurar un índice secundario.

Consultas de ancestro

Las consultas de ancestros te permiten hacer consultas con una coherencia sólida al almacén de datos. Sin embargo, las entidades con el mismo ancestro están limitadas a una escritura por segundo. A continuación, se muestra una comparación sencilla de las ventajas y desventajas, y de la estructura de una consulta de ancestro y una consulta sin ancestro que usan clientes y sus compras asociadas en el almacén de datos.

En el siguiente ejemplo de no ancestro, 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 encontrar 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 un alto rendimiento de escritura, pero solo coherencia final. Si se ha añadido una nueva compra, es posible que los datos estén obsoletos. Puedes eliminar este comportamiento mediante consultas de ancestros.

En el caso de los clientes y las compras con consultas de ancestros, la estructura sigue siendo la misma, con dos entidades independientes. La parte del cliente es la misma. Sin embargo, cuando creas compras, ya no es necesario que especifiques el KeyProperty(). Esto se debe a que, cuando usas consultas de ancestros, llamas a la clave de la entidad de cliente al crear una entidad de compra.

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

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

Cada compra tiene una clave y el cliente también tiene la suya. Sin embargo, cada clave de compra tendrá insertada la clave de customer_entity. Recuerda que solo se podrá escribir una vez por ancestro por segundo. El siguiente código crea una entidad con un ancestro:

purchase = Purchase(parent=customer_entity.key)

Para consultar las compras de un cliente concreto, utiliza la siguiente consulta.

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

Consultar atributos

Los objetos de consulta tienen los siguientes atributos de datos de solo lectura:

Atributo Tipo Predeterminado Descripción
kind str None Nombre del tipo (normalmente, el nombre de la clase)
antecedente Key None Antecesor especificado para la consulta
filters FilterNode None Expresión de filtrado
pedidos Order None Ordenar pedidos

Al imprimir un objeto de consulta (o llamar a str() o repr() en él), se genera una representación de cadena con un formato adecuado:

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 los valores de campo de las propiedades estructuradas. Por ejemplo, una consulta de todos los contactos con una dirección cuya ciudad sea 'Amsterdam' tendría este aspecto:

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

Si combina varios filtros de este tipo, es posible que los filtros coincidan con diferentes subentidades Address dentro de la misma entidad Contact. Por ejemplo:

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

puede encontrar contactos con una dirección cuya ciudad sea 'Amsterdam' y otra dirección (diferente) cuya calle sea 'Spear St'. Sin embargo, al menos en el caso de los filtros de igualdad, puedes crear una consulta que devuelva solo los resultados con varios valores en una sola subentidad:

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

Si usa esta técnica, las propiedades de la subentidad que sean iguales a None se ignorarán en la consulta. Si una propiedad tiene un valor predeterminado, debes asignarle explícitamente el valor None para que no se tenga en cuenta en la consulta. De lo contrario, la consulta incluirá un filtro que requiere que el valor de la propiedad sea igual al valor predeterminado. Por ejemplo, si el modelo Address tuviera una propiedad country con el valor default='us', el ejemplo anterior solo devolvería los contactos cuyo país fuera 'us'. Para tener en cuenta los contactos de otros países, tendrías que filtrar por Address(city='San Francisco', street='Spear St', country=None).

Si una subentidad tiene algún valor de propiedad igual a None, se ignorará. Por lo tanto, no tiene sentido filtrar por un valor de propiedad de subentidad de None.

Usar propiedades con nombre de cadena

A veces, quieres filtrar u ordenar una consulta en función de una propiedad cuyo nombre se especifica mediante una cadena. Por ejemplo, si permites que el usuario introduzca consultas de búsqueda como tags:python, sería conveniente convertirla de alguna forma en una consulta como

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

Si tu modelo es un Expando, tu filtro puede usar GenericProperty, la clase Expando usa para las propiedades dinámicas:

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

También puedes usar GenericProperty si tu modelo no es un Expando, pero si quieres asegurarte de que solo usas nombres de propiedad definidos, también puedes usar el atributo de clase _properties.

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

También puedes usar getattr() para obtenerla de la clase:

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

La diferencia es que getattr() usa el "nombre de Python" de la propiedad, mientras que _properties se indexa por el "nombre de almacén de datos" de la propiedad. Solo se diferencian cuando la propiedad se ha declarado con algo parecido a

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

En este caso, el nombre de Python es title, pero el nombre del almacén de datos es t.

Estos métodos también se pueden usar para ordenar los resultados de las consultas:

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

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

Iteradores de consultas

Mientras se está procesando una consulta, su estado se mantiene en un objeto iterador. La mayoría de las aplicaciones no los usan directamente. Normalmente, es más sencillo llamar a fetch(20) que manipular el objeto de iterador. Hay dos formas básicas de obtener un objeto de este tipo:

  • Usar la función iter() integrada de Python en un objeto Query
  • Llamar al método iter() del objeto Query

La primera admite el uso de un bucle for de Python (que llama implícitamente a la función iter()) para recorrer una consulta.

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

La segunda forma, que usa el método Query del objeto iter(), te permite transferir opciones al iterador para influir en su comportamiento. Por ejemplo, para usar una consulta de solo claves en un bucle for, puedes escribir lo siguiente:

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

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

Método Descripción
__iter__() Parte del protocolo de iteradores de Python.
next() Devuelve el siguiente resultado o genera la excepción StopIteration si no hay ninguno.

has_next() Devuelve True si una llamada posterior a next() devolverá un resultado o False si generará StopIteration.

Bloquea hasta que se conoce la respuesta a esta pregunta y almacena en el búfer el resultado (si hay alguno) hasta que lo recuperes con next().
probably_has_next() Como has_next(), pero usa un acceso directo más rápido (y a veces impreciso).

Puede devolver un falso positivo (True cuando next() realmente genere StopIteration), pero nunca un falso negativo (False cuando next() realmente devuelva un resultado).
cursor_before() Devuelve un cursor de consulta que representa un punto justo antes del último resultado devuelto.

Genera una excepción si no hay ningún cursor disponible (en concreto, si no se ha pasado la opción de consulta produce_cursors).
cursor_after() Devuelve un cursor de consulta que representa un punto justo después del último resultado devuelto.

Genera una excepción si no hay ningún cursor disponible (en concreto, si no se ha pasado la opción de consulta produce_cursors).
index_list() Devuelve una lista de los índices utilizados por una consulta ejecutada, incluidos los índices primarios, compuestos, de tipo y de una sola propiedad.

Cursores de consultas

Un cursor de consulta es una pequeña estructura de datos opaca que representa un punto de reanudación en una consulta. Esto es útil para mostrar a un usuario una página de resultados cada vez, así como para gestionar tareas largas que pueden tener que detenerse y reanudarse. Una forma habitual de usarlos es con el método fetch_page() de una consulta. Funciona de forma similar a fetch(), pero devuelve un triple (results, cursor, more). La marca more devuelta indica que probablemente haya más resultados. Una interfaz de usuario puede usarla, por ejemplo, para suprimir un botón o un enlace "Siguiente página". Para solicitar las páginas siguientes, pasa el cursor devuelto por una llamada a fetch_page() a la siguiente. Se genera un BadArgumentError si se pasa un cursor no válido. Ten en cuenta que la validación solo comprueba si el valor está codificado en Base64. Deberás realizar cualquier otra validación necesaria.

Por lo tanto, para permitir que el usuario vea todas las entidades que coinciden con una consulta, obteniéndolas una página a la vez, el código podría ser el siguiente:

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. De esta forma, puedes enviar un cursor a un cliente en la web en la respuesta a una solicitud y recibirlo de vuelta del cliente en una solicitud posterior.

Nota: El método fetch_page() suele devolver un cursor aunque no haya más resultados, pero no es seguro: el valor del cursor devuelto puede ser None. También debes tener en cuenta que, como la marca more se implementa mediante el método probably_has_next() del iterador, en raras ocasiones puede devolver True aunque la página siguiente esté vacía.

Algunas consultas de NDB no admiten cursores de consulta, pero puedes corregirlas. Si una consulta usa IN, OR o !=, los resultados de la consulta no funcionarán con 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 lugar de "paginar" los resultados de una consulta, puedes usar el método iter() de una consulta para obtener un cursor en un punto concreto. Para ello, transfiere produce_cursors=True a iter(); cuando el iterador esté en el lugar correcto, llama a su cursor_after() para obtener un cursor que esté justo después. O, de forma similar, llama a cursor_before() para obtener un cursor justo antes. Ten en cuenta que llamar a cursor_after() o a cursor_before() puede hacer una llamada de bloqueo de Datastore, volviendo a ejecutar parte de la consulta para extraer un cursor que apunte al centro de un lote.

Para usar un cursor para desplazarte hacia atrás por los resultados de una 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 ("Asignación")

Supongamos que necesitas obtener las entidades Account correspondientes a las entidades Message devueltas por una consulta. Puedes escribir algo como esto:

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, este proceso es bastante ineficiente: espera a obtener una entidad y, a continuación, la usa. Después, espera a obtener la siguiente entidad y la usa. Hay mucho tiempo de espera. Otra forma es escribir una función de retrollamada que se asigne 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á algo más rápido que el bucle for simple anterior, ya que es posible cierta simultaneidad. Sin embargo, como la llamada get() en callback() sigue siendo síncrona, la ganancia no es enorme. Este es un buen lugar para usar obtenciones asíncronas.

GQL

GQL es un lenguaje similar a SQL que se usa para recuperar entidades o claves de App Engine Datastore. Aunque las funciones de GQL son diferentes de las de un lenguaje de consulta para una base de datos relacional tradicional, la sintaxis de GQL es similar a la de SQL. La sintaxis de GQL se describe en la referencia de GQL.

Puedes usar GQL para crear consultas. Es similar a crear una consulta con Model.query(), pero usa la sintaxis de GQL para definir el filtro y el orden de la consulta. Para utilizarlo:

  • ndb.gql(querystring) devuelve un objeto Query (del mismo tipo que el que devuelve Model.query()). Todos los métodos habituales están disponibles en estos objetos Query: fetch(), map_async(), filter(), etc.
  • Model.gql(querystring) es una abreviatura de ndb.gql("SELECT * FROM Model " + querystring). Normalmente, querystring es algo parecido a "WHERE prop1 > 0 AND prop2 = TRUE".
  • Para consultar modelos que contengan propiedades estructuradas, puedes usar foo.bar en tu sintaxis de GQL para hacer referencia a subpropiedades.
  • GQL admite enlaces de parámetros similares a SQL. Una aplicación puede definir una consulta y, a continuación, vincular valores a 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)

    Al llamar a la función bind() de una consulta, se devuelve una nueva consulta. No se modifica la original.

  • Si tu clase de modelo anula el método de clase _get_kind(), tu consulta GQL debe usar el tipo devuelto por esa función, no el nombre de la clase.
  • Si una propiedad de tu modelo anula su nombre (por ejemplo, foo = StringProperty('bar')) tu consulta de GQL debe usar el nombre de la propiedad sustituida (en el ejemplo, bar).

Utilice siempre la función de enlace de parámetros si algunos valores de su consulta son variables proporcionadas por el usuario. De esta forma, se evitan los ataques basados en vulnerabilidades sintácticas.

Es un error consultar un modelo que no se ha importado (o, en general, que no se ha definido).

Es un error usar un nombre de propiedad que no esté definido por la clase de modelo, a menos que ese modelo sea un Expando.

Si se especifica un límite o un desplazamiento en la fetch() de la consulta, se anula el límite o el desplazamiento definidos por las cláusulas OFFSET y LIMIT de GQL. No combines OFFSET y LIMIT de GQL con fetch_page() Ten en cuenta que el máximo de 1000 resultados que impone App Engine a las consultas se aplica tanto al desplazamiento como al límite.

Si estás acostumbrado a SQL, ten cuidado con las suposiciones falsas al usar GQL. GQL se traduce a la API de consulta nativa de NDB. Esto es diferente de un asignador relacional de objetos típico (como SQLAlchemy o la compatibilidad con bases de datos de Django), donde las llamadas a la API se traducen a SQL antes de transmitirse al servidor de la base de datos. GQL no admite modificaciones en Datastore (inserciones, eliminaciones o actualizaciones), solo admite consultas.