Modellazione dei dati in Python

Nota: gli sviluppatori che creano nuove applicazioni sono vivamente invitati a utilizzare la libreria client NDB, che offre diversi vantaggi rispetto a questa libreria client, ad esempio la memorizzazione nella cache automatica delle entità tramite l'API Memcache. Se al momento utilizzi la libreria client DB precedente, leggi la guida alla migrazione da DB a NDB

Panoramica

Un'entità di Datastore ha una chiave e un insieme di proprietà. Un'applicazione utilizza l'API 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 la convalida dei valori delle proprietà.

Classi modello

La classe Model

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 un nuovo tipo di entità datastore e le proprietà che questo tipo deve assumere. Il nome del tipo è definito dal nome della classe creata con un'istanza che eredita da db.Model.

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

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 ne manipola le proprietà utilizzando gli attributi dell'istanza. Il costruttore dell'istanza del modello accetta i valori iniziali per le 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 per le proprietà del modello, i cui valori sono istanze di Property. Gli attributi dell'istanza del modello sono i valori effettivi delle proprietà, i cui valori sono del tipo accettato dalla classe Property.

La classe Model utilizza le istanze Property per convalidare i valori assegnati agli attributi dell'istanza del modello. La convalida del valore della proprietà avviene quando viene creata per la prima volta un'istanza del modello e quando a un attributo dell'istanza viene assegnato un nuovo valore. In questo modo, una proprietà non può mai avere un valore non valido.

Poiché la convalida avviene quando viene creata l'istanza, qualsiasi proprietà configurata in modo da essere obbligatoria deve essere inizializzata nel costruttore. In questo esempio, name e type sono valori obbligatori, quindi i rispettivi valori iniziali vengono specificati nel costruttore. weight_in_pounds non è richiesto dal modello, quindi all'inizio non è stato assegnato e un valore viene assegnato in un secondo momento.

Un'istanza di un modello creata utilizzando il costruttore non esiste nel datastore finché non viene "inserita" per la prima volta.

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

The Expando Class

Un modello definito utilizzando la classe Model 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 dati, ma il data store 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 Datastore da un modello "expando". Una classe di modello Espandio crea sottoclassi della superclasse Espandio. Qualsiasi valore assegnato a un attributo di un'istanza di un modello espandibilio diventa una proprietà dell'entità datastore, utilizzando il nome dell'attributo. Queste proprietà sono note come proprietà dinamiche. Le proprietà definite utilizzando istanze di classe Property negli attributi di classe sono proprietà fisse.

Un modello di espansione può avere proprietà sia fisse sia dinamiche. La classe del modello imposta semplicemente gli attributi di classe con oggetti di configurazione Proprietà per le proprietà fisse. L'applicazione crea proprietà dinamiche quando assegna loro dei 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à del modello, non vengono convalidate. Qualsiasi proprietà dinamica può avere un valore di uno dei tipi di base del data store, incluso None. Due entità dello stesso tipo possono avere tipi di valori diversi per la stessa proprietà dinamica e una può lasciare una proprietà non impostata che l'altra imposta.

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

Gli attributi i cui nomi iniziano con un trattino basso (_) non vengono salvati nell'entità del 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à del datastore, indipendentemente dal fatto che si tratti di Expando, Model o che inizino con un'underscore (_).

del p.chess_elo_rating

Una query che utilizza una proprietà dinamica in un filtro restituisce solo le entità il cui valore per la proprietà è dello stesso tipo del valore utilizzato nella query. Analogamente, 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 tra gruppi di entità, il che potrebbe restituire risultati non aggiornati. Per risultati fortemente coerenti, utilizza le query sugli antenati all'interno dei gruppi di entità.

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

La classe PolyModel

L'API Python include un'altra classe per la modellazione dei dati che consente di definire le 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 sono chiamati "polimorfi" perché consentono alle istanze di una classe di essere i risultati di una query di una classe principale.

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 tutte le entità Company abbiano proprietà phone_number e address, mentre le query per le entità Contact possono restituire entità Person o Company. Solo le entità Person hanno proprietà mobile_number.

È possibile creare un'istanza per le sottoclassi come per 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 seguente codice stampa le informazioni per entrambe le entità create sopra:

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 le istanze di Company:

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

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

