2010-08-19 17 views
5

我在Google App Engine中遇到了一些麻煩,確保在使用沒有鍵名的祖先關係時我的數據是正確的。如何在不使用密鑰名稱的情況下確保Google應用引擎中的對象的數據完整性?

讓我解釋一下:我有一個父實體類別,我想創建一個子實體項目。我想創建一個採用類別名稱和項目名稱的函數,並在兩個實體不存在的情況下創建這兩個實體。最初,我創建了一個事務,並在需要時使用密鑰名稱在事務中創建了這兩個事務,並且這很好地工作。但是,我意識到,我並不想使用的名稱爲關鍵,因爲它可能需要改變,而且我想我的事務中要做到這一點:

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

我發現,當我試圖運行,這是App Engine會拒絕此操作,因爲它不會讓您在事務中運行查詢:Only ancestor queries are allowed inside transactions

望着example谷歌提供了關於如何解決此:

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) 

我試圖在交易前將我的類別取到:

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

這看似工作,但我當我用一些請求運行這個請求時,發現有重複的類別被創建,這是有道理的,因爲在事務之外查詢類別,並且多個請求可以創建多個類別。

有沒有人有任何想法我可以創建這些類別正確嗎?我嘗試將類別創建放入事務中,但只收到了有關祖先查詢的錯誤。

謝謝!

西蒙

+0

你是否試圖避免使用鍵名分類?確保唯一性的標準方法是使用db.get_or_insert(parent,key_name)。 – mahmoud 2010-08-19 20:05:36

+0

是的,這個原因我避免它是因爲類別名稱可以改變,如果我改變名稱,但有關鍵作爲舊名稱,恐怕事情會很混亂... – Simon 2010-08-19 21:51:46

回答

2

以下是解決問題的方法。這在很多方面都不是一個理想的方法,我真誠地希望其他AppEnginer能夠拿出比我更好的解決方案。如果沒有,請嘗試一下。

我的方法使用以下策略:它創建充當類別實體的別名的實體。類別名稱可以更改,但別名實體將保留其密鑰,並且我們可以使用別名密鑰的元素爲您的類別實體創建一個密鑰名稱,因此我們可以按名稱查找類別,但其存儲與其名稱分離。

這些別名都存儲在一個實體組中,並且允許我們使用事務友好的祖先查詢,因此我們可以查找或創建一個CategoryAlias,而不必擔心會創建多個副本。

當我想要查找或創建一個類別和項目組合時,我可以使用該類別的鍵名以編程方式在該事務內部生成一個鍵,並且允許我們通過它的鍵在事務中獲取一個實體。

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

注意事項:我沒有測試過這段代碼。顯然,你需要改變它來匹配你的實際模型,但我認爲它使用的原則是合理的。

更新: 西蒙,在你的評論中,你大多有正確的想法;儘管如此,你不應該錯過一個重要的微妙之處。您會注意到類別實體不是虛擬根目錄的子項。他們不共享父母,他們自己是他們自己實體組的根本實體。如果類別實體都具有相同的父項,那將會形成一個巨大的實體組,並且您會遇到性能惡夢,因爲每個實體組一次只能運行一個事務。

相反,CategoryAlias實體是虛假根實體的子代。這允許我在事務內部進行查詢,但實體組不會太大,因爲屬於每個類別的項目未附加到CategoryAlias。

此外,CategoryAlias實體中的數據可以在不更改實體的密鑰的情況下更改,並且我將Alias的密鑰用作生成可用於創建實際類別實體本身的密鑰名稱的數據點。因此,我可以更改存儲在CategoryAlias中的名稱,而不會失去將該實體與相同類別進行匹配的能力。

+0

Adam, 感謝您的解決方案 - 非常感謝。我讀了幾遍,據我瞭解,你在這裏做的是創建一個虛擬根實體,以確保當類別被提取時它從實體組中獲取(並因此允許在事務中) - 是對? 謝謝! -simon – Simon 2010-08-20 17:17:49

+0

@Simon,我已經編輯了我的回答以迴應您的評論。 – 2010-08-20 17:53:26

+0

有很大的區別重複:父類實體是不同的類別,感謝您的答案! – Simon 2010-08-20 18:54:07

0

幾件事情要注意的(我認爲他們很可能只是錯別字) -

  1. 你的交易方法調用的第一行得到()上的一個鍵 - 這是沒有文件記錄的功能。無論如何,你不需要在函數中擁有實際的類別對象 - 在使用類別實體的兩個地方,關鍵就足夠了。

  2. 您似乎沒有被調用或者類別或項目的(不過既然你說你是在數據存儲中獲取數據,我假設你已經離開了這一點,爲了簡潔?)

  3. 的put()

至於解決老話 - 你可以嘗試在內存緩存添加值的合理搭配到期 -

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

這至少阻止你創造的倍數。知道如果查詢沒有返回類別會有什麼影響仍然有點棘手,但是您無法從memcache中獲取鎖定。這意味着該類別正在創建過程中。

如果原始請求來自任務隊列,那麼只需引發異常,以便重新運行任務。

否則,您可以稍等一下並重新查詢,雖然這有點狡猾。

如果請求來自用戶,那麼您可以告訴他們存在衝突並重試。

+0

感謝您抓住我的錯別字 - 而不是複製我的確切功能,爲簡潔起見,我大量編輯它,並提出:( 這是一個有趣的解決方案,是保證memcache不會碰撞? – Simon 2010-08-20 17:06:50

+0

memcache.add()保證原子性 - 即你只能添加一個鍵一次 - 當然有了上面的代碼,這個條目會在60s內過期,然後你可以再次獲取鎖定。http://code.google.com/appengine/docs/python/memcache/clientclass。html#Client_add – hawkett 2010-08-20 17:46:25

相關問題