2010-08-19 9 views
5

Ho riscontrato un po 'di problemi in Google App Engine, assicurandomi che i miei dati siano corretti quando si utilizza una relazione antenato senza nomi chiave.Come posso garantire l'integrità dei dati per gli oggetti nel motore di app di google senza utilizzare i nomi chiave?

Mi spiego un po 'di più: ho una controllante categoria, e voglio creare un'entità figlio voce. Mi piacerebbe creare una funzione che prende il nome di una categoria e il nome di una voce e crea entrambe le entità se non esistono. Inizialmente ho creato una transazione e creato entrambi nella transazione, se necessario, utilizzando un nome chiave, e questo ha funzionato bene. Tuttavia, mi sono reso conto che non volevo usare il nome come chiave come può essere necessario cambiare, e ho cercato nel mio transazione per fare questo:

def add_item_txn(category_name, item_name): 
    category_query = db.GqlQuery("SELECT * FROM Category WHERE name=:category_name", category_name=category_name) 
category = category_query.get() 
if not category: 
    category = Category(name=category_name, count=0) 

item_query = db.GqlQuery("SELECT * FROM Item WHERE name=:name AND ANCESTOR IS :category", name=item_name, category=category) 
item_results = item_query.fetch(1) 
if len(item_results) == 0: 
    item = Item(parent=category, name=name) 

db.run_in_transaction(add_item_txn, "foo", "bar") 

Quello che ho trovato quando ho provato a correre questo è tale App Engine lo rifiuta poiché non consente di eseguire una query in una transazione: Only ancestor queries are allowed inside transactions.

Guardando il example Google dà su come affrontare questo:

def decrement(key, amount=1): 
    counter = db.get(key) 
    counter.count -= amount 
    if counter.count < 0: # don't let the counter go negative 
     raise db.Rollback() 
    db.put(counter) 

q = db.GqlQuery("SELECT * FROM Counter WHERE name = :1", "foo") 
counter = q.get() 
db.run_in_transaction(decrement, counter.key(), amount=5) 

ho tentato di spostare il mio prendere della categoria a prima che la transazione:

def add_item_txn(category_key, item_name): 
    category = category_key.get() 
    item_query = db.GqlQuery("SELECT * FROM Item WHERE name=:name AND ANCESTOR IS :category", name=item_name, category=category) 
    item_results = item_query.fetch(1) 
    if len(item_results) == 0: 
     item = Item(parent=category, name=name) 

category_query = db.GqlQuery("SELECT * FROM Category WHERE name=:category_name", category_name="foo") 
category = category_query.get() 
if not category: 
    category = Category(name=category_name, count=0) 
db.run_in_transaction(add_item_txn, category.key(), "bar") 

questo apparentemente ha funzionato, ma io trovato quando ho eseguito questo con un numero di richieste che ho creato categorie duplicate, che ha senso, come la query viene interrogata al di fuori della transazione e più richieste potrebbero creare più categorie.

Qualcuno ha idea di come sia possibile creare correttamente queste categorie? Ho provato a mettere la creazione della categoria in una transazione, ma ho ricevuto di nuovo l'errore sulle query degli antenati.

Grazie!

Simon

+0

Stai cercando di evitare di utilizzare i nomi chiave per le categorie? Il modo standard per garantire l'unicità è usando db.get_or_insert (parent, key_name). – mahmoud

+0

Sì, il motivo per cui sto evitando è perché il nome della categoria può cambiare, e se cambio il nome ma ho la chiave come il vecchio nome temo che le cose saranno molto confuse ... – Simon

risposta

2

Ecco un approccio per risolvere il problema. Non è un approccio ideale in molti modi, e sinceramente spero che qualcun altro AppEnginer troverà una soluzione migliore di quella che ho io. Altrimenti, provalo.

Il mio approccio utilizza la seguente strategia: crea entità che fungono da alias per le entità di categoria. Il nome della categoria può cambiare, ma l'entità alias manterrà la sua chiave, e possiamo usare gli elementi della chiave dell'alias per creare un keyname per le entità della categoria, quindi saremo in grado di cercare una categoria in base al suo nome, ma il suo deposito è disaccoppiato dal suo nome.

Gli alias sono tutti memorizzati in un singolo gruppo di entità e questo ci consente di utilizzare una query di antenato compatibile con le transazioni, in modo che possiamo cercare o creare un CategoryAlias ​​senza rischiare che vengano create più copie.

Quando voglio cercare o creare una combinazione di Categoria ed elemento, posso usare il nome chiave della categoria per generare automaticamente una chiave all'interno della transazione, e siamo autorizzati a ottenere un'entità tramite la sua chiave all'interno di una transazione.

class CategoryAliasRoot(db.Model): 
    count = db.IntegerProperty() 
    # Not actually used in current code; just here to avoid having an empty 
    # model definition. 

    __singleton_keyname = "categoryaliasroot" 

    @classmethod 
    def get_instance(cls): 
      # get_or_insert is inherently transactional; no chance of 
      # getting two of these objects. 
     return cls.get_or_insert(cls.__singleton_keyname, count=0) 

