Python 这是从Twisted生成线程化SQLAlchemy查询的一种可接受的方法吗?

Python 这是从Twisted生成线程化SQLAlchemy查询的一种可接受的方法吗?,python,orm,sqlalchemy,twisted,python-multithreading,Python,Orm,Sqlalchemy,Twisted,Python Multithreading,我一直在阅读有关在扭曲应用程序上下文中使用SQLAlchemy的ORM的内容。要消化的信息太多了,所以我把所有的信息放在一起有点困难。到目前为止,我收集了以下绝对真理: 一个会话意味着一个线程。总是 默认情况下,scoped_session为我们提供了一种将会话约束到给定线程的方法。换句话说,我确信通过使用作用域_session,我不会将会话传递给其他线程(除非我明确地这样做,我不会这样做) 我还发现了一些与延迟/急切加载相关的问题,一种可能的方法是将ORM对象与会话分离,并在更改线程时将它们重

我一直在阅读有关在扭曲应用程序上下文中使用SQLAlchemy的ORM的内容。要消化的信息太多了,所以我把所有的信息放在一起有点困难。到目前为止,我收集了以下绝对真理:

  • 一个会话意味着一个线程。总是
  • 默认情况下,
    scoped_session
    为我们提供了一种将会话约束到给定线程的方法。换句话说,我确信通过使用
    作用域_session
    ,我不会将会话传递给其他线程(除非我明确地这样做,我不会这样做)
  • 我还发现了一些与延迟/急切加载相关的问题,一种可能的方法是将ORM对象与会话分离,并在更改线程时将它们重新连接到另一个会话。我对细节不太清楚,但我也得出结论认为,
    scoped_session
    使这些观点中的许多都没有实际意义

    我的第一个问题是,我的上述结论是否严重错误

    除此之外,我还精心设计了这种方法,我希望这是令人满意的

    我首先创建一个
    scoped_session
    对象

    Session = scoped_session(sessionmaker(bind=_my_engine))
    
    。。。然后我将从上下文管理器中使用它,以便优雅地处理异常和清理:

    @contextmanager
    def transaction_context():
        session = Session()
        try:
            yield session
            session.commit()
        except:
            session.rollback()
            raise
        finally:
            session.remove()  # dispose of the session
    
    现在我需要做的就是在一个延迟到单独线程的函数中使用上面的上下文管理器。我请了一位装饰师来把东西弄得更漂亮一些:

    def threaded(fn):
        @wraps(fn)  # functools.wraps
        def wrapper(*args, **kwargs):
            return deferToThread(fn, *args, **kwargs)  # t.i.threads.deferToThread
        return wrapper
    
    下面是我打算如何使用整个shebang的一个例子。下面是一个使用SQLAlchemy ORM执行DB查找的函数:

    @threaded
    def get_some_attributes(group):
        with transaction_context() as session:
            return session.query(Attribute).filter(Attribute.group == group)
    
    我的第二个问题是这种方法是否可行。

    • 我是否做出了根本上有缺陷的假设
    • 有什么警告吗
    • 有更好的办法吗

    编辑:是一个与我的上下文管理器中的意外错误相关的问题。

    现在我正在研究这个问题,我想我找到了一个解决方案

    实际上,您必须将所有数据库访问函数延迟到一个线程。但是在您的解决方案中,在查询数据库之后删除会话,因此所有结果ORM对象都将分离,您将无法访问它们的字段

    您不能使用作用域_会话,因为在Twisted中,我们只有一个主线程(在deferToThread中工作的东西除外)。但是,我们可以将
    scoped session
    scopefunc
    一起使用

    在Twisted中,有一个很棒的东西叫做
    ContextTracker

    提供一种在调用中上下传递任意键/值数据的方法 堆栈,而不将它们作为参数传递给该调用上的函数 堆叠

    在我的twisted web应用程序中的方法
    render\u GET
    中,我设置了一个
    uuid
    参数:

    call = context.call({"uuid": str(uuid.uuid4())}, self._render, request)
    
    然后我调用
    \u render
    方法来执行实际工作(使用db、render html等)

    我创建的
    作用域\u会话如下所示:

    scopefunc = functools.partial(context.get, "uuid")
    Session = scoped_session(session_factory, scopefunc=scopefunc)
    
    现在,在
    \u render
    的任何函数调用中,我都可以获得以下会话:

    Session()
    
    \u render
    结束时,我必须执行
    Session.remove()
    以删除会话

    它与我的webapp配合使用,我认为可以用于其他任务

    这是一个完全独立的示例,展示了所有这些是如何协同工作的

    from twisted.internet import reactor, threads
    from twisted.web.resource import Resource
    from twisted.web.server import Site, NOT_DONE_YET
    from twisted.python import context
    from sqlalchemy import create_engine, Column, Integer, String
    from sqlalchemy.orm import sessionmaker, scoped_session
    from sqlalchemy.ext.declarative import declarative_base
    import uuid
    import functools
    
    engine = create_engine(
        'sqlite:///test.sql',
        connect_args={'check_same_thread': False},
        echo=False)
    
    session_factory = sessionmaker(bind=engine)
    scopefunc = functools.partial(context.get, "uuid")
    Session = scoped_session(session_factory, scopefunc=scopefunc)
    Base = declarative_base()
    
    
    class User(Base):
        __tablename__ = 'users'
        id = Column(Integer, primary_key=True)
        name = Column(String)
    
    Base.metadata.create_all(bind=engine)
    
    
    class TestPage(Resource):
        isLeaf = True
    
        def render_GET(self, request):
            context.call({"uuid": str(uuid.uuid4())}, self._render, request)
            return NOT_DONE_YET
    
        def render_POST(self, request):
            return self.render_GET(request)
    
        def work_with_db(self):
            user = User(name="TestUser")
            Session.add(user)
            Session.commit()
            return user
    
        def _render(self, request):
            print "session: ", id(Session())
            d = threads.deferToThread(self.work_with_db)
    
            def success(result):
                html = "added user with name - %s" % result.name
                request.write(html.encode('UTF-8'))
                request.finish()
                Session.remove()
            call = functools.partial(context.call, {"uuid": scopefunc()}, success)
            d.addBoth(call)
            return d
    
    if __name__ == "__main__":
        reactor.listenTCP(8888, Site(TestPage()))
        reactor.run()
    
    我打印出会话id,您可以看到每个请求的id都不同。如果从
    scoped_session
    构造函数中删除
    scopefunc
    ,并同时执行两个请求(将time.sleep插入
    work_与_db
    ),则这两个请求将获得一个公共会话

    默认情况下,作用域_会话对象使用threading.local()作为存储,以便为调用作用域_会话注册表的所有人维护单个会话,但仅在单个线程的作用域内

    这里的一个问题是,在twisted中,所有请求只有一个线程。这就是为什么我们必须创建自己的
    scopefunc
    ,以显示请求之间的差异

    另一个问题是,twisted没有将上下文传递给回调,我们必须包装回调并将当前上下文发送给它

    call = functools.partial(context.call, {"uuid": scopefunc()}, success)
    

    我仍然不知道如何使用我在代码中随处使用的
    defer.inLineCallback

    好吧,我想每个人都会遇到的首要问题是:它是否按照您编写它的方式工作?@bitcycle,顺便提一下(令人惊讶的是),不。。。它不起作用。我在上下文管理器中得到了一个
    属性错误
    ——显然
    会话
    没有属性
    删除
    。这相当令人惊讶——它看起来基本正确(除了应该是
    Session.remove()
    )。需要记住的一点是,一些数据库驱动程序,例如
    sqlite3
    ,不允许在线程之间传递它们的连接。我相当肯定
    scoped_session
    对象能够恰当地处理这个问题(自从我使用SQLAlchemy已经有一段时间了)。它在大部分情况下都能工作(尽管它仍然相当不完整),但它还没有必要的线程固定来让sqlite和朋友们开心。贡献是受欢迎的,这可能是一个比从头开始更好的起点。我很确定Alchimia不会包装ORM。(也许“还没有”,也许实际上很少有人喜欢ORM?)谢谢你的回答!您是否介意发布一个更详细的示例,明确说明
    ContextTracker
    \u render
    scoped\u session
    如何结合在一起?我很难看清全局。为什么
    scopefunc
    必须与
    作用域会话一起使用?谢谢大家!@blz使用添加scopefunc的示例和说明。@aborilov谢谢!你在这里找到了很好的解决方案。。。我想知道这是否可以通过一个装饰师更隐晦地完成