使用 session.add 或 session.execute 的 Flask-SQLAlchemy 事务回滚的不同行



我正在尝试应用此处描述的配方:http://alexmic.net/flask-sqlalchemy-pytest/使用pytest来测试使用Flask-SQLAlchemy的应用程序。

写作测试部分中,指出:

请注意,我们可以像往常一样自由提交会话。之所以实现这一点,是因为会话"加入"了我们在会话装置中显式创建的连接创建的外部事务,因此只有最外层的 BEGIN/COMMIT 对有任何影响。

但是,如果我在测试中使用session.add方法,这对我不起作用。但是,当我改用session.execute时,它可以工作。

下面是一个显示问题的示例:

database.py

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

models.py

from database import db

class Employee(db.Model):
__tablename__ = "employee"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)

test_employee.py

from models import Employee

def test_employee(session):
my_employee = Employee()
my_employee.name = "Toto"
session.add(my_employee)
# session.execute("INSERT INTO employee (id, name) VALUES (1, 'Tata');")
session.commit()

conftest.py(略微改编自示例)

import os
import pytest
from flask import Flask
from database import db as _db
TESTDB = 'test_utte.db'
TESTDB_PATH = "/tmp/{}".format(TESTDB)
TEST_DATABASE_URI = 'sqlite:///' + TESTDB_PATH

@pytest.fixture(scope='session')
def app(request):
"""Session-wide test `Flask` application."""
settings_override = {
'TESTING': True,
'SQLALCHEMY_DATABASE_URI': TEST_DATABASE_URI,
'SQLALCHEMY_TRACK_MODIFICATIONS': False  # Disabled to remove warning
}
app = Flask(__name__)
app.config.update(settings_override)
# Establish an application context before running the tests.
ctx = app.app_context()
ctx.push()
def teardown():
ctx.pop()
request.addfinalizer(teardown)
return app

@pytest.fixture(scope='session')
def db(app, request):
"""Session-wide test database."""
if os.path.exists(TESTDB_PATH):
os.unlink(TESTDB_PATH)
def teardown():
pass  # Commented to access database after test is run
# _db.drop_all()
# os.unlink(TESTDB_PATH)
_db.init_app(app)
_db.create_all()
request.addfinalizer(teardown)
return _db

@pytest.fixture(scope='function')
def session(db, request):
"""Creates a new database session for a test."""
db.engine.echo = True
connection = db.engine.connect()
transaction = connection.begin()
options = dict(bind=connection, binds={})
session = db.create_scoped_session(options=options)
db.session = session
def teardown():
transaction.rollback()
connection.close()
session.remove()
request.addfinalizer(teardown)
return session

运行pytest -s提供以下日志:

test_employee.py 2018-07-10 13:22:48,645 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2018-07-10 13:22:48,648 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2018-07-10 13:22:48,650 INFO sqlalchemy.engine.base.Engine INSERT INTO employee (name) VALUES (?)
2018-07-10 13:22:48,650 INFO sqlalchemy.engine.base.Engine ('Toto',)
2018-07-10 13:22:48,651 INFO sqlalchemy.engine.base.Engine COMMIT
.2018-07-10 13:22:48,657 INFO sqlalchemy.engine.base.Engine ROLLBACK

如果我签入数据库,条目在这里

sqlite> select * from employee;
1|Toto

相反,如果我在test_employee.py 中替换为 session.execute 部分:

from models import Employee

def test_employee(session):
# my_employee = Employee()
# my_employee.name = "Toto"
# session.add(my_employee)
session.execute("INSERT INTO employee (id, name) VALUES (1, 'Tata');")
session.commit()

我收到以下日志:

test_employee.py 2018-07-10 13:28:27,093 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2018-07-10 13:28:27,095 INFO sqlalchemy.engine.base.Engine INSERT INTO employee (id, name) VALUES (1, 'Tata');
2018-07-10 13:28:27,096 INFO sqlalchemy.engine.base.Engine ()
.2018-07-10 13:28:27,098 INFO sqlalchemy.engine.base.Engine ROLLBACK

并且该条目不在数据库中:

sqlite> select * from employee;
sqlite> 

最后,如果我把两者都放在测试中:

from models import Employee
def test_employee(session):
my_employee = Employee()
my_employee.name = "Toto"
session.add(my_employee)
session.execute("INSERT INTO employee (id, name) VALUES (1, 'Tata');")
session.commit()

那么我有一些类似于只有session.execute

的东西
test_employee.py 2018-07-10 13:31:57,179 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2018-07-10 13:31:57,180 INFO sqlalchemy.engine.base.Engine INSERT INTO employee (id, name) VALUES (1, 'Tata');
2018-07-10 13:31:57,181 INFO sqlalchemy.engine.base.Engine ()
2018-07-10 13:31:57,182 INFO sqlalchemy.engine.base.Engine INSERT INTO employee (name) VALUES (?)
2018-07-10 13:31:57,182 INFO sqlalchemy.engine.base.Engine ('Toto',)
.2018-07-10 13:31:57,183 INFO sqlalchemy.engine.base.Engine ROLLBACK

并且表保持为空:

sqlite> select * from employee;
sqlite> 

我的猜测是这与我的第一个示例中日志中显示的内部 BEGIN/COMMIT有关,但我真的不明白为什么它在这里或为什么它会覆盖最外层的BEGIN/ROLLBACK

这是一个非常棘手的问题,我想知道它是否与 sqlite 中session.execute的实现有关。我从未见过这样的东西在Postgres中工作。

虽然它没有触及问题的根源,但我想知道使用 pytest-flask-sqlalchemy-transactions 是否会解决您的问题。该插件旨在解决此确切用例,并公开一个db_session装置,可以满足您的需求。

您需要设置一个_db装置,以便让插件访问您的数据库:

@pytest.fixture
def _db(db):
return db

查看文档以获取安装说明,并让我知道它是否适合您。它还没有用sqlite进行过广泛的测试,所以至少它将有助于确定这是否是一个特定于sqlite的问题。

多亏了 jeancochrane 的回答,我设置了一个新的 virtualenv 来安装pytest-flask-sqlalchemy-transactions,却发现我什至无法重现该问题!

经过搜索和比较,我发现我的问题来自我最初使用 Flask-SQLAlchemy 2.1.0 的 venv 的事实。

升级到下一个版本(Flask-SQLAlchemy 2.2.0)时,问题消失了。

2.2.0 的更新日志提到:

  • 允许在 db.session 上侦听 SQLAlchemy 事件

也许这就是导致问题的原因?我不知道一些Flask-SQLAlchemy开发人员是否能够回答这个问题?

最新更新