Modelado de datos en Python

Nota: Se recomienda enfáticamente a los desarrolladores que compilan aplicaciones nuevas que usen la biblioteca cliente de NDB, ya que tiene muchas ventajas en comparación con esta biblioteca cliente, como el almacenamiento en caché automático de entidades mediante la API de Memcache. Si por el momento usas la biblioteca cliente de DB anterior, lee la Guía de migración de DB a NDB.

Descripción general

Una entidad de almacén de datos tiene una clave y un conjunto de propiedades. Una aplicación usa la API de Datastore con el fin de definir modelos de datos y crear instancias de esos modelos para que se almacenen como entidades. Los modelos proporcionan una estructura común a las entidades que crea la API y pueden definir reglas para validar valores de propiedad.

Clases de modelo

La clase Modelo

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

Las propiedades del modelo se definen mediante atributos de clase en la clase modelo. Cada atributo de clase es una instancia de una subclase de la clase Propiedad, por lo general, una de las clases de propiedad proporcionadas. Una instancia de propiedad mantiene la configuración de la propiedad, por ejemplo, si se requiere o no la propiedad con el fin de que la instancia sea válida, o un valor predeterminado para usar en 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 la API con una instancia de la clase de modelo correspondiente. La aplicación puede crear una entidad nueva con una llamada al constructor de la clase. La aplicación accede y manipula las propiedades de la entidad con atributos de la instancia. El constructor de instancia de modelo acepta valores iniciales para propiedades como argumentos de palabras clave.

from google.appengine.api import users

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

Nota: Los atributos de la clase modelo son parámetros de configuración para las propiedades del modelo, cuyos valores son instancias de Propiedad. Los atributos de la instancia del modelo son los valores de propiedad reales y son del tipo que la clase Propiedad acepta.

La clase Modelo usa las instancias de Propiedad para validar los valores asignados a los atributos de la instancia del modelo. La validación del valor de la propiedad se produce durante la construcción inicial de una instancia del modelo, y cuando a un atributo de la instancia se le asigna un valor nuevo. Esto garantiza que una propiedad nunca tenga un valor no válido.

Debido a que la validación se produce cuando se construye la instancia, cualquier propiedad que esté configurada como obligatoria debe inicializarse en el constructor. En este ejemplo, name y type son valores obligatorios, por lo que sus valores iniciales se especifican en el constructor. weight_in_pounds no es obligatorio para el modelo, por lo que comienza sin un valor asignado y, luego, se le asigna uno.

Una instancia de un modelo que se crea mediante 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 la clase Python, la configuración de la propiedad del modelo se inicializa cuando se importa la secuencia de comandos o el módulo por primera vez. Debido a que App Engine almacena en caché los módulos importados entre solicitudes, la configuración del módulo puede inicializarse durante una solicitud de un usuario y usarse de nuevo durante una solicitud de otro. No inicialices la configuración de propiedades del modelo, como los valores predeterminados, con datos específicos de la solicitud o del usuario actual. Consulta Almacenamiento de aplicaciones en caché para obtener más información.

La clase Expando

Un modelo definido con la clase Modelo establece un conjunto fijo de propiedades que debe tener cada instancia de la clase (quizás con valores predeterminados). Esta es una forma útil de modelar objetos de datos, pero el almacén de datos no requiere que cada entidad de un tipo determinado tenga el mismo conjunto de propiedades.

A veces es útil que una entidad tenga propiedades que no necesitan ser similares a las propiedades de otras entidades del mismo tipo. Esta entidad se representa en la API de Datastore con un modelo "expando". Una clase de modelo expando 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 del almacén de datos con el nombre del atributo. Estas propiedades se conocen como propiedades dinámicas. Las propiedades definidas mediante instancias de la clase Propiedad en atributos de clase son propiedades fijas.

Un modelo expando puede tener propiedades fijas y dinámicas. La clase de modelo solo establece atributos de clase con objetos de configuración de Propiedad 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

