Modelagem de dados em Python

Observação: é altamente recomendável a desenvolvedores que criam novos aplicativos usar a biblioteca de cliente NDB, porque ela oferece diversos benefícios em comparação com esta biblioteca de cliente, como armazenamento em cache automático de entidades por meio da API Memcache. Se você estiver usando a antiga biblioteca de cliente DB, leia o Guia de migração de DB para NDB.

Visão geral

Uma entidade do armazenamento de dados tem uma chave e um conjunto de propriedades. Um aplicativo usa a API de armazenamento de dados para definir modelos de dados e criar instâncias desses modelos que serão armazenadas como entidades. Modelos fornecem uma estrutura comum para as entidades criadas pela API e podem definir regras para a validação de valores de propriedade.

Classes de modelo

A classe Model

Um aplicativo descreve os tipos de dados usados com os modelos. Um modelo é uma classe Python herdeira da classe de Model. A classe de modelo define um novo Tipo de entidade do armazenamento de dados e as propriedades esperadas para o Tipo. O nome do Tipo é definido pelo nome da classe instanciada herdeira do db.Model.

Propriedades de modelo são definidas usando atributos de classe na classe de modelo. Cada atributo de classe é a instância de uma subclasse de Property, geralmente uma das classes de propriedade fornecidas. Uma instância de propriedade contém a configuração da propriedade, por exemplo, se a propriedade é ou não necessária para que a instância seja válida ou um valor padrão a ser usado para a instância se nenhum for fornecido.

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

Uma entidade de um dos tipos de entidade definidos é representada na API por uma instância da classe de modelo correspondente. O aplicativo pode criar uma nova entidade chamando o construtor da classe. O aplicativo acessa e manipula propriedades da entidade usando atributos da instância. O construtor da instância de modelo aceita valores iniciais para propriedades como argumentos de palavra-chave.

from google.appengine.api import users

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

Observação: os atributos da classe de modelo são configurações das propriedades do modelo, em que os valores são instâncias de Property. Os atributos da instância do modelo são valores reais de propriedade e são do tipo aceito pela classe Property.

A classe de Modelo usa as instâncias da Property para validar valores atribuídos aos atributos da instância de modelo. A validação de valor da Property ocorre quando uma instância de modelo é construída pela primeira vez e quando é atribuído um novo valor a um atributo de instância. Isso garante que uma propriedade nunca tenha um valor inválido.

Visto que a validação ocorre quando a instância é criada, qualquer propriedade configurada para ser obrigatória precisa ser inicializada no construtor. Neste exemplo, name e type são valores obrigatórios, então os valores iniciais são especificados no construtor. weight_in_pounds não é obrigatório para o modelo, então começa não atribuído e mais tarde tem um valor atribuído.

No armazenamento de dados, não existirá uma instância de modelo criada com o construtor até que ela seja "colocada" pela primeira vez.

Observação: assim como ocorre em todos os atributos de classe do Python, a configuração da propriedade de modelo é inicializada quando o script ou modelo é importado pela primeira vez. Uma vez que o Google App Engine armazena em cache os módulos importados entre solicitações, a configuração de módulo pode ser inicializada durante uma solicitação para um usuário e reutilizada durante uma solicitação para outro. Não inicialize a configuração de propriedade de modelo, como valores padrão, com dados específicos à solicitação ou ao usuário atual. Para mais informações, veja Armazenamento de aplicativos em cache.

A classe Expando

Um modelo definido usando a classe Model estabelece um conjunto fixo de propriedades que as instâncias da classe precisam ter (talvez com valores padrão). Essa é uma maneira útil de modelar objetos de dados, mas o armazenamento de dados não exige que as entidades de determinado tipo tenham o mesmo conjunto de propriedades.

Às vezes é útil para uma entidade ter propriedades que não sejam necessariamente como as de outras entidades do mesmo tipo. Tal entidade é representada na API do armazenamento de dados por um modelo "expando". Uma classe de modelo expando é uma subclasse da superclasse Expando. Qualquer valor atribuído a um atributo de uma instância de um modelo expando se torna uma propriedade da entidade do armazenamento de dados, usando o nome do atributo. Essas propriedades são conhecidas como propriedades dinâmicas. As propriedades definidas usando instâncias da classe Propriedade em atributos de classes são propriedades fixas.

Um modelo expando pode ter propriedades tanto fixas quanto dinâmicas. A classe de modelo simplesmente define atributos de classe com objetos de configuração de Propriedade para as propriedades fixas. O aplicativo cria propriedades dinâmicas quando atribui valores a elas.

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

As propriedades dinâmicas não têm definições de propriedade de modelo e, por isso, não são validadas. É possível que toda propriedade dinâmica tenha um dos valores de qualquer um dos tipos de base de armazenamento de dados, incluindo None. Duas entidades do mesmo tipo podem ter tipos diferentes de valores para a mesma propriedade dinâmica, e uma propriedade pode ser deixada sem definição em uma entidade mesmo que seja definida em outra.

