5

在使用没有键名的祖先关系时,我在确保我的数据正确的 Google App Engine 中遇到了一些麻烦。

让我再解释一下:我有一个父实体category,我想创建一个子实体item。我想创建一个接受类别名称和项目名称的函数,如果它们不存在,则创建这两个实体。最初,我创建了一个事务,并在需要时使用键名在事务中创建了两个事务,这很好用。但是,我意识到我不想使用名称作为键,因为它可能需要更改,我尝试在我的事务中执行此操作:

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.

看看谷歌给出的关于如何解决这个问题的例子:

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

这似乎可行,但我发现当我使用一些创建了重复类别的请求运行它时,这是有道理的,因为该类别是在事务之外查询的,并且多个请求可以创建多个类别。

有谁知道如何正确创建这些类别?我试图将类别创建放入事务中,但仅再次收到有关祖先查询的错误。

谢谢!

西蒙

4

2 回答 2

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

警告购买者:我没有测试过这段代码。显然,您需要更改它以匹配您的实际模型,但我认为它使用的原则是合理的。

更新:西蒙,在您的评论中,您的想法大多是正确的;虽然,有一个重要的微妙之处你不应该错过。您会注意到 Category 实体不是虚拟根的子级。它们不共享父级,它们本身就是它们自己实体组中的根实体。如果 Category 实体都具有相同的父级,那将形成一个巨大的实体组,并且您将面临性能噩梦,因为每个实体组一次只能运行一个事务。

相反,CategoryAlias 实体是假根实体的子实体。这允许我在事务内部进行查询,但实体组不会变得太大,因为属于每个类别的项目未附加到 CategoryAlias。

此外,CategoryAlias 实体中的数据可以在不更改实体键的情况下更改,并且我使用别名键作为数据点来生成可用于创建实际类别实体本身的键名。因此,我可以更改存储在 CategoryAlias 中的名称,而不会失去将该实体与相同类别匹配的能力。

于 2010-08-20T16:04:31.177 回答
0

需要注意的几件事(我认为它们可能只是拼写错误)-

  1. 事务方法的第一行在键上调用 get() - 这不是文档化的函数。无论如何,您不需要在函数中拥有实际的类别对象 - 在您使用类别实体的两个地方,密钥就足够了。

  2. 您似乎没有在类别或项目上调用 put() (但既然您说您正在数据存储中获取数据,我假设您为了简洁而忽略了这个?)

就解决方案而言 - 您可以尝试在内存缓存中添加一个合理到期的值 -

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

这至少可以阻止您创建倍数。如果查询没有返回类别,知道该怎么做仍然有点棘手,但是您无法从 memcache 中获取锁。这意味着该类别正在创建过程中。

如果原始请求来自任务队列,则只需抛出异常以便任务重新运行。

否则,您可以稍等片刻,然后再次查询,尽管这有点狡猾。

如果请求来自用户,那么您可以告诉他们存在冲突并重试。

于 2010-08-20T14:59:49.920 回答