Per ulteriori informazioni su come utilizzare i modelli polimorfici e su come vengono implementati, consulta la classe PolyModel.

Classi e tipi di proprietà

Il datastore supporta un set 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. Ciascun tipo di valore del datastore ha una classe Property corrispondente fornita dal modulo google.appengine.ext.db.

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

Stringhe e blob

Datastore supporta due tipi di valori per l'archiviazione di testo: stringhe di testo brevi con una lunghezza massima di 1500 byte e stringhe di testo lunghe con una lunghezza massima di 1 megabyte. Le stringhe brevi vengono indicizzate e possono essere utilizzate nelle condizioni di filtro delle query e negli ordinamento. Le stringhe lunghe non vengono indicizzate e non possono essere utilizzate nelle condizioni dei filtri o negli ordini di ordinamento.

Un valore di stringa breve può essere un valore unicode o un valore str. Se il valore è str, viene usata 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 lungo è 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 non di byte 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 per il suo costruttore. Le stringhe di byte vengono modellate utilizzando la classe ByteStringProperty.

Come db.Text, un valore db.Blob può avere una dimensione massima di 1 megabyte, ma non è indicizzato e non può essere utilizzato nei filtri delle query o negli ordini di ordinamento. La classe db.Blob prende un valore str come argomento per il 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 datastore come un list di Python. L'elenco può contenere valori di qualsiasi tipo supportato dal data store. Una singola proprietà elenco può anche avere valori di tipi diversi.

L'ordine viene generalmente mantenuto, 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. Esiste un'eccezione: i valori Blob e Text vengono spostati alla fine dell'elenco, ma mantengono il loro ordine originale rispetto all'altro.

La classe ListProperty modella un elenco e impone che tutti i valori dell'elenco siano di un determinato tipo. Per comodità, la libreria fornisce anche StringListProperty, come 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 testa ogni valore dell'elenco individualmente. L'entità corrisponderà alla query solo se esiste un valore nell'elenco utilizza tutti i filtri su quella proprietà. Per ulteriori informazioni, consulta la pagina Query 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 delle query agiscono solo sui membri dell'elenco. Non è possibile testare due elenchi per verificare la somiglianza in un filtro di query.

Internamente, il datastore rappresenta un valore di 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 data store. L'API Datastore tratta questa situazione in modo diverso per le proprietà statiche (con ListProperty) e le proprietà dinamiche:

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

Il modello ListProperty verifica che un valore aggiunto all'elenco sia del tipo corretto e genera un errore BadValueError se non lo è. Questo test viene eseguito (e potenzialmente non va a buon fine) anche quando un'entità memorizzata 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 sia i valori str che unicode. Per farlo, puoi utilizzare anche StringListProperty().

Per archiviare 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 una proprietà che è un elenco di blob come ListProperty(db.Blob).

Le proprietà elenco possono interagire con gli ordini di ordinamento in modi insoliti; consulta la pagina Query Datastore per maggiori dettagli.

Riferimenti

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

La classe ReferenceProperty modella un valore chiave e impone che tutti i valori facciano riferimento a entità di un determinato tipo. Per comodità, la libreria fornisce anche SelfReferenceProperty, equivalente a un 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()

Il valore di una proprietà ReferenceProperty può essere utilizzato 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 recupera automaticamente l'entità dal datastore. Una proprietà ReferenceProperty memorizza anche una chiave, ma il suo utilizzo comporta 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 ha un'altra utile funzionalità: i backreference. Quando un modello ha una proprietà ReferenceProperty a un altro modello, a ogni entità a cui viene fatto riferimento viene assegnata una proprietà il cui valore è una query che restituisce tutte le entità del primo modello che fanno riferimento a questa.

# 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 riferimento a ritroso è modelname_set per impostazione predefinita (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 hai più valori ReferenceProperty che fanno riferimento alla stessa classe di modello, la costruzione predefinita della proprietà back-reference 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 e il dereferenziamento automatici delle istanze del modello, il controllo del tipo e i riferimenti indiretti sono disponibili solo utilizzando la classe della proprietà del modello ReferenceProperty. Le chiavi memorizzate come valori delle proprietà dinamiche Espandio o dei valori ListProperty non dispongono di queste caratteristiche.