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 automatica nella cache delle entità tramite l'API Memcache. Se attualmente utilizzi la libreria client di DB precedente, leggi la guida alla migrazione da DB a NDB

Panoramica

Un'entità 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 convalidare i valori delle proprietà.

Classi modello

La 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 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 del modello. Ogni attributo class è 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 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 Property.

La classe Model utilizza le istanze di proprietà per convalidare i valori assegnati agli attributi dell'istanza del modello. La convalida del valore della proprietà avviene quando viene creata la prima istanza del modello e quando a un attributo dell'istanza viene assegnato un nuovo valore. In questo modo si garantisce che una proprietà non possa 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 creato utilizzando il costruttore non esiste nel datastore finché non viene "put" per la prima volta.

Nota: come per tutti gli attributi delle classi Python, la configurazione delle proprietà del modello viene inizializzata al momento della prima 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 ulteriori informazioni, vedi Memorizzazione nella cache delle app.

La classe Espandio

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

A volte è utile che un'entità abbia proprietà diverse da quelle di altre entità dello stesso tipo. Tale 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 le istanze delle classi di proprietà 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, 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 diversi tipi di valori per la stessa proprietà dinamica e una può lasciare la proprietà non impostata rispetto alle altre.

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 Espandio 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 il cui nome inizia con un trattino basso (_) non vengono salvati nell'entità datastore. Ciò consente di archiviare i valori nell'istanza del modello per un 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 Espandio, Modello o inizino con un trattino basso (_).

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 la 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 a elevata coerenza, utilizza le query predecessore all'interno dei gruppi di entità.

La classe expando è 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 "polimorfici", perché consentono alle istanze di una classe di essere i 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 tutte le entità Company abbiano proprietà phone_number e address, mentre le query per le entità Contact possono restituire entità Person o Company. Solo Person entità hanno mobile_number proprietà.

È 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 ora, i modelli polimorfi non devono essere passati direttamente al costruttore della classe Query. Utilizza invece il metodo all(), come nell'esempio precedente.

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

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 valore 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 di testo: stringhe di testo brevi di 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 negli ordinamento. Le stringhe lunghe non vengono indicizzate e non possono essere utilizzate nelle condizioni di filtro o negli ordinamenti.

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 lunga è rappresentato da un'istanza db.Text. Il suo costruttore accetta un valore unicode o un valore str e, facoltativamente, il nome della codifica utilizzata nel campo 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 nel caso di db.Text, un valore db.Blob può raggiungere anche un megabyte, ma non viene indicizzato e non può essere utilizzato nei filtri di query o negli ordinamenti. 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 list Python. L'elenco può contenere valori di qualsiasi tipo supportato dal datastore. 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 di quando erano stati archiviati. Esiste un'eccezione a questa regola: i valori Blob e Text vengono spostati alla fine dell'elenco, ma mantengono il loro ordine originale l'uno rispetto all'altro.

La classe ListProperty modella un elenco e applica che tutti i valori nell'elenco siano di un determinato tipo. Per praticità, 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 verifica singolarmente ogni valore nell'elenco. L'entità corrisponderà alla query solo se un valore nell'elenco supera 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 di query operano 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 è vuoto, la proprietà non ha alcuna rappresentazione nel datastore. L'API datastore tratta questa situazione in modo diverso per le proprietà statiche (con ListProperty) e le proprietà dinamiche:

  • Un ListProperty statico può essere assegnato all'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. Un ListProperty statico non può avere il valore None.
  • A una proprietà dinamica con un valore list non è possibile assegnare un valore di elenco vuoto. Tuttavia, può avere il valore 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 BadValueError. Questo test viene eseguito (e potenzialmente non riesce) anche quando un'entità precedentemente memorizzata 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 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; per informazioni dettagliate, consulta la pagina Query Datastore.

Riferimenti

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

La classe ReferenceProperty modella una coppia chiave-valore 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. Anche in ReferenceProperty viene archiviata una chiave, ma l'utilizzo della proprietà 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 potrebbe 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 un campo ReferenceProperty a un altro modello, ogni entità di 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à back-reference è 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 per il 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")

I riferimenti automatici e il dereferenziamento delle istanze del modello, il controllo del tipo e i backreference 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.