【问题标题】:How do I maintain multi-table integrity across multiple selects in SQLAlchemy in Pyramid?如何在 Pyramid 的 SQLAlchemy 中跨多个选择保持多表完整性?
【发布时间】:2013-08-21 04:14:53
【问题描述】:

我正在尝试构建一个 Pyramid 应用程序。我从 SQLAlchemy 脚手架开始。我遇到了一个问题,我想知道解决它的最佳方法是什么。在我的一个观点中,我需要从两个不相关的表中选择很多行。我需要确保在从第一个表中选择行和从第二个表中选择行之间没有任何行插入到第二个表中。

我有三个模型,NodeTestTaskingNodesTests 都有相当多的元数据。给定Nodes 的列表和Tests 的列表,可以创建Taskings 的全局列表。例如,我们可以有三个Nodesabc和两个Tests“我们需要一个节点来执行任务P”和“我们需要两个节点来执行任务@ 987654335@"。

根据该信息,应该创建三个Tasks。例如:

  1. “节点a应该做任务P
  2. “节点b应该做任务Q
  3. “节点c应该做任务Q

现在,我正在尝试为此提供一个 REST API。绝大多数时间客户将请求Tasks 的列表,因此需要快速。但是,有时客户端可能会添加NodeTest。发生这种情况时,我需要重新生成 Tasks 的整个列表。

这是一个粗略的例子:

@view_config(route_name='list_taskings')
def list_taskings(request):
    return DBSession.Query(Tasking).all()

@view_config(route_name='add_node')
def add_node(request):
    DBSession.add(Node())
    _update_taskings()

@view_config(route_name='add_test')
def add_test(request):
    DBSession.add(Test())
    _update_taskings()

def _update_taskings():
    nodes = DBSession.query(Node).all()
    tests = DBSession.query(Test).all()

    # Process...

    Tasking.query.delete()
    for t in taskings:
        DBSession.add(t)

我正在使用默认的 Pyramid SQLAlchemy 脚手架。因此,每个请求都会自动启动一个事务。因此,如果从一个请求(比如add_node)调用_update_tasking,那么新节点将被添加到本地DBSession,并在_update_tasking 中查询所有NodesTests 将返回新元素。此外,删除所有现有的Taskings 并添加新计算的也是安全的。

我有两个问题:

  1. 如果在nodes 列表和_update_taskings 中的tests 列表之间添加新行到Tests 表中会发生什么?在我的现实世界生产系统中,这些选择很接近,但并不相邻。存在竞争条件的可能性。

  2. 如何确保更新Taskings 的两个请求不会相互覆盖?例如,假设我们现有的系统有一个Node 和一个Test。两个请求同时进来,一个添加Node,一个添加Test。即使问题 #1 不是问题,并且我知道每个请求的一对选择代表“数据库中的单个时间实例”,仍然存在一个请求覆盖另一个请求的问题。如果第一个请求首先以两个Nodes 和一个Test 完成,则第二个请求仍将选择旧数据(可能)并将生成一个Taskings 列表,其中一个Node 和两个Tests .

那么,处理这个问题的最佳方法是什么?我在开发中使用 SQLite,在生产中使用 PostgreSQL,但我想要一个与数据库无关的解决方案。我不担心其他应用程序访问这个数据库。我的 REST API 将是唯一的访问机制。我应该锁定任何改变数据库的请求(添加NodeTest)吗?我应该以某种方式锁定数据库吗?

感谢您的帮助!

【问题讨论】:

    标签: python transactions sqlalchemy pyramid


    【解决方案1】:

    使用serializable 事务隔离级别应该可以防止这两个问题。如果一个事务修改了可能影响另一个事务中先前读取结果的数据,则存在序列化冲突。只有一个事务获胜,所有其他事务都被数据库中止以由客户端重新启动。 SQLite 通过锁定整个数据库来做到这一点,PostgreSQL 采用了更复杂的机制(详见docs)。不幸的是,没有可移植的 sqlalchemic 方法来捕获序列化异常并重试。您需要编写特定于 DB 的代码以可靠地将其与其他错误区分开来。

    我已经建立了一个示例程序,其中两个线程同时修改数据(您的方案的非常基本的复制),遇到冲突并重试:

    https://gist.github.com/khayrov/6291557

    使用 Pyramid 事务中间件和 Zope 事务管理器会更容易。在捕获序列化错误后,而不是手动重试,引发 TransientError 并且中间件将重试整个请求,直到 tm.attempts(在粘贴配置中)次。

    from transaction.interfaces import TransientError
    
    class SerializationConflictError(TransientError):
        def __init__(self, orig):
            self.orig = orig
    

    您甚至可以编写自己的位于堆栈中pyramid_tm 下方的中间件,该中间件将捕获序列化错误并将其透明地转换为瞬态错误。

    def retry_serializable_tween_factory(handler, registry):
    
        def retry_tween(request):
            try:
                return handler(request)
            except DBAPIError, e:
                orig = e.orig
                if getattr(orig, 'pgcode', None) == '40001':
                    raise SerializationConflictError(e)
                elif isinstance(orig, sqlite3.DatabaseError) and \
                    orig.args == ('database is locked',):
                    raise SerializationConflictError(e)
                else:
                    raise
    
        return retry_tween
    

    【讨论】:

    • 谢谢,这正是我需要的。尽管它不是 100% 漂亮,但知道锁定发生在数据库级别并且它对被认为是序列化错误的事情是智能的。我有一个问题。我正在使用使用 pyramid_tm 的金字塔脚手架。另外,我的 SQLAlchemy 会话是一个 scoped_session。我不能 .commit() 它(尽管我认为 .flush() 在这种特殊情况下做同样的事情),如果我 .rollback() 则我无法重试事务:“ResourceClosedError:此事务已关闭” .我要去google一下。谢谢!
    • 我已更新答案以涵盖事务中间件案例。
    • 我在您进行的精彩更新之前发布了我之前的“不是 100% 漂亮”的消息。补间的想法是完美的。它很好地解耦了一切。完美的!你有什么理由引发 SerializationConflictError 而不是简单的 TransientError?
    • 不,我只是认为这会增加一些自我说明。
    猜你喜欢
    • 2011-03-04
    • 2017-05-15
    • 2017-05-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-10
    相关资源
    最近更新 更多