class CategoryAlias(db.Model): 
    alias = db.StringProperty() 

    @classmethod 
    def get_or_create(cls, category_alias): 
     alias_root = CategoryAliasRoot.get_instance() 
     def txn(): 
      existing_alias = cls.all().ancestor(alias_root).filter('alias = ', category_alias).get() 
      if existing_alias is None: 
       existing_alias = CategoryAlias(parent=alias_root, alias=category_alias) 
       existing_alias.put() 

      return existing_alias 

     return db.run_in_transaction(txn) 

    def keyname_for_category(self): 
     return "category_" + self.key().id 

    def rename(self, new_name): 
     self.alias = new_name 
     self.put() 

class Category(db.Model): 
    pass 

class Item(db.Model): 
    name = db.StringProperty() 

def get_or_create_item(category_name, item_name): 

    def txn(category_keyname): 
     category_key = Key.from_path('Category', category_keyname) 

     existing_category = db.get(category_key) 
     if existing_category is None: 
      existing_category = Category(key_name=category_keyname) 
      existing_category.put() 

     existing_item = Item.all().ancestor(existing_category).filter('name = ', item_name).get() 
     if existing_item is None: 
      existing_item = Item(parent=existing_category, name=item_name) 
      existing_item.put() 

     return existing_item 

    cat_alias = CategoryAlias.get_or_create(category_name) 
    return db.run_in_transaction(txn, cat_alias.keyname_for_category()) 

Caveat emptor: non ho testato questo codice. Ovviamente, dovrai cambiarlo per adattarlo ai tuoi modelli attuali, ma penso che i principi che usa siano solidi.

AGGIORNAMENTO: Simon, nel tuo commento, per lo più hai l'idea giusta; anche se c'è un'importante sottigliezza da non perdere. Noterai che le entità di categoria non sono figli della radice fittizia. Non condividono un genitore e sono essi stessi le entità radice nei propri gruppi di entità. Se le entità di categoria hanno tutte lo stesso genitore, ciò renderebbe un gruppo di entità gigante e avresti un incubo di prestazioni perché ogni gruppo di entità può avere solo una transazione in esecuzione su di esso alla volta.

Piuttosto, le entità CategoryAlias ​​sono i figli dell'entità radice fittizia. Ciò mi consente di interrogare all'interno di una transazione, ma il gruppo di entità non diventa troppo grande perché gli articoli che appartengono a ciascuna categoria non sono collegati a CategoryAlias.

Inoltre, i dati nell'entità CategoryAlias ​​possono essere modificati senza modificare la chiave di accesso e sto utilizzando la chiave di Alias ​​come punto dati per generare un nome chiave che può essere utilizzato nella creazione delle entità di categoria effettive. Quindi, posso cambiare il nome che è memorizzato in CategoryAlias ​​senza perdere la mia capacità di abbinare quell'entità con la stessa Categoria.

+0

Adam, Grazie per la soluzione - molto apprezzata. L'ho letto alcune volte e, a quanto ho capito, quello che stai facendo qui è la creazione di un'entità root fittizia per garantire che quando la categoria viene recuperata venga recuperata da un gruppo di entità (e quindi consentita in una transazione) - è quella destra? grazie! -simon – Simon

+0

@Simon, ho modificato la mia risposta in risposta al tuo commento. –

+0

Ci si distingue molto: l'entità padre è distinta dalla categoria, grazie per la risposta! – Simon

0

Un paio di cose da notare (penso che siano probabilmente solo errori di battitura) -

  1. La prima linea delle chiamate di metodo transazionali get() su una chiave - questo è non una funzione documentata. Non è necessario avere comunque l'oggetto categoria effettivo nella funzione - la chiave è sufficiente in entrambi i punti in cui si utilizza l'entità della categoria.

  2. Lei non sembrano essere chiamata put() su entrambi della categoria o la voce (ma dato che si dice che si stanno ottenendo i dati nell'archivio dati, presumo che hai lasciato questo fuori per brevità?)

per quanto riguarda la soluzione va - si potrebbe tentare di aggiungere un valore in memcache con una scadenza ragionevole -

if memcache.add("category.%s" % category_name, True, 60): create_category(...) 

Questo almeno vieta di creare multipli. È ancora un po 'complicato sapere cosa fare se la query non restituisce la categoria, ma non è possibile afferrare il blocco da memcache. Ciò significa che la categoria è in fase di creazione.

Se la richiesta di origine proviene dalla coda delle attività, è sufficiente generare un'eccezione in modo che l'attività venga rieseguita.

Altrimenti si potrebbe aspettare un po 'e interrogare nuovamente, anche se questo è un po' rischioso.

Se la richiesta proviene dall'utente, si potrebbe dire che c'è stato un conflitto e riprovare.

+0

grazie per aver catturato i miei refusi - piuttosto che copiare la mia esatta funzione per brevità l'ho pesantemente modificata e ho fatto questi :( Questa è una soluzione interessante, è memcache garantito di non scontrarsi? – Simon

+0

memcache.add() garantisce l'atomicità - cioè puoi solo aggiungere una chiave una volta - garantito. Ovviamente con il codice sopra riportato, la voce scade tra 60 secondi e puoi quindi prendere nuovamente il blocco. http://code.google.com/appengine/docs/python/memcache/clientclass.html # Client_add – hawkett

Problemi correlati