Modelos de datos en Python

Nota: Se recomienda encarecidamente a los desarrolladores que creen aplicaciones nuevas que usen la biblioteca de cliente de NDB, que ofrece varias ventajas en comparación con esta biblioteca de cliente, como el almacenamiento automático en caché de entidades mediante la API Memcache. Si actualmente usas la biblioteca de cliente de DB anterior, consulta la guía de migración de DB a NDB.

Información general

Las entidades de almacén de datos incluyen una clave y un conjunto de propiedades. Las aplicaciones utilizan el API de almacén de datos para definir modelos de datos y crear instancias de dichos modelos que se almacenarán como entidades. Los modelos proporcionan una estructura común a las entidades creadas por el API y pueden definir reglas para validar los valores de propiedad.

Clases de modelos

La clase Model

Una aplicación describe los tipos de datos que usa con modelos. Un modelo es una clase de Python que hereda de la clase Model. La clase de modelo define un nuevo tipo de entidad de almacén de datos y las propiedades que se espera que tenga el tipo. El nombre de tipo se define mediante el nombre de la clase instanciada que hereda de db.Model.

Las propiedades de Model se definen con atributos de clase en la clase de modelo. Cada atributo de clase es una instancia de una subclase de la clase Property, normalmente una de las clases de propiedad proporcionadas. La instancia de propiedad contiene la configuración de la propiedad; por ejemplo, indica si la propiedad es necesaria para que la instancia sea válida o incluye un valor predeterminado que puede utilizarse para la instancia si no se proporciona ninguno.

from google.appengine.ext import db

class Pet(db.Model):
    name = db.StringProperty(required=True)
    type = db.StringProperty(required=True, choices=set(["cat", "dog", "bird"]))
    birthdate = db.DateProperty()
    weight_in_pounds = db.IntegerProperty()
    spayed_or_neutered = db.BooleanProperty()

Una entidad de uno de los tipos de entidad definidos se representa en el API con una instancia de la clase de modelo correspondiente. La aplicación puede crear una nueva entidad mediante una llamada al constructor de la clase. La aplicación accede y manipula las propiedades de la entidad mediante los atributos de la instancia. El constructor de instancias de modelos acepta los valores iniciales de las propiedades como argumentos de palabra clave.

from google.appengine.api import users

pet = Pet(name="Fluffy",
          type="cat")
pet.weight_in_pounds = 24

Nota: Los atributos de la clase del modelo son la configuración de las propiedades del modelo, cuyos valores son instancias de Property. Los atributos de la instancia del modelo son los valores de propiedad reales, cuyos valores son del tipo aceptado por la clase Property.

La clase Model utiliza las instancias de Property para validar valores asignados a los atributos de instancia de modelo. La validación del valor Property se produce cuando una instancia de modelo se construye por primera vez y cuando a un atributo de instancia se le asigna un valor nuevo. De esta forma se garantiza que las propiedades nunca puedan contener un valor no válido.

Dado que la validación se produce cuando se construye la instancia, todas las propiedades que se configuren como necesarias se deberán inicializar en el constructor. En este ejemplo, name y type son valores obligatorios, por lo que sus valores iniciales se especifican en el constructor. El modelo no requiere weight_in_pounds, por lo que empieza sin asignar y, más adelante, se le asigna un valor.

Una instancia de un modelo creado con el constructor no existe en el almacén de datos hasta que se "coloca" por primera vez.

Nota: Al igual que con todos los atributos de clase de Python, la configuración de las propiedades del modelo se inicializa cuando se importa por primera vez la secuencia de comandos o el módulo. Dado que App Engine almacena en caché módulos importados entre solicitudes, la configuración del módulo se puede inicializar durante una solicitud de un usuario y volver a utilizarse durante una solicitud de otro. No inicialices la configuración de propiedad de modelo como, por ejemplo, los valores predeterminados, con datos específicos de la solicitud o del usuario actual. Consulta Almacenamiento en caché de aplicaciones para obtener más información.

La clase Expando

