围绕此`Manager`结构创建事务以回滚数据库操作

时间:2017-08-27 17:52:40

标签: python transactions sqlalchemy pytest

我在SQLAlchemy数据库引擎/连接/会话周围创建了一个“manager”对象:

Base = declarative_base()

class Manager(object):
    def __init__(self, connection: str = 'sqlite://'):
        self.engine = create_engine(connection, echo=True)
        Base.metadata.create_all(self.engine)
        self.sessionmaker = sessionmaker(bind=self.engine)
        self.session = scoped_session(self.sessionmaker)

    def do_db_stuff(self):
        self.session.query(Whatever).all()

    def ensure_thing(self):
        thing = Thing()
        self.session.add(thing)
        self.session.commit()

我想创建两个py.test fixtures:一个用于实例化管理器,另一个用于在可能调用commit的测试中包装和回滚事务。 This is the pattern我试图效仿,但没有成功:

@pytest.fixture(scope='session')
def manager():
    m = Manager()
    return m


@pytest.fixture(scope='function')
def manager_session(manager):
    connection = manager.session.connection()
    transaction = connection.begin()

    yield manager

    manager.session.close()
    transaction.rollback()
    connection.close()

不幸的是,管理员创建的对象即使在被transaction.rollback()调用之后也被上面所包围。

围绕现有会话包装事务的正确方法是什么?

编辑:

另一种不同的尝试:

@pytest.fixture(scope='function')
def manager_session(manager):
    connection = manager.engine.connect()
    transaction = connection.begin()
    manager.sessionmaker.configure(bind=connection)

    yield manager

    manager.session.close()
    transaction.rollback()

编辑2:

第三次尝试似乎有效,但Ilja Everilä's answer below中提到的警告说线程代码会引起麻烦。

@pytest.fixture(scope='session')
def manager():
    return Manager()


@pytest.fixture(scope='function')
def manager_transaction(manager):
    connection = manager.engine.connect()
    transaction = connection.begin()
    manager.session_maker.configure(bind=connection)

    yield manager

    manager.session_maker.configure(bind=manager.engine)
    manager.session.remove()
    transaction.rollback()
    connection.close()

1 个答案:

答案 0 :(得分:1)

第一次尝试失败,因为会话实际上是在控制连接及其事务。您可以通过查看生成的日志记录来验证。当您调用manager.session.connection()并且之后对begin()的显式调用是返回正在进行的事务对象的无操作时,会话开始一个隐式事务。因此,当您在管理器方法中提交时,您将提交实际内容,而现在过时的事务对象在您回滚时不会执行任何操作。

如果使用内存中的SQLite数据库,第二次尝试对我来说就像一样,但如果您的实际代码与您提供的内容略有不同,则无效。您将创建的连接设置为self.sessionmaker上的绑定,而不是已创建的scoped session registry self.session中的会话,因此如果您在配置制造商之前以任何方式触摸了会话注册表,您实际上已使用引擎创建了一个会话作为当前线程中的绑定:

In [7]: m = Manager()

In [8]: m.session.bind
Out[8]: Engine(sqlite://)

In [9]: connection = m.engine.connect()

In [10]: transaction = connection.begin()
2017-08-28 14:24:02,584 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)

In [11]: m.sessionmaker.configure(bind=connection)

In [12]: m.session.bind
Out[12]: Engine(sqlite://)

因此,除了配置sessionmaker之外,还应确保之前未在注册表中注册会话。另请注意,如果您有使用线程的代码,注册表将共享它们之间的连接,这将导致麻烦。