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

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

这是我偶然发现的一个奇怪的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 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出现了错误,我没有收到任何错误。我更新了我的答案,提供了更多解释,同时也承认了您的问题。我已将您的问题标记为解决方案。感谢您花费所有时间进行研究!