Flask-SQLAlchemy db.session.query(Model) 与 Model.query

Posted

技术标签:

【中文标题】Flask-SQLAlchemy db.session.query(Model) 与 Model.query【英文标题】:Flask-SQLAlchemy db.session.query(Model) vs Model.query 【发布时间】:2017-02-22 12:59:47 【问题描述】:

这是我偶然发现的一个奇怪的错误,我不确定它为什么会发生,无论是 SQLAlchemy、Flask-SQLAlchemy 中的错误,还是我还不知道的 Python 的任何功能。

我们使用 Flask 0.11.1,Flask-SQLAlchemy 2.1 使用 PostgreSQL 作为 DBMS。

示例使用以下代码更新数据库中的数据:

entry = Entry.query.get(1)
entry.name = 'New name'
db.session.commit()

从 Flask shell 执行时,这完全可以正常工作,因此数据库配置正确。现在,我们用于更新条目的控制器稍微简化(没有验证和其他样板文件),如下所示:

def details(id):
    entry = Entry.query.get(id)

    if entry:
        if request.method == 'POST':
            form = request.form
            entry.name = form['name']
            db.session.commit()
            flash('Updated successfully.')
        return render_template('/entry/details.html', entry=entry)
    else:
        flash('Entry not found.')
        return redirect(url_for('entry_list'))

# In the application the URLs are built dynamically, hence why this instead of @app.route
app.add_url_rule('/entry/details/<int:id>', 'entry_details', details, methods=['GET', 'POST'])

当我在 details.html 中提交表单时,我可以完美地看到更改,这意味着表单已正确提交、有效并且模型对象已更新。但是,当我重新加载页面时,更改就消失了,就好像它已被 DBMS 回滚了一样。

我已启用app.config['SQLALCHEMY_ECHO'] = True,并且在我自己的手动提交之前可以看到“ROLLBACK”。

如果我换行:

entry = Entry.query.get(id)

收件人:

entry = db.session.query(Entry).get(id)

正如https://***.com/a/21806294/4454028 中解释的那样,它确实按预期工作,所以我猜Flask-SQLAlchemy 的Model.query 实现中存在某种错误。

但是,由于我更喜欢​​第一种构造,所以我对 Flask-SQLAlchemy 进行了快速修改,并重新定义了原来的 query @property

class _QueryProperty(object):

    def __init__(self, sa):
        self.sa = sa

    def __get__(self, obj, type):
        try:
            mapper = orm.class_mapper(type)
            if mapper:
                return type.query_class(mapper, session=self.sa.session())
        except UnmappedClassError:
            return None

收件人:

class _QueryProperty(object):

    def __init__(self, sa):
        self.sa = sa

    def __get__(self, obj, type):
        return self.sa.session.query(type)

其中sa 是Flask-SQLAlchemy 对象(即控制器中的db)。

现在,事情变得奇怪了:它仍然没有保存更改。代码完全相同,但 DBMS 仍在回滚我的更改。

我读到 Flask-SQLAlchemy 可以在拆卸时执行提交,并尝试添加:

app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True

突然之间,一切正常。问题是:为什么?

不应该只在视图完成渲染后才进行拆卸吗?为什么修改后的Entry.query 的行为与db.session.query(Entry) 不同,即使代码相同?

【问题讨论】:

【参考方案1】:

以下是对模型实例进行更改并将其提交到数据库的正确方法:

# get an instance of the 'Entry' model
entry = Entry.query.get(1)

# change the attribute of the instance; here the 'name' attribute is changed
entry.name = 'New name'

# now, commit your changes to the database; this will flush all changes 
# in the current session to the database
db.session.commit()

注意:不要使用SQLALCHEMY_COMMIT_ON_TEARDOWN,因为它被认为是有害的,也会从文档中删除。见the changelog for version 2.0。

编辑:如果您有两个 普通会话 对象(使用 sessionmaker() 创建)而不是 作用域会话 ,然后调用 @ 987654325@ 以上代码将引发错误sqlalchemy.exc.InvalidRequestError: Object '' is already attached to session '2' (this is '3')。有关 sqlalchemy 会话的更多了解,请阅读以下部分

