Modellazione dei dati in Python

Nota: gli sviluppatori che creano nuove applicazioni sono vivamente incoraggiati a utilizzare la libreria client NDB, che offre diversi vantaggi rispetto a questa libreria client, come la memorizzazione nella cache automatica delle entità tramite l'API Memcache. Se attualmente utilizzi la libreria client DB precedente, leggi la Guida alla migrazione dal database a NDB

Panoramica

Un'entità datastore ha una chiave e un insieme di proprietà. Un'applicazione utilizza l'API di datastore per definire i modelli di dati e creare istanze di questi modelli da archiviare come entità. I modelli forniscono una struttura comune alle entità create dall'API e possono definire regole per convalidare i valori delle proprietà.

Classi di modelli

Classe del modello

Un'applicazione descrive i tipi di dati che utilizza con i modelli. Un modello è una classe Python che eredita dalla classe Model. La classe del modello definisce una nuova entità Kind di datastore e le proprietà che il tipo dovrebbe prendere. Il nome del tipo è definito dal nome della classe con un'istanza che eredita da db.Model.

Le proprietà del modello vengono definite utilizzando gli attributi di classe nella classe del modello. Ogni attributo classe è un'istanza di una sottoclasse della classe Property, in genere una delle classi di proprietà fornite. Un'istanza di proprietà contiene la configurazione della proprietà, ad esempio se questa è necessaria o meno affinché l'istanza sia valida o un valore predefinito da utilizzare per l'istanza se non viene fornito alcun valore.

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

Un'entità di uno dei tipi di entità definiti è rappresentata nell'API da un'istanza della classe del modello corrispondente. L'applicazione può creare una nuova entità chiamando il costruttore della classe. L'applicazione accede alle proprietà dell'entità e le manipola utilizzando gli attributi dell'istanza. Il costruttore dell'istanza del modello accetta i valori iniziali delle proprietà come argomenti delle parole chiave.

from google.appengine.api import users

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

Nota: gli attributi della classe del modello sono la configurazione delle proprietà del modello, i cui valori sono istanze Proprietà. Gli attributi dell'istanza del modello sono i valori effettivi della proprietà, i cui valori sono del tipo accettato dalla classe Proprietà.

La classe Modello utilizza le istanze di proprietà per convalidare i valori assegnati agli attributi dell'istanza modello. La convalida del valore della proprietà si verifica quando viene creata per la prima volta un'istanza del modello e quando viene assegnato un nuovo valore all'attributo di un'istanza. Ciò garantisce che una proprietà non possa mai avere un valore non valido.

Poiché la convalida avviene durante la creazione dell'istanza, qualsiasi proprietà configurata come obbligatoria deve essere inizializzata nel costruttore. In questo esempio, name e type sono valori obbligatori, quindi i loro valori iniziali sono specificati nel costruttore. Il modello weight_in_pounds non è richiesto dal modello, pertanto all'inizio non è stato assegnato e gli viene assegnato un valore in un secondo momento.

Un'istanza di un modello creato utilizzando il costruttore non esiste nel datastore fino a quando non viene "put" per la prima volta.

Nota. Come per tutti gli attributi di classe Python, la configurazione della proprietà del modello viene inizializzata alla prima importazione dello script o del modulo. Poiché App Engine memorizza nella cache i moduli importati tra le richieste, la configurazione dei moduli può essere inizializzata durante una richiesta per un utente e riutilizzata durante una richiesta per un altro. Non inizializzare la configurazione della proprietà del modello, ad esempio i valori predefiniti, con dati specifici della richiesta o dell'utente corrente. Per ulteriori informazioni, consulta Memorizzazione nella cache delle app.

La classe expando

Un modello definito utilizzando la classe Modello stabilisce un insieme fisso di proprietà che ogni istanza della classe deve avere (forse con valori predefiniti). Si tratta di un modo utile per modellare gli oggetti di dati, ma il datastore non richiede che ogni entità di un determinato tipo abbia lo stesso insieme di proprietà.

