google 云数据存储嵌套事务的意外行为

Unexpected behavior around google cloud datastore nested transactions

我遇到的业务问题是:一个父实体有一个子实体后代。子实体具有需要唯一的值,因此存在 ChildLookup 实体来强制执行该唯一性。为了抽象一些东西,实体 puts/deletes 被放入它们自己的方法中,并且它们都有 batch/transaction 语句作为它们逻辑的一部分。

在Python中(使用this library),当结构是这样的时候,一切都很好:

# 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',因为它会在最后自动使用 commit/rollback。

所以,它会像下面这样。

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)