Un modelo definido mediante la clase Model establece un conjunto fijo de propiedades que deben incluir todas las instancias de la clase (quizá con los valores predeterminados). Esto resulta útil para crear modelos de objetos de datos, pero el almacén de datos no requiere que todas las entidades de un tipo determinado contengan el mismo conjunto de propiedades.

A veces, es útil que una entidad tenga propiedades que no sean necesariamente como las propiedades de otras entidades del mismo tipo. Este tipo de entidad se representa en la API de Datastore mediante un modelo "expando". Una subclase de la clase de modelo de expansión es una subclase de la superclase Expando. Cualquier valor asignado a un atributo de una instancia de un modelo Expando se convierte en una propiedad de la entidad de almacén de datos y utiliza el nombre del atributo. Estas propiedades se denominan propiedades dinámicas. Las propiedades definidas mediante instancias de la clase Property en atributos de clase son propiedades fijas.

Un modelo Expando puede presentar tanto propiedades fijas como dinámicas. La clase de modelo simplemente establece atributos de clase con objetos de configuración de Property para las propiedades fijas. La aplicación crea propiedades dinámicas cuando les asigna valores.

class Person(db.Expando):
    first_name = db.StringProperty()
    last_name = db.StringProperty()
    hobbies = db.StringListProperty()

p = Person(first_name="Albert", last_name="Johnson")
p.hobbies = ["chess", "travel"]

p.chess_elo_rating = 1350

p.travel_countries_visited = ["Spain", "Italy", "USA", "Brazil"]
p.travel_trip_count = 13

Dado que las propiedades dinámicas no disponen de definiciones de propiedades de modelo, las propiedades dinámicas no se validan. Cualquier propiedad dinámica puede tener un valor de cualquiera de los tipos base del almacén de datos, incluido None. Dos entidades del mismo tipo pueden contener distintos tipos de valores para la misma propiedad dinámica y una puede dejar sin definir una propiedad que defina la otra.

A diferencia de las propiedades fijas, no es necesario que las propiedades dinámicas existan. Una propiedad dinámica con el valor None es diferente de una propiedad dinámica que no existe. Si una instancia de modelo Expando no dispone de un atributo para una propiedad, la entidad de datos correspondiente no contendrá esa propiedad. Para eliminar una propiedad dinámica, suprime el atributo.

Los atributos cuyos nombres empiezan por un guion bajo (_) no se guardan en la entidad del almacén de datos. De esta forma, puedes almacenar valores de la instancia de modelo para un uso interno temporal sin que ello afecte a los datos guardados en la entidad.

Nota: Las propiedades estáticas siempre se guardarán en la entidad del almacén de datos, independientemente de si es Expando, Model o empieza por un guion bajo (_).

del p.chess_elo_rating

Las consultas que utilizan una propiedad dinámica en un filtro solo devuelven entidades cuyo valor para la propiedad sea del mismo tipo que el valor utilizado en la consulta. Del mismo modo, la consulta solo devuelve entidades con esa propiedad establecida.

p1 = Person()
p1.favorite = 42
p1.put()

p2 = Person()
p2.favorite = "blue"
p2.put()

p3 = Person()
p3.put()

people = db.GqlQuery("SELECT * FROM Person WHERE favorite < :1", 50)
# people has p1, but not p2 or p3

people = db.GqlQuery("SELECT * FROM Person WHERE favorite > :1", 50)
# people has no results

Nota: En el ejemplo anterior se usan consultas en grupos de entidades, que pueden devolver resultados obsoletos. Para obtener resultados con coherencia fuerte, usa consultas de ancestros en grupos de entidades.

La clase Expando es una subclase de la clase Model y hereda todos sus métodos.

La clase PolyModel

El API Python incluye otra clase de modelos de datos que te permite definir jerarquías de clases y realizar consultas que pueden devolver entidades de una clase determinada o de cualquiera de sus subclases. Estos modelos y consultas se denominan "polimórficos" porque permiten que las instancias de una clase sean resultados de una consulta de una clase superior.

En el siguiente ejemplo se define una clase Contact con las subclases Person y Company:

from google.appengine.ext import db
from google.appengine.ext.db import polymodel

class Contact(polymodel.PolyModel):
    phone_number = db.PhoneNumberProperty()
    address = db.PostalAddressProperty()