Scoped Session 与 Normal Session 的主要区别

我们主要从sessionmaker() 调用构建并用于与我们的数据库通信的会话对象是普通会话。如果您第二次调用sessionmaker(),您将获得一个新的会话对象,其状态与前一个会话无关。例如,假设我们有两个按以下方式构造的会话对象:

from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String)


from sqlalchemy import create_engine
engine = create_engine('sqlite:///')

from sqlalchemy.orm import sessionmaker
session = sessionmaker()
session.configure(bind=engine)
Base.metadata.create_all(engine)

# Construct the first session object
s1 = session()
# Construct the second session object
s2 = session()

然后,我们将无法同时向s1s2 添加相同的用户对象。换句话说,一个对象最多只能附加一个唯一的会话对象。

>>> jessica = User(name='Jessica')
>>> s1.add(jessica)
>>> s2.add(jessica)
Traceback (most recent call last):
......
sqlalchemy.exc.InvalidRequestError: Object '' is already attached to session '2' (this is '3')

但是,如果会话对象是从 scoped_session 对象中检索的,那么我们就不会有这样的问题,因为 scoped_session 对象为同一会话对象维护了一个注册表。

>>> session_factory = sessionmaker(bind=engine)
>>> session = scoped_session(session_factory)
>>> s1 = session()
>>> s2 = session()
>>> jessica = User(name='Jessica')
>>> s1.add(jessica)
>>> s2.add(jessica)
>>> s1 is s2
True
>>> s1.commit()
>>> s2.query(User).filter(User.name == 'Jessica').one()

注意s1s2 是同一个会话对象,因为它们都是从维护对同一会话对象的引用的scoped_session 对象中检索的。

提示

因此,请尽量避免创建多个正常会话对象。创建会话的一个对象,并在从声明模型到查询的所有地方使用它。

【讨论】:

sqlalchemy.exc.InvalidRequestError: Object '&lt;Condition at 0x24909321e80&gt;' is already attached to session '2' (this is '3') Entry.query 通过会话加载记录。您无需再次添加它。您只需将新内容添加到会话中(例如,entry = Entry())。 @MarcosVivesDelSol 我也在上面做了,但我没有收到任何此类错误already attached to session '2' (this is '3') @dirn 感谢您的澄清。但是,再次重新添加会话不应引发错误,但 marcos 出错了,我没有得到任何错误。【参考方案2】:

我们的项目被分成几个文件以方便维护。一个是带有控制器的routes.py,另一个是models.py,其中包含SQLAlchemy 实例和模型。

所以,当我删除样板文件以获取最小的工作 Flask 项目以将其上传到 git 存储库以将其链接到此处时,我找到了原因。

显然,原因是我的同事在尝试使用查询而不是模型对象插入数据时(不,我不知道他到底为什么要这样做,但他花了一整天的时间编写代码), routes.py中定义了另一个SQLAlchemy实例

所以,当我尝试从 Flask shell 插入数据时:

from .models import *
entry = Entry.query.get(1)
entry.name = 'modified'
db.session.commit()

我使用了正确的 db 对象,如 models.py 中定义的那样,它工作得很好。

然而,就像在routes.py 中一样,在模型导入后定义了另一个db这个覆盖了对正确 SQLAlchemy 实例的引用,所以我正在提交另一个会话

【讨论】:

我用更多解释更新了我的答案,也承认了你的问题。 我已将您的标记为解决方案。感谢您花时间研究它!

以上是关于Flask-SQLAlchemy db.session.query(Model) 与 Model.query的主要内容,如果未能解决你的问题,请参考以下文章

Flask-SQLAlchemy 的隔离级别

Flask-SQLAlchemy:用户角色模型和关系

Flask-SQLAlchemy:在回滚无效事务之前无法重新连接

Flask-SQLAlchemy 学习总结

Flask-SQLAlchemy:如何更改表结构?

Flask-SQLAlchemy:如何有条件地插入或更新一行