Las propiedades dinámicas no se validan porque no tienen definiciones de propiedades de modelo. Cualquier propiedad dinámica puede tener un valor de cualquiera de los tipos básicos de almacén de datos, incluido None. Dos entidades del mismo tipo pueden tener tipos de valores distintos para la misma propiedad dinámica, y una puede dejar sin configurar una propiedad que la otra sí configura.

A diferencia de las propiedades fijas, la existencia de propiedades dinámicas no es obligatoria. Una propiedad dinámica con el valor None es diferente de una propiedad dinámica no existente. Si una instancia de modelo expando no tiene un atributo para una propiedad, la entidad de datos correspondiente no tiene esa propiedad. Puedes borrar una propiedad dinámica si borras el atributo.

Los atributos cuyos nombres comienzan con un guion bajo (_) no se guardan en la entidad del almacén de datos. Esto te permite almacenar valores en la instancia del modelo para uso interno temporal sin afectar los datos guardados con la entidad.

Nota: Las propiedades estáticas siempre se guardan en la entidad del almacén de datos, sin importar si es Expando, Modelo o si comienza con un guion bajo (_).

del p.chess_elo_rating

Una consulta que usa una propiedad dinámica en un filtro solo muestra entidades cuyo valor para la propiedad sea del mismo tipo que el valor usado en la consulta. De forma similar, la consulta solo muestra entidades con ese conjunto de propiedades.

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 entre grupos de entidades, que pueden mostrar resultados obsoletos. Para obtener resultados con coherencia sólida, usa consultas principales en grupos de entidades.

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

La clase Polimodelo

La API de Python incluye otra clase para el modelado de datos que te permite definir jerarquías de clases y realizar consultas que muestren entidades de una clase determinada o 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 garantiza que todas las entidades Person y Company tengan propiedades phone_number y address, y que las consultas para entidades Contact puedan mostrar entidades Person o Company. Solo las entidades Person tienen propiedades mobile_number.

Se pueden crear instancias de las subclases de la misma forma que 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 sobre las entidades Contact puede mostrar instancias de Contact, Person o Company. El siguiente código muestra información para las dos entidades creadas antes:

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

Una consulta sobre las entidades Company solo muestra instancias de Company:

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

Por ahora, los modelos polimórficos no se deben pasar de forma directa al constructor de la clase Consulta. En su lugar, usa el método all(), como en el ejemplo anterior.

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

Tipos y clases de propiedad

El almacén de datos admite un conjunto fijo de tipos de valores para las propiedades de entidad, lo que incluye strings Unicode, números enteros, números de punto flotante, fechas, claves de entidad, strings de bytes (BLOB) y varios tipos de GData. Cada uno de los tipos de valores del almacén de datos tiene una clase Propiedad correspondiente que proporciona el módulo google.appengine.ext.db.

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

Strings y BLOB

El almacén de datos admite dos tipos de valores para almacenar texto: strings de texto cortas de hasta 1,500 bytes de longitud y strings de texto largas de hasta un megabyte. Las strings cortas se indexan y se pueden usar en condiciones de filtro de consulta y órdenes de clasificación. Las strings largas no se indexan y no se pueden usar en condiciones de filtro ni órdenes de clasificación.