class Person(Contact):
    first_name = db.StringProperty()
    last_name = db.StringProperty()
    mobile_number = db.PhoneNumberProperty()

class Company(Contact):
    name = db.StringProperty()
    fax_number = db.PhoneNumberProperty()

Este modelo asegura que todas las entidades Person y Company tengan propiedades phone_number y address, y que las consultas de entidades Contact puedan devolver entidades Person o Company. Solo las entidades Person tienen propiedades mobile_number.

Se pueden crear instancias de las subclases como si se tratara de cualquier otra clase de modelo:

p = Person(phone_number='1-206-555-9234',
           address='123 First Ave., Seattle, WA, 98101',
           first_name='Alfred',
           last_name='Smith',
           mobile_number='1-206-555-0117')
p.put()

c = Company(phone_number='1-503-555-9123',
            address='P.O. Box 98765, Salem, OR, 97301',
            name='Data Solutions, LLC',
            fax_number='1-503-555-6622')
c.put()

Una consulta de entidades Contact puede devolver instancias de Contact, Person o Company. El código que aparece a continuación imprime información para las dos entidades creadas más arriba:

for contact in Contact.all():
    print 'Phone: %s\nAddress: %s\n\n' % (contact.phone_number,
                                          contact.address)

Una consulta de entidades Company solo devuelve instancias de Company:

for company in Company.all()
    # ...

Por ahora, los modelos polimórficos no deben pasarse directamente al constructor de la clase Query. En su lugar, usa el método all(), como en el ejemplo anterior.

Para obtener más información sobre cómo usar los modelos polimórficos y cómo se implementan, consulta La clase PolyModel.

Clases y tipos de propiedades

El almacén de datos admite un conjunto fijo de tipos de valores para propiedades de entidad, incluidos números enteros, cadenas de Unicode, números de punto flotante, fechas, claves de entidad, cadenas de bytes (blobs) y diversos tipos de GData. Cada uno de los tipos de valor de Datastore tiene una clase Property correspondiente proporcionada por el módulo google.appengine.ext.db.

En Tipos y clases de propiedades se describen todos los tipos de valores admitidos y sus clases de propiedades correspondientes. A continuación se describen varios tipos de valor especiales.

Cadenas y blobs

El almacén de datos admite dos tipos de valores para almacenar texto: cadenas de texto cortas de hasta 1500 bytes y cadenas de texto largas de hasta un megabyte. Las cadenas cortas se indexan y se pueden utilizar en filtros de consultas y en criterios de ordenación. Las cadenas largas no se indexan y no se pueden utilizar en filtros de consultas ni en criterios de ordenación.

Una cadena corta puede ser un valor unicode o str. Si el valor es str, se presupone una codificación de 'ascii'. Para especificar una codificación diferente para un valor str, puedes convertirlo en un valor unicode con el constructor de tipo unicode(), que toma el valor str y el nombre de la codificación como argumentos. Las cadenas cortas se pueden modelar con la clase StringProperty.

class MyModel(db.Model):
    string = db.StringProperty()

obj = MyModel()

# Python Unicode literal syntax fully describes characters in a text string.
obj.string = u"kittens"

# unicode() converts a byte string to a Unicode string using the named codec.
obj.string = unicode("kittens", "latin-1")

# A byte string is assumed to be text encoded as ASCII (the 'ascii' codec).
obj.string = "kittens"

# Short string properties can be used in query filters.
results = db.GqlQuery("SELECT * FROM MyModel WHERE string = :1", u"kittens")

Un valor de cadena largo se representa mediante una instancia de db.Text. Su constructor toma un valor unicode o un valor str y, opcionalmente, el nombre de la codificación utilizada en str. Las cadenas largas se pueden modelar con la clase TextProperty.

class MyModel(db.Model):
    text = db.TextProperty()

obj = MyModel()

# Text() can take a Unicode string.
obj.text = u"lots of kittens"

# Text() can take a byte string and the name of an encoding.
obj.text = db.Text("lots of kittens", "latin-1")

# If no encoding is specified, a byte string is assumed to be ASCII text.
obj.text = "lots of kittens"

