如何在Pyramid中的SQLAlchemy中跨多个选择维护多表完整性?

时间:2013-08-21 04:14:54

标签: python transactions sqlalchemy pyramid

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

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

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

  1. “节点Tasks应该执行任务a
  2. “节点P应该执行任务b
  3. “节点Q应该执行任务c
  4. 现在,我正在尝试为此提供REST API。绝大多数时间客户端将请求Q列表,因此需要快速。但是,有时客户可能会添加TasksNode。当发生这种情况时,我需要重新生成Test的整个列表。

    这是一个粗略的例子:

    Tasks

    我正在使用默认的Pyramid SQLAlchemy脚手架。因此,每个请求都会自动启动一个事务。因此,如果从一个请求(例如@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) )调用_update_tasking,则新节点将添加到本地add_node,并查询所有DBSession和{{在Nodes中的1}}将返回该新元素。此外,删除所有现有的Tests并添加新计算的也是安全的。

    我有两个问题:

    1. 如果在_update_tasking的{​​{1}}列表和Taskings的{​​{1}}列表之间添加了新行,会发生什么情况? ?在我的真实世界生产系统中,这些选择是紧密相连但不是紧挨着彼此。有可能出现竞争条件。

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

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

      感谢您的帮助!

1 个答案:

答案 0 :(得分:5)

使用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