Un valor de string corta puede ser un valor de unicode o str. Si el valor es str, se supone que es una codificación de 'ascii'. Si deseas especificar una codificación diferente para un valor str, puedes convertirlo en un valor unicode con el constructor de tipo unicode(), que toma el str y el nombre de la codificación como argumentos. Las strings cortas se pueden modelar mediante 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 string larga se representa mediante una instancia db.Text. Su constructor toma un valor unicode o str y, de forma opcional, el nombre de la codificación que se usó en str. Las strings largas se pueden modelar mediante 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 strings de bytes que no son de texto: db.ByteString y db.Blob. Estos valores son strings de bytes sin procesar y no se tratan como texto codificado (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 1,500 bytes. Una instancia de ByteString representa una string corta de bytes y toma un valor de str como un argumento para su constructor. Las strings de bytes se modelan mediante la clase ByteStringProperty.

Al igual que db.Text, un valor db.Blob puede ser tan grande como un megabyte, pero no se indexa y no puede usarse en filtros de consulta ni órdenes de clasificación. La clase db.Blob toma un valor str como un argumento para su constructor, pero puedes asignar el valor directamente. Los BLOB 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 del almacén de datos como una list de Python. La lista puede contener valores de cualquiera de los tipos admitidos por el almacén de datos. Una propiedad de lista individual puede incluir valores de diferentes tipos.

El orden por lo general se conserva, por eso, cuando se muestran entidades mediante consultas y get(), los valores de las propiedades de la lista están en el mismo orden en que se almacenaron. Hay una excepción a esto: los valores Blob y Text se mueven al final de la lista, aunque conservan su orden original uno respecto del otro.

La clase ListProperty modela una lista y exige 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.

Una consulta con filtros en una propiedad de lista prueba cada valor de la lista de forma individual. La entidad coincidirá con la consulta solo si algún valor de la lista pasa todos los filtros de esa propiedad. Visita 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 consulta solo funcionan en los miembros de la lista. No hay forma de probar la similitud de dos listas en un filtro de consulta.

De forma interna, el almacén de datos representa un valor de propiedad de lista como valores múltiples para la propiedad. Si un valor de propiedad de lista es la lista vacía, entonces la propiedad no tiene representación en el almacén de datos. La API de Datastore trata esta situación de forma diferente para las propiedades estáticas (con ListProperty) y las propiedades dinámicas:

  • A una ListProperty estática se le puede asignar la lista vacía como un 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 un valor None.
  • No se puede asignar un valor de lista vacío a una propiedad dinámica con un valor list. Sin embargo, puede tener un valor None y se puede borrar (mediante del).

El modelo ListProperty comprueba que un valor agregado a la lista sea del tipo correcto y, si no lo es, muestra un error BadValueError. Esta prueba se produce (y potencialmente falla) incluso cuando una entidad almacenada con anterioridad se recupera y se carga en el modelo. Debido a que los valores str se convierten en valores unicode (como texto ASCII) antes del almacenamiento, 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 este propósito.

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

Las propiedades de lista pueden interactuar con los órdenes de clasificación de maneras inusuales. Visita la página Consultas de Datastore para obtener más información.

Referencias

Un valor de propiedad puede contener la clave de otra entidad. El valor es una instancia de Clave.

La clase ReferenceProperty modela un valor de clave y exige que todos los valores se refieran a entidades de un tipo determinado. Para mayor comodidad, la biblioteca también proporciona SelfReferenceProperty, equivalente a una ReferenceProperty que hace referencia al mismo tipo que la entidad con la propiedad.

La asignación de una instancia de modelo a una propiedad ReferenceProperty usa su clave como valor de manera automática.

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

Se puede usar un valor de propiedad ReferenceProperty como si fuera la instancia del modelo de la entidad a la que se hace referencia. Si la entidad a la que se hace referencia no está en la memoria, el uso de la propiedad como una instancia recupera de forma automática la entidad del almacén de datos. Una ReferenceProperty también almacena una clave, pero el uso de la propiedad hace que se cargue 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, el acceso a la propiedad genera un error. Si una aplicación anticipa que una referencia puede no ser válida, puede probar la existencia del objeto mediante un bloque try/except:

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

ReferenceProperty tiene otra característica útil: referencias inversas. Cuando un modelo tiene una ReferenceProperty para otro modelo, cada entidad a la que se hace referencia obtiene una propiedad cuyo valor es una Consulta que muestra todas las entidades del primer modelo a las que hacen referencia.

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

El nombre predeterminado de la propiedad de referencia inversa es modelname_set (con el nombre de la clase de modelo en letras minúsculas y “_set” agregado al final) y se puede ajustar mediante el argumento collection_name al constructor de ReferenceProperty.

Si tienes varios valores de ReferenceProperty que se refieren a la misma clase de modelo, la construcción predeterminada de la propiedad de referencia inversa genera 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 establecer el argumento collection_name de forma explícita:

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

Las referencias y desreferencias automáticas de las instancias de modelo, la comprobación de tipos y las referencias inversas solo están disponibles mediante la clase de propiedad de modelo ReferenceProperty. Las claves almacenadas como valores de propiedades dinámicas Expando o valores de ListProperty no tienen estas características.