Ao contrário de propriedades fixas, as propriedades dinâmicas não precisam existir. Uma propriedade dinâmica com um valor de None é diferente de uma propriedade dinâmica não existente. Se uma instância de modelo expando não tiver um atributo para uma propriedade, a entidade de dados correspondente não terá essa propriedade. É possível excluir uma propriedade dinâmica com a exclusão do atributo.

Atributos com nomes que começam com um sublinhado (_) não serão salvos na entidade do armazenamento de dados. Isso permite o armazenamento de valores na instância do modelo para uso interno temporário, sem afetar os dados salvos com a entidade.

Observação: as propriedades estáticas sempre serão salvas na entidade do armazenamento de dados, não importando se for "Expando", "Model" ou comece com um sublinhado (_).

del p.chess_elo_rating

Uma consulta que use uma propriedade dinâmica em um filtro retorna apenas entidades em que o valor para a propriedade for do mesmo tipo do valor usado na consulta. Da mesma forma, a consulta retorna somente entidades com essa propriedade definida.

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

Observação: o exemplo acima utiliza consultas entre grupos de entidades que talvez retornem resultados obsoletos. Para resultados altamente consistentes, utilize consultas ancestrais nos grupos de entidades.

A classe Expando é uma subclasse de Model e herda todos os métodos correspondentes.

A classe PolyModel

A API do Python inclui outra classe para modelagem de dados que permite a você definir hierarquias de classes e realizar consultas que podem mostrar entidades de determinada classe ou qualquer uma das respectivas subclasses. Tais modelos e consultas são chamados de "polimórficos", porque permitem que instâncias de uma classe sejam resultados da consulta de uma classe pai.

O exemplo a seguir define uma classe Contact, com as subclasses Person e 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()

Esse modelo garante que todas as entidades Person e todas as entidades Company tenham as propriedades phone_number e address e consultas para entidades Contact podem mostrar entidades Person ou Company. Apenas entidades Person têm propriedades mobile_number.

As subclasses podem ser instanciadas da mesma forma que qualquer outra classe 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()

Uma consulta por entidades Contact pode retornar instâncias de Contact, Person ou Company. O código a seguir imprime informações para ambas as entidades criadas acima:

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

Uma consulta por entidades Company mostra apenas instâncias de Company:

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

Por enquanto, os modelos polimórficos não são transmitidos diretamente ao construtor da classe Query. Em vez disso, use o método all(), como no exemplo acima.

Para mais informações sobre como usar modelos polimórficos e como são implementados, veja A classe PolyModel.

Tipos e classes de propriedade

O armazenamento de dados suporta um conjunto fixo de tipos de valor para propriedades de entidade, incluindo strings Unicode, números inteiros, números de ponto flutuante, datas, chaves de entidade, strings de byte (blobs) e vários tipos GData. Cada um dos tipos de valor do armazenamento de dados tem uma classe Property correspondente fornecida pelo módulo google.appengine.ext.db.

Em Tipos e classes de propriedade você encontra todos os tipos de valores compatíveis e as classes Property correspondentes. Vários tipos de valor especiais são descritos abaixo.

Strings e blobs

O armazenamento de dados é compatível com dois tipos de valores para armazenamento de texto: strings de texto curto de até 1.500 bytes de comprimento e strings de texto longo de até um megabyte de comprimento. Strings curtas são indexadas e podem ser usadas em condições de filtro de consulta e ordens de classificação. Strings longas não são indexadas e não podem ser usadas em condições de filtro ou em ordens de classificação.

Um valor de string curta pode ser um valor unicode ou um valor str. Se o valor for uma str, é presumida uma codificação de 'ascii'. Para especificar uma codificação diferente para um valor str, é possível convertê-lo em um valor unicode com o construtor de tipo unicode(), que toma a str e o nome da codificação como argumentos. Strings curtas são modeladas usando a classe 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")

O valor de uma string longa é representado por uma instância db.Text. Seu construtor toma ou um valor unicode ou um valor str e opcionalmente o nome da codificação usada na str. Strings longas são modeladas usando a classe 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")

O armazenamento de dados também é compatível com dois tipos semelhantes de strings de bytes não textuais: db.ByteString e db.Blob. Esses valores são strings de bytes puros e não são tratados como texto codificado (como UTF-8).

Como valores db.StringProperty, os valores db.ByteString são indexados. Como propriedades db.TextProperty, valores db.ByteString são limitados a 1500 bytes. Uma instância ByteString representa uma string de bytes curta e recebe um valor str como um argumento para o respectivo construtor. As strings de bytes são modeladas usando a classe ByteStringProperty.

Como db.Text, um valor db.Blob pode ter até um megabyte, mas não é indexado e não pode ser usado em filtros de consulta ou em ordens de classificação. A classe db.Blob usa um valor str como um argumento para seu construtor, ou você pode atribuir o valor diretamente. Os blobs são modelados usando a classe BlobProperty.

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

