有关 Google 云数据存储嵌套事务的意外行为



我遇到的业务问题是:父实体有一个子实体后代。子实体具有需要唯一的值,因此存在 ChildLookup 实体来强制实施该唯一性。为了抽象出一些东西,实体放置/删除已被放入它们自己的方法中,并且两者都有批处理/事务语句作为其逻辑的一部分。

在 Python 中(使用此库(,当结构像这样时,一切都很好:

# assuming ('Parent', 11), ('Parent', 11, 'Child', 'foo') (with value 'bar'), and ('ChildLookup-foo', 'bar') all exist
def put_parent(c):
p2 = c.get(c.key('Parent', 22))
if p2 is None:
p2 = Entity(c.key('Parent', 22))
c.put(p2)
def put_child(c):
with c.transaction():
e2 = Entity(c.key(*p2.key.flat_path, 'Child', 'foo'))
e2['__val'] = 'bar'
el2 = c.get(c.key('ChildLookup-foo', e2['__val']))
if el2 is not None:
raise ValueError('insert would create duplicate')
el2 = Entity(c.key('ChildLookup-foo', 'val'))
c.put(el2)
c.put(e2)
c = google.cloud.datastore.Client()
with c.transaction():
put_parent(c)
put_child(c)

尝试运行此操作将导致正确的行为:将引发异常,并且不会插入 p2 或 e2。但是,我可以将put_parent更改为如下所示:

def put_parent():
with c.transaction():  # only actual change. can also be c.batch()
p2 = c.get(c.key('Parent', 22))
if p2 is None:
p2 = Entity(c.key('Parent', 22))
c.put(p2)

当我这样做时,尽管第二个事务回滚,但 p2 还是入。这对我来说是出乎意料的:我希望回滚仅限于最里面的事务(或批处理(,或者我希望回滚会影响最外层事务(或批处理(的所有子事务。

当然,在上面的琐碎玩具示例中,我可以取出内部批次并从顶层进行管理。但是将它们放入方法的要点是,我可能偶尔想要单独调用它们,而调用它们的方法没有相同的保证,并且我希望它们的事务性要求的业务对这些方法的使用者来说并不重要。是否有一种设计模式,或者一些Python Google Cloud Datastore库功能,可以让我做我正在尝试做的事情?

编辑:

接受答案中的代码是以下内容的基础,我将其包含在好奇中。它最终产生了我想要的行为。

from contextlib import contextmanager
@contextmanager
def use_existing_or_new_transaction(client):
if client.current_transaction:
yield client.current_transaction
else:
with client.transaction() as xact:
yield xact

@contextmanager
def use_existing_or_new_batch(client):
if client.current_transaction:
yield client.current_batch
else:
with client.batch() as xact:
yield xact

然后像

with use_existing_or_new_transaction(c) as xact:
xact.put(something)
xact.delete(something_else)
# etc

你试过c.current_transaction吗?

https://googleapis.dev/python/datastore/latest/client.html

这个想法是,你使用

与 c.transaction((

在您的所有调用之外和每个调用中,只需获取当前事务并使用它来执行操作。我认为您不应该在函数中使用"with",因为这会在最后自动提交/回滚。

因此,它将如下所示。

def put_parent(c):
txn = c.current_transaction
p2 = txn.get(c.key('Parent', 22))
if p2 is None:
p2 = Entity(c.key('Parent', 22))
txn.put(p2)
def put_child(c):
txn = c.current_transaction
e2 = Entity(c.key(*p2.key.flat_path, 'Child', 'foo'))
e2['__val'] = 'bar'
el2 = txn.get(c.key('ChildLookup-foo', e2['__val']))
if el2 is not None:
raise ValueError('insert would create duplicate')
el2 = Entity(c.key('ChildLookup-foo', 'val'))
txn.put(el2)
txn.put(e2)
c = google.cloud.datastore.Client()
with c.transaction():
put_parent(c)
put_child(c)

最新更新