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()
然后,我们将无法同时向s1
和s2
添加相同的用户对象。换句话说,一个对象最多只能附加一个唯一的会话对象。
>>> 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()
注意s1
和s2
是同一个会话对象,因为它们都是从维护对同一会话对象的引用的scoped_session
对象中检索的。
提示
因此,请尽量避免创建多个正常会话对象。创建会话的一个对象,并在从声明模型到查询的所有地方使用它。
【讨论】:
sqlalchemy.exc.InvalidRequestError: Object '<Condition at 0x24909321e80>' 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的主要内容,如果未能解决你的问题,请参考以下文章