# Text properties can store large values.
obj.text = db.Text(open("a_tale_of_two_cities.txt").read(), "utf-8")

El almacén de datos también admite dos tipos similares para cadenas de bytes que no son de texto: db.ByteString y db.Blob. Estos valores son cadenas de bytes sin procesar y no se tratan como texto codificado (como, por ejemplo, UTF-8).

Al igual que los valores db.StringProperty, los valores db.ByteString se indexan. Al igual que las propiedades db.TextProperty, los valores db.ByteString están limitados a 1500 bytes. Una instancia de ByteString representa una cadena corta de bytes y toma un valor str como argumento de su constructor. Las cadenas de bytes se modelan mediante la clase ByteStringProperty.

Al igual que db.Text, un valor de db.Blob puede tener un tamaño de hasta un megabyte, pero no se indexa y no se puede usar en filtros de consulta ni en criterios de ordenación. La clase db.Blob toma un valor str como argumento para su constructor, o bien puedes asignar el valor directamente. Los blobs se modelan mediante la clase BlobProperty.

class MyModel(db.Model):
    blob = db.BlobProperty()

obj = MyModel()

obj.blob = open("image.png").read()

Listas

Una propiedad puede tener varios valores, representados en la API Datastore como un list de Python. La lista puede contener valores de cualquiera de los tipos de valores que admite el almacén de datos. Una propiedad de lista única puede incluso incluir valores de distintos tipos.

El orden se suele conservar, por lo que, cuando las consultas y get() devuelven entidades, los valores de las propiedades de lista están en el mismo orden que cuando se almacenaron. Hay una excepción: los valores Blob y Text se mueven al final de la lista, pero conservan el orden original entre sí.

La clase ListProperty modeliza una lista y obliga a que todos los valores de la lista sean de un tipo determinado. Para mayor comodidad, la biblioteca también proporciona StringListProperty, similar a ListProperty(basestring).

class MyModel(db.Model):
    numbers = db.ListProperty(long)

obj = MyModel()
obj.numbers = [2, 4, 6, 8, 10]

obj.numbers = ["hello"]  # ERROR: MyModel.numbers must be a list of longs.

En una consulta con filtros en una propiedad de lista, se prueba cada valor de la lista individualmente. La entidad solo coincidirá con la consulta si algún valor de la lista supera todos los filtros de esa propiedad. Consulta la página Consultas de Datastore para obtener más información.

# Get all entities where numbers contains a 6.
results = db.GqlQuery("SELECT * FROM MyModel WHERE numbers = 6")

# Get all entities where numbers contains at least one element less than 10.
results = db.GqlQuery("SELECT * FROM MyModel WHERE numbers < 10")

Los filtros de consultas solo funcionan con los miembros de la lista. En un filtro de consultas no se puede comprobar la similitud de dos listas.

Internamente, el almacén de datos representa un valor de propiedad de lista como varios valores de la propiedad. Si uno de los valores de propiedad de lista es la lista vacía, la propiedad no tendrá representación en el almacén de datos. El API de almacén de datos trata esta situación de forma distinta en función de si trabaja con propiedades estáticas (con ListProperty) o dinámicas:

  • A una propiedad ListProperty estática se le puede asignar la lista vacía como valor. La propiedad no existe en el almacén de datos, pero la instancia de modelo se comporta como si el valor fuera la lista vacía. Una ListProperty estática no puede tener el valor None.
  • A una propiedad dinámica con un valor list no se le puede asignar una lista vacía. Sin embargo, puede tener el valor None y se puede eliminar (con del).

El modelo ListProperty comprueba que el valor añadido a la lista sea del tipo correcto y genera un error BadValueError si no lo es. Esta prueba se realiza (y puede fallar) incluso cuando se recupera una entidad almacenada anteriormente y se carga en el modelo. Como los valores str se convierten en valores unicode (como texto ASCII) antes de almacenarse, ListProperty(str) se trata como ListProperty(basestring), el tipo de datos de Python que acepta valores str y unicode. También puedes usar StringListProperty() para ello.