A volte è utile che un'entità abbia proprietà che non sono necessariamente simili a quelle di altre entità dello stesso tipo. Questa entità è rappresentata nell'API del datastore da un modello "expando". Una classe del modello expando sottoclasse la superclasse Espandio. Qualsiasi valore assegnato a un attributo di un'istanza di un modello expando diventa una proprietà dell'entità datastore, utilizzando il nome dell'attributo. Queste proprietà sono note come proprietà dinamiche. Le proprietà definite utilizzando le istanze della classe di proprietà negli attributi della classe sono proprietà fisse.

Un modello espandibile può avere proprietà fisse e dinamiche. La classe del modello imposta semplicemente gli attributi della classe con gli oggetti di configurazione della proprietà per le proprietà fisse. L'applicazione crea proprietà dinamiche quando assegna loro valori.

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

Poiché le proprietà dinamiche non hanno definizioni di proprietà modello, le proprietà dinamiche non vengono convalidate. Qualsiasi proprietà dinamica può avere un valore di qualsiasi tipo di base di datastore, tra cui None. Due entità dello stesso tipo possono avere tipi diversi di valori per la stessa proprietà dinamica e una proprietà può non essere impostata mentre l'altra viene impostata.

A differenza delle proprietà fisse, le proprietà dinamiche non devono necessariamente esistere. Una proprietà dinamica con valore None è diversa da una proprietà dinamica inesistente. Se un'istanza del modello expando non ha un attributo per una proprietà, l'entità dati corrispondente non possiede questa proprietà. Puoi eliminare una proprietà dinamica eliminando l'attributo.

Gli attributi i cui nomi iniziano con un trattino basso (_) non vengono salvati nell'entità datastore. In questo modo puoi archiviare i valori nell'istanza del modello per uso interno temporaneo senza influire sui dati salvati con l'entità.

Nota: le proprietà statiche verranno sempre salvate nell'entità datastore, indipendentemente dal fatto che si tratti di espandere, modello o che inizi con un trattino basso (_).

del p.chess_elo_rating

Una query che utilizza una proprietà dinamica in un filtro restituisce solo entità il cui valore per la proprietà è dello stesso tipo del valore utilizzato nella query. Allo stesso modo, la query restituisce solo le entità con quella proprietà impostata.

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:l'esempio precedente utilizza query relative a gruppi di entità, il che potrebbe restituire risultati obsoleti. Per risultati a elevata coerenza, utilizza le query predecessori all'interno dei gruppi di entità.

La classe Espandio è una sottoclasse della classe Model e eredita tutti i suoi metodi.

Classe PolyModel

L'API Python include un'altra classe per la modellazione dei dati che consente di definire gerarchie di classi ed eseguire query che possono restituire entità di una determinata classe o di una qualsiasi delle sue sottoclassi. Questi modelli e query vengono chiamati "polimorfici" perché consentono alle istanze di una classe di essere risultati per una query di una classe padre.

L'esempio seguente definisce una classe Contact, con le sottoclassi 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()

Questo modello garantisce che tutte le entità Person e Company abbiano proprietà phone_number e address e le query per entità Contact possono restituire entità Person o Company. Solo Person entità hanno proprietà mobile_number.

Puoi creare un'istanza delle sottoclassi come qualsiasi altra classe di modello:

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 query per le entità Contact può restituire istanze di Contact, Person o Company. Il codice seguente stampa informazioni per entrambe le entità create in precedenza:

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

Una query per le entità Company restituisce solo istanze di Company:

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

Per ora, i modelli polimorfici non devono essere passati direttamente al costruttore della classe Query. Utilizza invece il metodo all(), come nell'esempio precedente.

Per saperne di più su come utilizzare i modelli polimorfici e come vengono implementati, vedi La classe PolyModel.

Classi e tipi di proprietà

