web-dev-qa-db-ja.com

Flask-SQLAlchemy db.session.query(Model)とModel.query

これは私が偶然見つけた奇妙なバグであり、SQLAlchemyのバグ、Flask-SQLAlchemyのバグ、またはPython I'mまだ気づいていません。

Flask 0.11.1を使用しています。Flask-SQLAlchemy2.1では、PostgreSQLをDBMSとして使用しています。

例では、次のコードを使用してデータベースからデータを更新します。

_entry = Entry.query.get(1)
entry.name = 'New name'
db.session.commit()
_

これは、Flaskシェルから実行すると完全に正常に機能するため、データベースは正しく構成されます。これで、エントリを更新するためのコントローラーが少し検証されました(検証やその他のボイラープレートなし)。

_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に簡単な変更を加え、元の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)と異なるのはなぜですか?

以下は、モデルインスタンスに変更を加えてデータベースにコミットする正しい方法です。

_# 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_は有害であると見なされ、ドキュメントからも削除されるため、使用しないでください。 バージョン2.0の変更ログ を参照してください。

Edit:normal sessionの2つのオブジェクトがある場合(sessionmaker())の代わりにscoped sessionを使用すると、上記のdb.session.add(entry)を呼び出すとエラーsqlalchemy.exc.InvalidRequestError: Object '' is already attached to session '2' (this is '3')が発生します。 sqlalchemyセッションの詳細については、以下のセクションをお読みください

スコープセッションと通常セッションの主な違い

sessionmaker()呼び出しから構築してデータベースとの通信に使用したセッションオブジェクトは、通常のセッションです。 sessionmaker()をもう一度呼び出すと、前のセッションとは独立した状態の新しいセッションオブジェクトが取得されます。たとえば、次のように構築された2つのセッションオブジェクトがあるとします。

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

次に、同じUserオブジェクトを_s1_と_s2_の両方に同時に追加することはできません。つまり、オブジェクトは、多くても1つの一意のセッションオブジェクトにのみアタッチできます。

_>>> 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_オブジェクトから取得されるため、同じセッションオブジェクトであることに注意してください。

チップ

したがって、複数の通常のセッションオブジェクトを作成しないようにしてください。セッションの1つのオブジェクトを作成し、モデルの宣言からクエリまで、あらゆる場所で使用します。

10

私たちのプロジェクトは、保守を容易にするためにいくつかのファイルに分かれています。 1つはコントローラを備えたroutes.pyであり、もう1つはSQLAlchemyインスタンスとモデルを含むmodels.pyです。

それで、ボイラープレートを削除して最小限の作業を行うときにFlaskプロジェクトをここにリンクするためにgitリポジトリにアップロードするために、その原因を見つけました。

どうやら、理由は私の同僚がモデルオブジェクトの代わりにクエリを使用してデータを挿入しようとしているためです(いいえ、一体なぜ彼がそれをしたかったのかわかりませんが、彼は丸一日それをコーディングしました)別のSQLAlchemyインスタンスをroutes.pyで定義しました

したがって、Flask Shellを使用してデータを挿入しようとしたとき、

from .models import *
entry = Entry.query.get(1)
entry.name = 'modified'
db.session.commit()

models.pyで定義されている正しいdbオブジェクトを使用していて、完全に正常に動作していました。

ただし、routes.pyのように、モデルのインポート後に別のdbが定義されましたこれは上書きされました参照は正しいSQLAlchemyインスタンスへの参照なので、別のセッションでコミットしました。