Para almacenar cadenas de bytes que no sean de texto, usa valores db.Blob. Los bytes de una cadena de blob se conservan cuando se almacenan y recuperan. Puedes declarar una propiedad que sea una lista de blobs como ListProperty(db.Blob).

Las propiedades de lista pueden interactuar con los criterios de ordenación de formas inusuales. Consulta la página Consultas de Datastore para obtener más información.

Referencias

Un valor de propiedad contiene la clave de otra entidad. El valor es una instancia de Key.

La clase ReferenceProperty modeliza un valor de clave y obliga a que todos los valores hagan referencia a entidades de un tipo determinado. Para mayor comodidad, la biblioteca también proporciona SelfReferenceProperty, que equivale a ReferenceProperty, pero hace referencia al mismo tipo que la entidad con la propiedad.

La asignación de una instancia de modelo a una propiedad ReferenceProperty hace que su clave se utilice automáticamente como valor.

class FirstModel(db.Model):
    prop = db.IntegerProperty()

class SecondModel(db.Model):
    reference = db.ReferenceProperty(FirstModel)

obj1 = FirstModel()
obj1.prop = 42
obj1.put()

obj2 = SecondModel()

# A reference value is the key of another entity.
obj2.reference = obj1.key()

# Assigning a model instance to a property uses the entity's key as the value.
obj2.reference = obj1
obj2.put()

Un valor de propiedad ReferenceProperty se puede utilizar como si fuera la instancia de modelo de la entidad a la que se hace referencia. Si la entidad a la que se hace referencia no se encuentra en memoria, utiliza la propiedad como instancia para extraer automáticamente la entidad del almacén de datos. Un valor de propiedad ReferenceProperty también almacena una clave, pero al utilizar la propiedad, se carga la entidad relacionada.

obj2.reference.prop = 999
obj2.reference.put()

results = db.GqlQuery("SELECT * FROM SecondModel")
another_obj = results.fetch(1)[0]
v = another_obj.reference.prop

Si una clave apunta a una entidad que no existe, al acceder a la propiedad, se genera un error. Si una aplicación espera que una referencia pueda no ser válida, puede comprobar si el objeto existe mediante un bloque try/except:

try:
  obj1 = obj2.reference
except db.ReferencePropertyResolveError:
  # Referenced entity was deleted or never existed.

ReferenceProperty presenta otra función útil: las referencias a otros elementos. Cuando un modelo tiene una ReferenceProperty a otro modelo, cada entidad referenciada obtiene una propiedad cuyo valor es una Query que devuelve todas las entidades del primer modelo que hacen referencia a ella.

# To fetch and iterate over every SecondModel entity that refers to the
# FirstModel instance obj1:
for obj in obj1.secondmodel_set:
    # ...

El nombre de la propiedad de referencia inversa es modelname_set de forma predeterminada (con el nombre de la clase de modelo en minúsculas y "_set" añadido al final) y se puede ajustar mediante el argumento collection_name del constructor ReferenceProperty.

Si dispones de varios valores de ReferenceProperty que hacen referencia a la misma clase de modelo, la construcción predeterminada de la propiedad de referencia a otro elemento generará un error:

class FirstModel(db.Model):
    prop = db.IntegerProperty()

# This class raises a DuplicatePropertyError with the message
# "Class Firstmodel already has property secondmodel_set"
class SecondModel(db.Model):
    reference_one = db.ReferenceProperty(FirstModel)
    reference_two = db.ReferenceProperty(FirstModel)

Para evitar este error, debes definir explícitamente el argumento collection_name:

class FirstModel(db.Model):
    prop = db.IntegerProperty()

# This class runs fine
class SecondModel(db.Model):
    reference_one = db.ReferenceProperty(FirstModel,
        collection_name="secondmodel_reference_one_set")
    reference_two = db.ReferenceProperty(FirstModel,
        collection_name="secondmodel_reference_two_set")

La referencia y la anulación de las referencias automáticas de las instancias de modelo, la comprobación del tipo y las referencias a otros elementos solo están disponibles a través de la clase de propiedad de modelo ReferenceProperty. Las claves almacenadas como valores de propiedades dinámicas Expando o valores ListProperty no disponen de estas funciones.