obj = MyModel()

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

Listas

Uma propriedade tem diversos valores, representados na API de armazenamento de dados como uma list do Python. A lista pode conter valores de qualquer tipo compatível com o armazenamento de dados. Uma única propriedade de lista pode até ter valores de diferentes tipos.

A ordem geralmente é preservada. Portanto, quando as entidades são retornadas por consultas e pelo get(), os valores das propriedades da lista estão na mesma ordem em que foram armazenados. Há uma exceção: os valores Blob e Text são movidos para o final da lista; No entanto, eles mantêm a ordem original em relação ao outro.

A classe ListProperty modela uma lista e determina que todos os valores na lista sejam de um determinado tipo. Por conveniência, a biblioteca também fornece uma StringListProperty, semelhante 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.

Em uma propriedade de lista, uma consulta com filtros testa cada valor dessa lista individualmente. A entidade corresponderá à consulta apenas no caso de algum valor da lista passar por todos os filtros dessa propriedade. Para mais informações, veja a página Consultas do Datastore.

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

Filtros de consulta operam apenas em membros da lista. Não é possível testar duas listas por similaridade em um filtro de consulta.

Internamente, o armazenamento de dados representa um valor de propriedade de lista como valores múltiplos para a propriedade. Se um valor de propriedade de lista é a lista vazia, a propriedade não terá representação no armazenamento de dados. A API do armazenamento de dados trata essa situação de modo diferente para propriedades estáticas (com ListProperty) e para propriedades dinâmicas:

  • Uma lista vazia pode ser atribuída como valor a uma ListProperty estática. A propriedade não existe no armazenamento de dados, mas a instância de modelo se comporta como se o valor fosse a lista vazia. Uma ListProperty estática não pode ter um valor de None.
  • Uma propriedade dinâmica com um valor list não pode ter um valor de lista vazio atribuído a ela. Entretanto, a propriedade dinâmica pode ter um valor de None e pode ser excluída (usando del).

O modelo ListProperty testa se um valor adicionado à lista é do tipo correto e, caso não seja, lança um BadValueError. O teste ocorre, e possivelmente falha, mesmo que uma entidade previamente armazenada seja recuperada e carregada para o modelo. Uma vez que valores str são convertidos para valores unicode (como texto ASCII) antes do armazenamento, ListProperty(str) é tratado como ListProperty(basestring), o tipo de dados do Python que aceita tanto valores str quanto valores unicode. Você também pode usar StringListProperty() para esse fim.

Para armazenar strings de bytes não textuais, use valores db.Blob. Os bytes de uma string blob são preservados quando são armazenados e recuperados. É possível declarar uma propriedade que seja uma lista de blobs como ListProperty(db.Blob).

As propriedades interagem com ordens de classificação de maneiras incomuns. Para mais detalhes, veja a página Consultas do Datastore.

Referências

Um valor de propriedade pode conter a chave de outra entidade. O valor é uma instância de Key.

A classe ReferenceProperty modela um valor de chave e determina que todos os valores se refiram a entidades de um determinado tipo. Por conveniência, a biblioteca também fornece SelfReferenceProperty, equivalente a uma "ReferenceProperty" relativa ao mesmo tipo da entidade com a propriedade.

A atribuição de uma instância de modelo a uma propriedade ReferenceProperty automaticamente usa a respectiva chave como o 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()

Um valor de propriedade ReferenceProperty pode ser usado como se fosse a instância de modelo da entidade referenciada. Se a entidade referenciada não estiver na memória, o uso da propriedade como uma instância automaticamente busca a entidade do armazenamento de dados. Uma ReferenceProperty também armazena uma chave, mas o uso da propriedade faz com que a entidade relacionada seja carregada.

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

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

Se uma chave aponta para uma entidade inexistente, o acesso à propriedade gera um erro. Se um aplicativo espera que uma referência possa ser inválida, ele pode testá-la para a existência do objeto usando um bloco try/except:

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

A ReferenceProperty tem outro recurso útil: referências anteriores. Quando um modelo tem uma ReferenceProperty para outro modelo, cada entidade referenciada recebe uma propriedade em que o valor é uma Consulta que retorna todas as entidades do primeiro modelo que se referem a ele.

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

O nome da propriedade de referência anterior é modelname_set (com o nome da classe de modelo em letras minúsculas e "_set" adicionado ao final) e pode ser ajustado usando o argumento collection_name no campo do construtor ReferenceProperty.

Se houver diversos valores de ReferenceProperty fazendo referência à mesma classe de modelo, a construção padrão da propriedade de referência anterior emitirá um erro:

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 esse erro, é necessário definir o argumento collection_name de maneira 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")

O referenciamento e o desreferenciamento de instâncias de modelo, a verificação de tipo e as referências anteriores estão disponíveis somente quando é usada a classe de propriedade de modelo ReferenceProperty. As chaves armazenadas como valores de propriedades dinâmicas Expando ou valores de ListProperty não têm esses recursos.