这是我偶然发现的一个奇怪的错误,我不确定它为什么会发生,不管它是SQLAlchemy中的错误,还是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'])

当我提交详细的表格时.html,我可以很好地看到更改,这意味着表单已经正确提交,是有效的,并且模型对象已经更新.然而,当我重新加载页面时,更改消失了,就好像它被DBMS回滚了一样.

我已经启用了app.config['SQLALCHEMY_ECHO'] = True,我可以在手动提交之前看到"回滚".

如果我改变路由:

entry = Entry.query.get(id)

致:

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

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

然而,由于我更喜欢第一种 struct ,我对Alchemy做了一个快速修改,并重新定义了原来的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是Alchemy对象(即控制器中的db).

现在,这就是事情变得奇怪的地方:它仍然无法保存更改.代码完全相同,但DBMS仍在回滚我的更改.

我读到Alchemy可以在拆卸时执行提交,并try 添加以下内容:

app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True

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

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

推荐答案

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

# 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()

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

Edit:如果您有两个normal session的对象(使用sessionmaker()创建),而不是scoped session,那么在调用上面的db.session.add(entry)时,代码将引发错误sqlalchemy.exc.InvalidRequestError: Object '' is already attached to session '2' (this is '3').有关sqlalchemy会话的更多了解,请阅读下面的部分

作用域会话与正常会话之间的主要区别

我们主要从sessionmaker()调用构造的会话对象是normal session,用于与数据库通信.如果你第二次调用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对象检索的.

提示

所以,尽量避免创建一个以上的对象.创建会话的一个对象,并在任何地方使用它,从声明模型到查询.

Postgresql相关问答推荐

用于JSON数组的带有组合条件的Postgres JSONB Select 查询

使用函数返回值作为另一个函数的参数

无法继承BYPASSRLS

如何诊断字符编码问题

PL/pgSQL中的IP递增函数

数据库所有者无法授予 Select 权限

如何准确确定边界附近的点和地理的 ST_Intersects(ST_Intersects geography vs. Geometry diffrepancy)

如何获取在 Go 中完成的 SQL 插入的错误详细信息?

AGE Graph 实际上存储为 postgreSQL 表,对吧?如何检索该表(不是图表)?

这个 Supbase 查询在 SQL ( Postgresql ) 中的类似功能是什么

如何从 postgresql Select 查询中的 age() 函数中仅获取年份

Postgres 图像未创建数据库

如何使用 pg_dump 或 psql 从 *.sql 恢复 PostgreSQL 表?

PostgreSQL 中的 JSON 外键

Psycopg2 使用占位符插入表格

H2 postgresql mode模式似乎不起作用

从没有行的计数中获取 0 值

是否可以在 CSV 格式的 Postgres COPY 命令中关闭报价处理?

Android 的 JDBC 与 Web 服务

如何在 PostgreSQL 中生成一系列重复数字?