03/10/2018, 1:11 PM GMT+9

FlaskとSQLAlchemyでDetachedInstanceErrorが出たときの解決法

環境

  • Flask
  • SQLAlchemy
  • MariaDB

問題

SQLAlchemy でセッションを作成し、それを用いて DB からデータを取得。そのデータをテンプレートに渡し、テンプレート内で DB クラスの relation を参照した時に DetachedInstanceError が出ました。

def PenaltyList():
    session = Session():
    pens = session.query(Penalty).all()
    session.close()
    return render_template('penalty_list.html', pens=pens)

このようなコードで penalty_list.html 内で Penalty クラス(名前は気にしないでください)の relationship になっているクラスを参照していると、DetachedInstanceError が出ます。

原因は?

このエラーの原因は以下のとおりであると考えられます。

  • SQLAlchemyはrelationship の対象となるインスタンスのためのデータを、遅延して取得する。(例えば、Penalty クラスが relationship として User クラスのインスタンスをメンバとして持っているとき、Penalty を DB から取得しインスタンスを生成した時点では User クラスのインスタンスのためのデータを取得していない。)
  • (当然ながら)Session が関連付けられていない、あるいは、関連付けられていた Session が close されてしまうと、relationship の遅延ロードをするときにエラーになる。

解決法

まずはコードから。

def PenaltyList():
    try:
        session = Session():
        pens = session.query(Penalty).all()
        return render_template('penalty_list.html', pens=pens)
    except:
        flash('エラー', 'error')
        return redirect('/top')
    finally:
        try:
            session.close()
        except:
            pass

DB アクセス部分を try で囲ってきちんと例外処理をします。(基本的なことなのにしていなかった。)エラー処理の内容は割とどうでもいいのですが、大事なことは、session のclose は finally 節で行うということです。こうすると、render_template を処理した後、return の直前に finally 節が実行されるはずです。

要するに、sessionのcloseはrender_templateの後に行わなければならないということです。例外処理をせず本質を抜き出すなら、次のようにも書けます。

def PenaltyList():
        session = Session():
        pens = session.query(Penalty).all()
        rendered = render_template('penalty_list.html', pens=pens)
        session.close()
        return rendered

あとがき

多くの人にとって、エラー処理をするんだからこれは当たり前の書き方なのかもしれませんが、私は知らなくて苦労しました。誰かの参考になればいいなと思います。もっと標準的な書き方があれば教えてください。


Cosnomi
Cosnomi

コンピュータ(Web, 機械学習など)が好きな医学部生

Twitter / GitHub