我們使用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://stackoverflow.com/a/21806294/4454028中所解釋的那樣,它確實按預期工作,因此我猜測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
至:
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)的行為不同,即使代碼相同?
# 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()創建)而不是作用域會話,那么在上面調用db.session.add(entry)代碼會引發錯誤sqlalchemy.exc.InvalidRequestError:對象”已經附加會話’2′(這是’3′).有關sqlalchemy會話的更多信息,請閱讀以下部分
范圍會話與正常會話的主要區別
我們主要從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添加相同的User對象.換句話說,一個對象最多只能附加一個唯一的會話對象.
>>> 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對象中檢索的.
提示
因此,盡量避免創建多個普通會話對象.創建會話的一個對象,並在從聲明模型到查詢的任何地方使用它.
補充:
也存在使用db.session.commit()提交數據后,使用model.query()查不到新增加的數據
原因:db.session.commit()是提交了數據到數據庫,但是沒有刷新模型映射中的數據,也就是model.query()中的數據。
而使用db.session.query()則是 從整個服務會話中進行查詢,而db.session.commit()提交的數據在這里是有刷新的,根源還是上面提到的范圍會話與正常會話的主要區別。