Python Flask SQLAlchemy db.session.query(Model)与Model.query
这是我偶然发现的一个奇怪的bug,我不确定它为什么会发生,不管它是SQLAlchemy中的bug,还是SQLAlchemy中的bug,或者是Python中我还不知道的任何特性 我们使用Flask 0.11.1,Flask SQLAlchemy 2.1使用PostgreSQL作为DBMS 示例使用以下代码更新数据库中的数据:Python Flask SQLAlchemy db.session.query(Model)与Model.query,python,postgresql,flask,sqlalchemy,flask-sqlalchemy,Python,Postgresql,Flask,Sqlalchemy,Flask Sqlalchemy,这是我偶然发现的一个奇怪的bug,我不确定它为什么会发生,不管它是SQLAlchemy中的bug,还是SQLAlchemy中的bug,或者是Python中我还不知道的任何特性 我们使用Flask 0.11.1,Flask SQLAlchemy 2.1使用PostgreSQL作为DBMS 示例使用以下代码更新数据库中的数据: entry = Entry.query.get(1) entry.name = 'New name' db.session.commit() # get an instan
entry = Entry.query.get(1)
entry.name = 'New name'
db.session.commit()
# 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()
当从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'])
致:
正如中所解释的,它确实按预期工作,所以我猜Flask SQLAlchemy的Model.query
实现中存在某种错误
但是,由于我更喜欢第一个构造,我对Flask SQLAlchemy进行了快速修改,并从原始属性重新定义了查询
@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
致:
其中,sa
是Alchemy对象(即控制器中的db
)
现在,这就是事情变得奇怪的地方:它仍然没有保存更改。代码完全相同,但DBMS仍在回滚我的更改
我了解到SQLAlchemy可以在拆卸时执行提交,并尝试添加以下内容:
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
突然间,一切正常。问题是:为什么
难道不应该仅在视图完成渲染后才进行拆卸吗?为什么修改后的
Entry.query
行为与db.session.query(Entry)
不同,即使代码相同?下面是对模型实例进行更改并将其提交到数据库的正确方法:
entry = Entry.query.get(1)
entry.name = 'New name'
db.session.commit()
# 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\u COMMIT\u
,因为它被认为是有害的,也会从文档中删除。看
编辑:如果您有两个普通会话的对象(使用
sessionmaker()
创建),而不是范围会话的对象,则调用db.session.add(entry)
以上代码将引发错误sqlalchemy.exc.InvalidRequestError:对象“”已附加到会话“2”(这是“3”)
。有关sqlalchemy会话的更多了解,请阅读下面的部分
作用域会话与正常会话之间的主要区别
我们主要从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')
>>> 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()
但是,如果会话对象是从scoped_session
对象检索的,那么我们就不会有这样的问题,因为scoped_session
对象为同一会话对象维护一个注册表
>>> 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')
>>> 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
是同一个会话对象,因为它们都是从维护对同一会话对象的引用的作用域_会话
对象检索的
>>> 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')
>>> 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()
提示
因此,尽量避免创建多个普通会话对象。创建会话的一个对象,并在从声明模型到查询的任何地方使用它。我们的项目分为几个文件,以便于管理。一个是带有控制器的
routes.py
,另一个是models.py
,其中包含ns SQLAlchemy实例和模型
所以,当我删除样板文件以获得一个最小的工作烧瓶项目,将其上传到git存储库并链接到这里时,我找到了原因
显然,原因是我的同事在尝试使用查询而不是模型对象插入数据时(不,我不知道他到底为什么要这样做,但他花了一整天的时间编码),在routes.py
中定义了另一个SQLAlchemy实例
因此,当我尝试使用以下方法从烧瓶外壳插入数据时:
from .models import *
entry = Entry.query.get(1)
entry.name = 'modified'
db.session.commit()
我使用了正确的db
对象,如models.py
中定义的,它工作得非常好
但是,正如在
routes.py
中一样,在模型导入之后定义了另一个db
,这一个正在覆盖对正确的SQLAlchemy实例的引用,因此我正在提交另一个会话SQLAlchemy.exc.InvalidRequestError:对象“”已附加到会话“2”“(这是'3')
Entry。query
通过会话加载记录。您不需要再次添加记录。您只需要在会话中添加新的内容(例如,Entry=Entry()
)。@MarcosVivesDelSol我也在上面做了,但我没有在会话'2'中附加任何此类错误。”(这是'3'))
@dirn感谢您的澄清。但是,重新添加会话不应抛出错误,但marcos出现了错误,我没有收到任何错误。我更新了我的答案,提供了更多解释,同时也承认了您的问题。我已将您的问题标记为解决方案。感谢您花费所有时间进行研究!