Il datastore supporta un insieme fisso di tipi di valori per le proprietà delle entità, tra cui stringhe Unicode, numeri interi, numeri in virgola mobile, date, chiavi di entità, stringhe di byte (blob) e vari tipi di GData. A ogni tipo di valore del datastore corrisponde una classe di proprietà corrispondente fornita dal modulo google.appengine.ext.db.

Tipi e classi di proprietà descrive tutti i tipi di valori supportati e le rispettive classi di proprietà. Di seguito sono descritti diversi tipi di valori speciali.

Stringhe e BLOB

Il datastore supporta due tipi di valori per l'archiviazione del testo: stringhe di testo brevi con una lunghezza massima di 1500 byte e stringhe di testo lunghe fino a un megabyte. Le stringhe brevi vengono indicizzate e possono essere utilizzate nelle condizioni di filtro delle query e nell'ordinamento. Le stringhe lunghe non vengono indicizzate e non possono essere utilizzate nelle condizioni di filtro o negli ordini di ordinamento.

Un valore stringa breve può essere un valore unicode o un valore str. Se il valore è str, viene utilizzata la codifica 'ascii'. Per specificare una codifica diversa per un valore str, puoi convertirlo in un valore unicode con il costruttore di tipo unicode(), che prende str e il nome della codifica come argomenti. Le stringhe brevi possono essere modellate utilizzando la 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")

Un valore di stringa lunga è rappresentato da un'istanza db.Text. Il relativo costruttore accetta un valore unicode o str e, facoltativamente, il nome della codifica utilizzata in str. Le stringhe lunghe possono essere modellate utilizzando la 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")

Il datastore supporta anche due tipi simili per le stringhe di byte non di testo: db.ByteString e db.Blob. Questi valori sono stringhe di byte non elaborati e non vengono trattati come testo codificato (ad esempio UTF-8).

Come i valori db.StringProperty, anche i valori db.ByteString sono indicizzati. Come per le proprietà db.TextProperty, i valori db.ByteString sono limitati a 1500 byte. Un'istanza ByteString rappresenta una breve stringa di byte e prende un valore str come argomento del suo costruttore. Le stringhe di byte sono modellate utilizzando la classe ByteStringProperty.

Come db.Text, un valore db.Blob può avere le dimensioni massime di un megabyte, ma non viene indicizzato e non può essere utilizzato nei filtri delle query o negli ordinamenti. La classe db.Blob prende un valore str come argomento al suo costruttore oppure puoi assegnare il valore direttamente. I BLOB vengono modellati utilizzando la classe BlobProperty.

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

obj = MyModel()

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

Elenchi

Una proprietà può avere più valori, rappresentati nell'API del datastore come list Python. L'elenco può contenere uno qualsiasi dei tipi di valori supportati dal datastore. Una singola proprietà elenco può anche avere valori di tipi diversi.

In genere l'ordine viene conservato, quindi quando le entità vengono restituite dalle query e da get(), i valori delle proprietà dell'elenco sono nello stesso ordine in cui sono stati archiviati. C'è un'eccezione a questa regola: i valori Blob e Text vengono spostati alla fine dell'elenco, ma mantengono l'ordine originale l'uno rispetto all'altro.

La classe ListProperty modella un elenco e impone che tutti i valori nell'elenco siano di un determinato tipo. Per praticità, la libreria fornisce anche StringListProperty, simile 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 query con filtri su una proprietà elenco verifica individualmente ogni valore nell'elenco. L'entità corrisponderà alla query solo se un valore nell'elenco supera tutti i filtri della proprietà. Per ulteriori informazioni, consulta la pagina Query sul 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")

I filtri di query operano solo sui membri dell'elenco. Non è possibile testare la somiglianza tra due elenchi in un filtro delle query.

