如何在不使用键名称的情况下确保 Google 应用引擎中对象的数据完整性?
我在 Google App Engine 中遇到了一些麻烦,确保在使用没有键名称的祖先关系时我的数据正确。
让我再解释一下:我有一个父实体类别,并且我想创建一个子实体项目。我想创建一个函数,它接受类别名称和项目名称,并创建两个实体(如果它们不存在)。最初,我创建了一个事务,并在需要时使用密钥名称在事务中创建了两个事务,效果很好。但是,我意识到我不想使用名称作为密钥,因为它可能需要更改,并且我尝试在事务中执行此操作:
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 拒绝了此命令,因为它不允许您在事务中运行查询:事务内仅允许祖先查询
。
查看 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")
这似乎有效,但我发现当我使用多个请求运行此命令时,我创建了重复的类别,这是有道理的,因为类别是在外部查询的事务和多个请求可以创建多个类别。
有谁知道我如何正确创建这些类别?我尝试将类别创建放入事务中,但仅再次收到有关祖先查询的错误。
谢谢!
西蒙
I'm having a bit of trouble in Google App Engine ensuring that my data is correct when using an ancestor relationship without key names.
Let me explain a little more: I've got a parent entity category, and I want to create a child entity item. I'd like to create a function that takes a category name and item name, and creates both entities if they don't exist. Initially I created one transaction and created both in the transaction if needed using a key name, and this worked fine. However, I realized I didn't want to use the name as the key as it may need to change, and I tried within my transaction to do this:
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")
What I found when I tried to run this is that App Engine rejects this as it won't let you run a query in a transaction: Only ancestor queries are allowed inside transactions
.
Looking at the example Google gives about how to address this:
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)
I attempted to move my fetch of the category to before the transaction:
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")
This seemingly worked, but I found when I ran this with a number of requests that I had duplicate categories created, which makes sense, as the category is queried outside the transaction and multiple requests could create multiple categories.
Does anyone have any idea how I can create these categories properly? I tried to put the category creation into a transaction, but received the error about ancestor queries only again.
Thanks!
Simon
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
这是解决您的问题的方法。从很多方面来说,这都不是一种理想的方法,我真诚地希望其他 AppEnginer 能够提出比我更简洁的解决方案。如果没有,请尝试一下。
我的方法采用以下策略:它创建充当类别实体别名的实体。类别的名称可以更改,但别名实体将保留其键,我们可以使用别名键的元素为您的类别实体创建键名,因此我们将能够通过名称查找类别,但是它的存储与其名称是分离的。
别名全部存储在单个实体组中,这允许我们使用事务友好的祖先查询,因此我们可以查找或创建 CategoryAlias,而不必冒创建多个副本的风险。
当我想要查找或创建类别和项目组合时,我可以使用类别的键名以编程方式在事务内生成密钥,并且允许我们通过事务内的密钥获取实体。
买者自负:我还没有测试过这段代码。显然,您需要更改它以匹配您的实际模型,但我认为它使用的原理是合理的。
更新:
西蒙,在你的评论中,你的想法大多是正确的;不过,有一个重要的微妙之处您不应错过。您会注意到类别实体不是虚拟根的子实体。它们不共享父实体,并且它们本身就是自己实体组中的根实体。如果类别实体确实都具有相同的父级,那么这将形成一个巨大的实体组,并且您将面临性能噩梦,因为每个实体组一次只能运行一个事务。
相反,CategoryAlias 实体是虚假根实体的子实体。这允许我在事务内部进行查询,但实体组不会变得太大,因为属于每个类别的项目未附加到类别别名。
此外,CategoryAlias 实体中的数据可以更改,而无需更改实体的键,并且我使用 Alias 的键作为数据点来生成可用于创建实际类别实体本身的键名。因此,我可以更改存储在 CategoryAlias 中的名称,而不会失去将该实体与相同类别相匹配的能力。
Here is an approach to solving your problem. It is not an ideal approach in many ways, and I sincerely hope that someone other AppEnginer will come up with a neater solution than I have. If not, give this a try.
My approach utilizes the following strategy: it creates entities that act as aliases for the Category entities. The name of the Category can change, but the alias entity will retain its key, and we can use elements of the alias's key to create a keyname for your Category entities, so we will be able to look up a Category by its name, but its storage is decoupled from its name.
The aliases are all stored in a single entity group, and that allows us to use a transaction-friendly ancestor query, so we can lookup or create a CategoryAlias without risking that multiple copies will be created.
When I want to lookup or create a Category and item combo, I can use the category's keyname to programatically generate a key inside the transaction, and we are allowed to get an entity via its key inside a transaction.
Caveat emptor: I have not tested this code. Obviously, you will need to change it to match your actual models, but I think that the principles that it uses are sound.
UPDATE:
Simon, in your comment, you mostly have the right idea; although, there is an important subtlety that you shouldn't miss. You'll notice that the Category entities are not children of the dummy root. They do not share a parent, and they are themselves the root entities in their own entity groups. If the Category entities did all have the same parent, that would make one giant entity group, and you'd have a performance nightmare because each entity group can only have one transaction running on it at a time.
Rather, the CategoryAlias entities are the children of the bogus root entity. That allows me to query inside a transaction, but the entity group doesn't get too big because the Items that belong to each Category aren't attached to the CategoryAlias.
Also, the data in the CategoryAlias entity can change without changing the entitie's key, and I am using the Alias's key as a data point for generating a keyname that can be used in creating the actual Category entities themselves. So, I can change the name that is stored in the CategoryAlias without losing my ability to match that entity with the same Category.
有几点需要注意(我认为它们可能只是拼写错误) -
事务方法的第一行在键上调用 get() - 这不是记录的函数。无论如何,您不需要在函数中拥有实际的类别对象 - 在您使用类别实体的两个地方,该键就足够了。
您似乎没有在类别或项目上调用 put() (但既然您说您正在数据存储区中获取数据,我假设您为了简洁而忽略了这一点?)
就解决方案而言 -您可以尝试在内存缓存中添加一个具有合理到期时间的值 -
这至少会阻止您创建多个值。如果查询没有返回类别,但你无法从内存缓存中获取锁,那么知道该怎么办仍然有点棘手。这意味着该类别正在创建过程中。
如果原始请求来自任务队列,则只需抛出异常,以便任务重新运行。
否则,您可以稍等一下,然后再次查询,尽管这有点狡猾。
如果请求来自用户,那么您可以告诉他们存在冲突并重试。
A couple of things to note (I think they're probably just typos) -
The first line of your transactional method calls get() on a key - this is not a documented function. You don't need to have the actual category object in the function anyway - the key is sufficient in both of the places where you are using the category entity.
You don't appear to be calling put() on either of the category or the item (but since you say you are getting data in the datastore, I assume you have left this out for brevity?)
As far as a solution goes - you could attempt to add a value in memcache with a reasonable expiry -
This at least stops you creating multiples. It is still a bit tricky to know what do if the query does not return the category, but you cannot grab the lock from memcache. This means the category is in the process of being created.
If the originating request comes from the task queue, then just throw an exception so the task gets re-run.
Otherwise you could wait a bit and query again, although this is a little dodgy.
If the request comes from the user, then you could tell them there has been a conflict and to try again.