这是从Twisted进行线程SQLAlchemy查询的可接受方式吗?

时间:2014-01-12 14:58:30

标签: python orm sqlalchemy twisted python-multithreading

我一直在阅读在Twisted应用程序的上下文中使用SQLAlchemy的ORM。这是消化的大量信息,所以我把所有部分放在一起会遇到一些麻烦。到目前为止,我收集了以下绝对真理:

  1. 一个会话意味着一个线程。总是。
  2. 默认情况下,
  3. scoped_session为我们提供了一种约束给定线程的会话的方法。换句话说,我确信通过使用scoped_session,我不会将会话传递给其他线程(除非我明确地这样做,我不会这样做。)
  4. 我还收集了一些与lazy / eager-loading相关的问题,一种可能的方法是将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执行数据库查找的函数:

    @threaded
    def get_some_attributes(group):
        with transaction_context() as session:
            return session.query(Attribute).filter(Attribute.group == group)
    

    我的第二个问题是这种方法是否可行。

    • 我做出任何根本上有缺陷的假设吗?
    • 有什么警告吗?
    • 有更好的方法吗?

    修改: Here是与我的上下文管理器中的意外错误相关的问题。

1 个答案:

答案 0 :(得分:3)

现在我处理这个确切的问题,我想我找到了一个解决方案。

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

你不能使用scoped_session,因为在Twisted中我们只有一个MainThread(除了在deferToThread中有效的东西)。但是,我们可以将scoped_sesssionscopefunc一起使用。

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

  

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

在方法render_GET的扭曲网页应用中,我设置了uuid参数:

call = context.call({"uuid": str(uuid.uuid4())}, self._render, request)

然后我调用_render方法来完成实际工作(使用db,render html等)。

我像这样创建scoped_session

scopefunc = functools.partial(context.get, "uuid")
Session = scoped_session(session_factory, scopefunc=scopefunc)

现在,在_render的任何函数调用中,我都可以使用:

进行会话
Session()

_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,你可以看到每个请求的不同。如果从scopefunc构造函数中删除scoped_session并同时执行两个请求(将time.sleep插入work_with_db),则会为这两个请求获得一个公共会话。

  

scoped_session对象默认使用threading.local()作为存储,因此为所有调用scoped_session注册表的人维护一个Session,但只在单个线程的范围内

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

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

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

我仍然不知道如何使其与defer.inLineCallback一起使用,我在代码中随处可用。