Internamente, il datastore rappresenta un valore di una proprietà elenco come più valori per la proprietà. Se il valore di una proprietà elenco è l'elenco vuoto, la proprietà non ha alcuna rappresentazione nel datastore. L'API del datastore tratta questa situazione in modo diverso per le proprietà statiche (con ListProperty) e dinamiche:

  • Un elenco ListProperty statico può essere assegnato come valore all'elenco vuoto. La proprietà non esiste nel datastore, ma l'istanza del modello si comporta come se il valore fosse l'elenco vuoto. Un valore ListProperty statico non può avere un valore di None.
  • A una proprietà dinamica con un valore list non può essere assegnato un valore di elenco vuoto. Tuttavia, può avere un valore pari a None e può essere eliminato (utilizzando del).

Il modello ListProperty verifica che un valore aggiunto all'elenco sia del tipo corretto e, in caso contrario, genera un errore BadValueError. Questo test si verifica (e potenzialmente non riesce) anche quando un'entità archiviata in precedenza viene recuperata e caricata nel modello. Poiché i valori str vengono convertiti in valori unicode (come testo ASCII) prima dell'archiviazione, ListProperty(str) viene trattato come ListProperty(basestring), il tipo di dati Python che accetta entrambi i valori str e unicode. A questo scopo, puoi anche utilizzare StringListProperty().

Per memorizzare stringhe di byte non di testo, utilizza i valori db.Blob. I byte di una stringa blob vengono conservati quando vengono archiviati e recuperati. Puoi dichiarare come ListProperty(db.Blob) una proprietà inclusa in un elenco di blob.

Le proprietà elenco possono interagire con gli ordinamenti in modi insoliti; per informazioni dettagliate, consulta la pagina Query del datastore.

Riferimenti

Il valore di una proprietà può contenere la chiave di un'altra entità. Il valore è un'istanza Chiave.

La classe ReferenceProperty modella un valore chiave e impone che tutti i valori facciano riferimento a entità di un determinato tipo. Per praticità, la libreria fornisce anche SelfReferenceProperty, equivalente a una proprietà ReferenceProperty che fa riferimento allo stesso tipo dell'entità con la proprietà.

L'assegnazione di un'istanza del modello a una proprietà ReferenceProperty utilizza automaticamente la relativa chiave come valore.

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

È possibile utilizzare un valore di proprietà ReferenceProperty come se fosse l'istanza del modello dell'entità di riferimento. Se l'entità a cui viene fatto riferimento non è in memoria, l'utilizzo della proprietà come istanza consente di recuperare automaticamente l'entità dal datastore. Anche un oggetto ReferenceProperty archivia una chiave, ma l'utilizzo della proprietà determina il caricamento dell'entità correlata.

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 una chiave rimanda a un'entità inesistente, l'accesso alla proprietà genera un errore. Se un'applicazione prevede che un riferimento possa non essere valido, può verificare l'esistenza dell'oggetto utilizzando un blocco try/except:

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

ReferenceProperty dispone di un'altra pratica funzione: i riferimenti a ritroso. Quando un modello include una proprietà di riferimento per un altro modello, ogni entità a cui viene fatto riferimento riceve una proprietà il cui valore è una query che restituisce tutte le entità del primo modello che vi fanno riferimento.

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

Il nome della proprietà di backreference viene impostato in modo predefinito su modelname_set (con il nome della classe del modello in lettere minuscole e "_set" aggiunto alla fine) e può essere modificato utilizzando l'argomento collection_name del costruttore ReferenceProperty.

Se sono presenti più valori ReferenceProperty che fanno riferimento alla stessa classe di modello, la costruzione predefinita della proprietà di backreference genera un errore:

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)

Per evitare questo errore, devi impostare esplicitamente l'argomento 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")

Il riferimento automatico e il deriferimento delle istanze del modello, il controllo dei tipi e i backreference sono disponibili solo utilizzando la classe di proprietà del modello ReferenceProperty. Le chiavi memorizzate come valori delle proprietà dinamiche Growo o dei valori ListProperty non presentano queste funzionalità.