在使用不带键名的祖先关系时,我在 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
。
查看 example Google 给出了如何解决这个问题:
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")
这似乎有效,但我发现当我运行它时有很多请求,我创建了重复的类别,这是有道理的,因为类别是在事务之外查询的,并且多个请求可以创建多个类别。
有人知道如何正确创建这些类别吗?我尝试将类别创建放入事务中,但再次收到关于祖先查询的错误。
谢谢!
西蒙
最佳答案
这是解决您的问题的方法。这在很多方面都不是理想的方法,我真诚地希望其他 AppEngineer 能提出比我更简洁的解决方案。如果没有,请尝试一下。
我的方法采用以下策略:它创建充当类别实体的别名的实体。类别的名称可以更改,但别名实体将保留其键,我们可以使用别名键的元素为您的类别实体创建键名,因此我们将能够通过名称查找类别,但是它的存储与其名称分离。
别名都存储在单个实体组中,这使我们能够使用事务友好的祖先查询,因此我们可以查找或创建 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 实体中的数据可以在不更改实体键的情况下更改,我使用别名的键作为数据点来生成可用于创建实际类别实体本身的键名。因此,我可以更改存储在 CategoryAlias 中的名称,而不会失去将该实体与相同类别匹配的能力。
关于python - 如何在不使用键名的情况下确保 Google App Engine 中对象的数据完整性?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3525387/