# Flask のアプリケーションコンテキストとリクエストコンテキスト徹底解説：current_app / g / request / session を正しく使う

> Flask 3.1系の2つのコンテキスト（アプリ／リクエスト）を公式仕様に忠実に解説。current_app・g・request・sessionの正体、contextvars+LocalProxyによるasync安全な仕組み、teardown_appcontext、app_context/test_request_context、copy_current_request_context、Working outside of context エラーの正しい対処までを実コードで体系化します。

- 公開日: 2026-06-24
- 著者: 友田 陽大
- タグ: Python, Flask, アーキテクチャ設計, 本番運用, バックエンド
- URL: https://tomodahinata.com/blog/flask-application-request-context-g-current-app-guide

## 要点

- Flaskには『アプリケーションコンテキスト』と『リクエストコンテキスト』の2つがあり、current_app/gは前者、request/sessionは後者に属する。リクエスト処理時もCLI実行時もFlaskが自動でpushする
- current_appはアプリへのプロキシ。app-factory構成では『importできるappが存在しない』ため、循環importを避けつつアプリ参照を得る唯一の正しい手段になる
- gはコンテキスト内グローバルであってリクエスト間の保存先ではない。get_db + teardown_appcontextパターンで、リクエスト中は同じ接続を使い回し終了時に必ず閉じる
- コンテキストローカルはcontextvars + Werkzeug LocalProxyで実装されており、単なるthread-localではない。だからasync/coroutineビューでも正しく動く（requestを別スレッドへ渡すと壊れる）
- Working outside of application/request context は『contextが無い場所でproxyに触れた』サイン。app_context()／test_request_context()／copy_current_request_contextで正しく解消する

---

## **導入：`current_app` を初めて使うと、必ず一度はこのエラーに出会う**

Flask を本番で扱うエンジニアが、ほぼ全員一度は踏むエラーがあります。

```text
RuntimeError: Working outside of application context.
```

あるいは、

```text
RuntimeError: Working outside of request context.
```

スクリプトの中で `current_app.config["DATABASE"]` を読もうとした、テストの外で `request.json` に触れた、バックグラウンドスレッドに `request` を渡した——いずれも「**コンテキストが無い場所で、コンテキストに依存するオブジェクトへ触れた**」という、たった一つの原因から来ています。逆に言えば、Flask の**2 つのコンテキスト**（アプリケーションコンテキストとリクエストコンテキスト）の仕組みを正確に理解すれば、このエラーは「なぜ起きたか」と「どう直すか」が即座に分かるようになります。

この記事は、[Flask 本番運用ガイド](/blog/flask-production-guide) の §5「コンテキスト」を、本番で必要な深さまで掘り下げるスポークです。`current_app` / `g` / `request` / `session` という 4 つのプロキシが「いつ使えて」「いつ使えないか」、その背後の `contextvars` + `LocalProxy` の仕組み、`teardown_appcontext` による資源の後始末、手動でコンテキストを push する場面、そしてバックグラウンドタスクとテストでの正しい扱いまでを、Flask 3.1 系の公式仕様に忠実に解説します。

筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを **Python / Flask / SQLAlchemy / PostgreSQL** で設計・実装し、**API Gateway → ALB → ECS(Fargate)** 上で本番運用してきました。マルチテナント SaaS では「いまのリクエストはどのテナントのものか」「DB セッションをリクエストの寿命にどう縛るか」がコンテキスト設計そのものであり、ここで示すパターンはその実戦で磨いたものです。

> 💡 **この記事で扱うバージョン**：**Flask 3.1 系**（現行安定版）を前提とします。コンテキストの実装はバージョン間で安定していますが、`g` がリクエストコンテキストからアプリケーションコンテキストへ移った（0.10）、`teardown_appcontext` が追加された（0.9）、`copy_current_request_context` / `appcontext_pushed` が追加された（0.10）といった「いつ何が変わったか」も要所で触れます。コードは公式ドキュメントのパターンに基づきます。

---

## **1. メンタルモデル：Flask には「2 つのコンテキスト」がある**

最初に、この記事全体の地図を頭に入れます。Flask が 1 つのリクエストを処理する間、内部では **2 種類のコンテキスト**が積まれています。

| コンテキスト | 保持するもの | このコンテキストに属するプロキシ | いつ push されるか |
|---|---|---|---|
| **アプリケーションコンテキスト**（app context） | アプリ単位のデータ | `current_app` / `g` | リクエスト処理時・CLI コマンド実行時に**自動** |
| **リクエストコンテキスト**（request context） | リクエスト単位のデータ | `request` / `session` | リクエスト処理時に**自動**（その際 app context も先に push） |

公式の定義はこうです——アプリケーションコンテキストは「**リクエスト、CLI コマンド、その他のアクティビティの間、アプリケーションレベルのデータを追跡する。アプリを引き回す代わりに、`current_app` と `g` プロキシ経由でアクセスする**」。リクエストコンテキストは「**リクエストレベルのデータを追跡し、`request` と `session` プロキシを提供する**」。

### 1.1 なぜ「コンテキスト」などという仕組みが必要なのか

ここがすべての出発点です。素朴に考えれば、ビュー関数から設定を読みたいなら `app` を import すればよさそうに見えます。

```python
from myapp import app  # ← これが本番で破綻する

@some_blueprint.route("/")
def index():
    return app.config["SOMETHING"]
```

しかしこれは 2 つの理由で破綻します。

1. **循環 import を起こす**。`app` は普通ビューやモデルを import して組み立てます。そのビューが `app` を import し返すと、依存が循環します。
2. **app-factory 構成では、そもそも import できる `app` が存在しない**。[アプリケーションファクトリ](/blog/flask-application-factory-blueprints-large-app-structure-guide)では `app` は `create_app()` の**中**で生成されるため、モジュールトップに「import 可能なグローバル `app`」がありません。再利用可能な Blueprint も、どのアプリに登録されるか定義時には分かりません。

この 2 つを同時に解決するのが `current_app` です。公式の言葉では——「`current_app` は**いま処理中のアクティビティを担当しているアプリ**を指す。これはプロキシであり、アプリケーションコンテキストが push されているときだけ利用できる」。`app` という名前のグローバルを撲滅し、「いま動いているアプリ」を実行時に解決する。これがコンテキストの存在理由です。

> 💡 **寿命のイメージ**：リクエスト処理の開始時に、Flask は**リクエストコンテキストと（必要なら）アプリケーションコンテキストを push** し、終了時に**リクエストコンテキストを pop してからアプリケーションコンテキストを pop** します。だから「アプリケーションコンテキストの寿命 ≒ リクエストの寿命」です。この入れ子構造（外側が app context、内側が request context）が、後で `teardown` の順序を理解する鍵になります。

---

## **2. `current_app`：アプリへのプロキシを正しく使う**

`current_app` は、`flask` から import する**プロキシオブジェクト**です。アクセスした瞬間に「いま push されているアプリケーションコンテキストのアプリ」へ転送されます。

```python
from flask import current_app

@bp.get("/version")
def version():
    # app を import せずに、いま動いているアプリの設定へ届く
    return {"version": current_app.config["API_VERSION"]}
```

押さえるべき性質は 3 つです。

- **プロキシである**：`current_app` 自体は本物の `Flask` インスタンスではなく、その時々のアプリへの「窓口」です。
- **コンテキストが push されているときだけ使える**：app context が無い場所（モジュールのトップレベル、別スレッド、コンテキスト外のスクリプト）で触れると `RuntimeError: Working outside of application context.` になります（§6 で対処）。
- **手動で制御できる**：リクエスト外でアプリ参照が必要なら `with app.app_context():` で自分で push します（§6.1）。

### 2.1 `_get_current_object()`：プロキシの「中身」を取り出す

プロキシは普段は透過的ですが、**プロキシのままでは困る**場面が 2 つあります。`isinstance()` での型チェックと、**シグナルの送信者として実体を渡したいとき**です。プロキシは `Flask` のサブクラスではないので `isinstance(current_app, Flask)` は意図通りになりません。実体が必要なら `_get_current_object()` を呼びます。

```python
from flask import current_app

# プロキシではなく、本物の Flask インスタンスが欲しいとき
app = current_app._get_current_object()
some_signal.send(app)          # シグナルの sender には実体を渡す
assert isinstance(app, Flask)  # 実体なので型チェックも通る
```

> ⚠️ **アンチパターン**：`current_app` を変数に代入して、コンテキストが切れた後（別スレッド・別タスク）で使い回す。`current_app` は「いまのコンテキスト」に束縛されるプロキシなので、コンテキストを越えて持ち出すと壊れます。コンテキストを越えてアプリを渡したいなら、`current_app._get_current_object()` で**実体を取り出してから**渡してください。

---

## **3. `g`：アプリケーションコンテキストに縛られた名前空間**

`g` は公式の定義で「**アプリケーションコンテキストの間、データを保存できる名前空間オブジェクト**」です。`current_app` と同じくプロキシで、同じアプリケーションコンテキストに属します。

ここで最も誤解されるのが `g` の名前です。公式は**わざわざ注記**しています（そのまま引用します）。

> The `g` name stands for "global", but that is referring to the data being global *within a context*. The data on `g` is lost after the context ends, and it is not an appropriate place to store data between requests. Use the `session` or a database to store data across requests.

つまり——`g` の "g" は global だが、それは「**コンテキスト内で**グローバル」という意味に過ぎない。`g` のデータはコンテキスト終了で失われ、**リクエストをまたぐ保存場所としては不適切**である。リクエスト間で保存したいなら `session` か DB を使え、と。

> ⚠️ **`g` をキャッシュ/グローバル変数だと思った瞬間に事故る**：「ログイン中のユーザーを `g.user` に入れておけば次のリクエストでも使える」——これは**間違い**です。`g` は各コンテキスト（≒各リクエスト）ごとに新品で、リクエストが終われば消えます。`g` に適しているのは「**このリクエストの中で何度も使う、リクエストスコープの値**」（DB 接続・解決済みの現在ユーザー・現在テナント）であって、リクエスト間で共有したい状態ではありません。

### 3.1 正準パターン：`get_db()` + `teardown_appcontext`

`g` の最も重要な用途が、「**リクエスト中は同じ DB 接続を使い回し、コンテキスト終了時に必ず閉じる**」パターンです。公式ドキュメントのコードをそのまま示します。

```python
from flask import g

def get_db():
    if 'db' not in g:
        g.db = connect_to_database()
    return g.db

@app.teardown_appcontext
def teardown_db(exception):
    db = g.pop('db', None)
    if db is not None:
        db.close()
```

公式の説明はこうです——「**リクエストの間、`get_db()` の呼び出しは毎回同じ接続を返し、リクエストの終わりに自動的に閉じられる**」。仕組みを分解すると：

- `if 'db' not in g:` で**初回だけ**接続を生成し、`g.db` に保存する。`'x' in g` は `g` の便利なメンバ判定です。
- 2 回目以降の `get_db()` は、すでに `g.db` がいるのでそれを返す。リクエスト内で接続が**1 本に保たれる**。
- `teardown_appcontext` に登録した `teardown_db` が、コンテキスト pop 時に呼ばれ、`g.pop('db', None)` で取り出して閉じる。

`g` のメンバ操作は dict に似た API を持ちます。本番で多用するのは次の 3 つです。

| 操作 | 意味 | 用途 |
|---|---|---|
| `'db' in g` | `g` に `db` 属性があるか | 初回生成の判定 |
| `g.get('db')` | 無ければ `None`（既定値も指定可） | 「あれば使う」読み取り |
| `g.pop('db', None)` | 取り出して削除（無ければ既定値） | teardown での後始末 |

### 3.2 `LocalProxy` で `get_db()` を「変数のように」見せる（任意）

`get_db()` を毎回呼ぶのが煩わしければ、Werkzeug の `LocalProxy` でラップして、**関数呼び出しを変数アクセスのように**見せられます。

```python
from werkzeug.local import LocalProxy

db = LocalProxy(get_db)   # db に触れるたび get_db() が呼ばれる

# 以降は current_app のように db を使える
def find_user(user_id):
    return db.execute("SELECT * FROM users WHERE id = ?", (user_id,))
```

これは「あってもなくてもよい」糖衣ですが、`current_app` / `g` / `request` が**すべて同じ `LocalProxy` 機構**で実装されていることを理解するのに役立ちます。次節でその機構そのものを見ます。

---

## **4. `request` / `session`：コンテキストローカルの正体**

`request`（受信リクエスト）と `session`（署名付き Cookie セッション）は、**リクエストコンテキスト**に属するプロキシです。

```python
from flask import request, session

@bp.post("/login")
def login():
    email = request.json["email"]        # 受信ボディ
    session["user_id"] = authenticate(email)  # 次のリクエストへ持ち越す（Cookie）
    return {"ok": True}
```

ここで多くの記事が「`request` はスレッドローカルだ」と説明しますが、**それは Flask 3.1 では正確ではありません**。この区別が本番の async / 並行設計を左右するので、正確に押さえます。

### 4.1 「context local」であって「thread local」ではない

公式の説明はこうです——「ワーカー（サーバーに応じてスレッド・プロセス・コルーチン）は**一度に 1 つのリクエストしか処理しない**ため、リクエストデータはそのワーカーにとってそのリクエストの間グローバルとみなせる。Flask はこれを **"context local"** と呼ぶ」。そして決定的な一文：

> Context locals are implemented using Python's `contextvars` and Werkzeug's `LocalProxy`.

つまりコンテキストローカルは、Python 標準の **`contextvars`** と Werkzeug の **`LocalProxy`** で実装されています。これを「スレッドローカル」と呼んではいけません。`contextvars` こそが、`async def` ビューやコルーチンでも `request` が正しいリクエストを指す理由だからです。スレッドローカルだと、1 スレッド上で複数コルーチンが走る async 環境では「どのリクエストの `request` か」が混線します。`contextvars` はコンテキスト（コルーチン）単位で値を持つので、それが起きません。

```text
[プロキシ]         [機構]                  [実体]
request   ──→  LocalProxy ──┐
session   ──→  LocalProxy   ├─ contextvars ──→ いまのコンテキストの
current_app ─→ LocalProxy   │                  RequestContext / AppContext
g         ──→  LocalProxy ──┘
```

### 4.2 だから「`request` を別スレッドへ渡す」と壊れる

この仕組みの直接的な帰結が、**`request` を別スレッドに渡してはいけない**という鉄則です。公式は明言します——「`request` を別スレッドに渡すことはできない。別スレッドは異なるコンテキストを持つ」。

```python
import threading
from flask import request

@bp.get("/bad")
def bad():
    def worker():
        # ❌ 別スレッドには「このリクエストのコンテキスト」が無い → 壊れる
        print(request.path)
    threading.Thread(target=worker).start()
    return "started"
```

`worker` スレッドは別の `contextvars` コンテキストで動くため、`request` は「このリクエスト」を指しません。バックグラウンドにリクエスト情報を持ち出したい正しい方法は §7 の `copy_current_request_context` です。

> 💡 **push されるのは request だけではない**：リクエストコンテキストが push されるとき、そのアプリ用のアプリケーションコンテキストがまだトップに無ければ、**Flask は先にアプリケーションコンテキストを push** します。だから「リクエストを処理しているなら `current_app` も `g` も必ず使える」のです。逆は成り立ちません——CLI コマンドや手動 push では app context だけがあり、`request` はありません。

---

## **5. `teardown_appcontext` と `teardown_request`：資源を確実に後始末する**

`g` に積んだ DB 接続のように、コンテキストの終わりで**必ず**解放したい資源があります。Flask はそのためのフックを 2 つ提供します。

| フック | 呼ばれるタイミング | 典型用途 |
|---|---|---|
| `teardown_request(f)` | **リクエストコンテキスト** pop 時 | リクエスト固有の後始末 |
| `teardown_appcontext(f)` | **アプリケーションコンテキスト** pop 時（各リクエストの request ctx の後、CLI コマンド終了時、手動 push の終了時） | DB 接続など「リクエストでも CLI でも閉じたい」資源 |

### 5.1 呼ばれる順序と「例外があっても呼ばれる」保証

レスポンスを返した後、コンテキストは pop され、**`teardown_request()` → `teardown_appcontext()`** の順で呼ばれます（外側＝app context が後）。重要なのは、**上のコードで未処理例外が送出されても、これらの teardown は呼ばれる**という保証です。だから「接続を確実に閉じる」場所として信頼できます。

ただし注意があります——公式いわく「他のリクエストディスパッチの処理が先に実行されたという保証は無い」。teardown は「**何があっても最後に資源を返す**」場所であって、「正常系の後処理を書く」場所ではありません。

### 5.2 teardown 関数は「絶対に例外を投げてはいけない」

公式の最重要ルール（そのまま守ってください）：

- **teardown 関数は例外を送出してはならない**。teardown 中の例外は後始末の連鎖を壊します。
- **teardown 関数の戻り値は無視される**。何かを return しても意味はありません。
- teardown 関数は引数として例外（あれば）を受け取りますが、それは「ハンドリングのため」ではなく「ログ等の参考のため」です。

```python
@app.teardown_appcontext
def teardown_db(exception):
    # exception は teardown のトリガになった例外（無ければ None）
    db = g.pop("db", None)
    if db is not None:
        try:
            db.close()
        except Exception:
            # ⚠️ teardown の中で例外を外へ漏らさない。ログに留める
            current_app.logger.exception("failed to close db connection")
```

> 💡 **CLI コマンドでも `teardown_appcontext` は走る**：`@app.cli.command()` で登録したコマンドの実行時、Flask はアプリケーションコンテキストを push し、コマンド終了時に pop します。このとき `teardown_appcontext` も呼ばれるので、**`get_db()` パターンは Web リクエストでも CLI バッチでも同じように動きます**。これが「`teardown_request` ではなく `teardown_appcontext` に DB 後始末を置く」理由です。リクエストにしか無い `teardown_request` だと、CLI バッチで接続が閉じられません。

---

## **6. 手動でコンテキストを push する：スクリプト・初期化・テスト**

Flask が自動で push してくれるのは「リクエスト処理時」と「CLI コマンド実行時」だけです。それ以外——**初期化スクリプト・cron バッチ・テストの一部**——では、自分でコンテキストを push する必要があります。ここを理解すると、冒頭の 2 つの `RuntimeError` が完全に制御下に入ります。

### 6.1 `app.app_context()`：アプリケーションコンテキストを手動 push

DB 初期化やスタンドアロンスクリプトのように「リクエストは無いがアプリ参照（`current_app` / `g`）が要る」場面では、`with app.app_context():` で push します。公式の正準形：

```python
with app.app_context():
    init_db()
```

このブロックの中でなら `current_app` も `g` も使えます。ブロックを抜けると pop され、`teardown_appcontext` も呼ばれます。これを忘れて、コンテキスト外で `current_app` に触れると：

```text
RuntimeError: Working outside of application context.
```

**正しい対処は「その処理を `with app.app_context():` で囲む」**ことです。`flask shell` を使うと、対話セッションで自動的に app context が push されるので、デバッグ時に便利です。

### 6.2 `app.test_request_context()`：リクエストコンテキストを手動 push（テスト用）

`request` や `session` に依存するコードを、**実サーバーを立てずに**テストしたいときは `test_request_context()` を使います。これはダミーのリクエストコンテキストを push します。

```python
def test_login_reads_email():
    app = create_app({"TESTING": True})
    with app.test_request_context("/login", method="POST", json={"email": "a@example.com"}):
        # このブロック内では request / session が使える
        assert request.json["email"] == "a@example.com"
```

リクエストコンテキストが無い場所で `request` に触れると：

```text
RuntimeError: Working outside of request context.
```

公式の指針はこうです——「これは通常、**アクティブなリクエストを前提とするコードをテストするとき**にだけ起きるべきだ」。だから対処は文脈で分かれます。

| `RuntimeError: Working outside of request context.` の発生場所 | 正しい対処 |
|---|---|
| **テストコードの中** | `test_request_context()` で push する。または `test_client()` 経由でリクエストする |
| **テスト以外（通常のアプリコード）** | **そのコードをビュー関数の中へ移す**。リクエスト外で `request` を読もうとしているのが設計の誤り |

公式も「テスト以外でこのエラーを見たら、そのコードをビュー関数の中へ移せ」と述べています。`test_client` / `test_request_context` を使ったテストの組み立て方は、[テスト実践ガイド](/blog/flask-testing-pytest-test-client-fixtures-guide) で詳説しています。

> 💡 **2 つの `RuntimeError` の見分け方**：エラーが `application context` なら `current_app` / `g` に触れた、`request context` なら `request` / `session` に触れた、というだけのことです。前者は `app_context()`、後者は `test_request_context()`（テスト）かビュー関数への移動（本番コード）で直ります。エラーメッセージがどちらのコンテキストを指しているかを読むだけで、原因の半分は特定できます。

---

## **7. バックグラウンドタスク：`copy_current_request_context`**

§4.2 で見たとおり、`request` を素のスレッドに渡すと壊れます。では、リクエスト処理の途中で重い処理をバックグラウンドへ逃がしつつ、その中で `request` / `session` を参照したい場合はどうするか。Flask は `copy_current_request_context`（0.10 で追加）を用意しています。これは「**いまのリクエストコンテキストを、バックグラウンドで動く関数のためにコピーして紐付ける**」デコレータです。公式の例（`gevent` を使ったもの）：

```python
import gevent
from flask import copy_current_request_context

@app.route('/')
def index():
    @copy_current_request_context
    def do_some_work():
        # ここでは flask.request / flask.session にアクセスできる
        ...
    gevent.spawn(do_some_work)
    return 'Regular response'
```

`@copy_current_request_context` を付けないと、`gevent.spawn` で起動した `do_some_work` は**アプリ/リクエストオブジェクトを一切見られません**（別コンテキストだから）。付けることで、起動時点のリクエストコンテキストがコピーされ、バックグラウンド側でも `request` が正しいリクエストを指します。

> ⚠️ **公式の警告：コンテキストのコピーより「データを渡す」を優先する**。`copy_current_request_context` は便利ですが、公式は明確に注意しています。
> - **可能なら、必要なデータを引数で渡す**方が安全（コンテキスト全体を持ち回らない）。
> - バックグラウンドに渡す前に**リクエストボディを読み終えておく**こと（後から読もうとすると、親リクエストが既に閉じている可能性がある）。
> - **`session` の操作は親（ビュー）側で行う**こと。
>
> つまり `copy_current_request_context` は「どうしてもコンテキストごと必要な場合の最後の手段」であり、第一選択は「`request` から必要な値を抜き出して、プレーンな引数として渡す」設計です。マルチテナント SaaS でも、バックグラウンドのインポート処理には `tenant_id` のような**値**を渡すのが基本で、コンテキストのコピーは例外的にしか使いません。

---

## **8. シグナル：`appcontext_pushed` でテストに資源を仕込む（簡潔に）**

Flask は Blinker ベースのシグナルで、コンテキストのライフサイクルにフックできます。コンテキスト関連のシグナルは 3 つです。

| シグナル | タイミング |
|---|---|
| `appcontext_pushed` | アプリケーションコンテキストが push された直後（0.10 で追加） |
| `appcontext_tearing_down` | コンテキストが pop される直前 |
| `appcontext_popped` | コンテキストが pop された後 |

最も実用的なのが `appcontext_pushed` です。**ユニットテストで、コンテキストが立ち上がった瞬間に `g` へリソース（テスト用ユーザーなど）を仕込む**のに使えます。公式の例：

```python
from contextlib import contextmanager
from flask import appcontext_pushed, g

@contextmanager
def user_set(app, user):
    def handler(sender, **kwargs):
        g.user = user
    with appcontext_pushed.connected_to(handler, app):
        yield
```

`with user_set(app, some_user):` のブロックの中では、新しく push される各コンテキストで `g.user` が `some_user` にセットされます。本番コードを汚さずに、テスト時だけ `g` の状態を差し替えられるのが利点です。可観測性の文脈では、`has_request_context()` をログフォーマッタで使い「リクエスト中なら `request.url` を、そうでなければ省く」といった分岐も定番です（[エラー処理・可観測性ガイド](/blog/flask-error-handling-logging-observability-guide) を参照）。

> 💡 `has_request_context()` / `has_app_context()` は「いまコンテキストがあるか」を**例外を出さずに**確かめる関数です。ログフォーマッタやユーティリティのように「リクエスト中にも CLI からも呼ばれうる」コードで、`request` に触れる前にガードするのに使います。`if has_request_context(): ...` と書けば、CLI 実行時に `Working outside of request context` を踏みません。

---

## **9. 本番例：リクエストスコープの DB セッションと「現在テナント」を `g` に載せる**

ここまでの部品を、実際のマルチテナント B2B SaaS の形に組み上げます。やりたいことは 2 つです。

1. **DB セッションをリクエストの寿命に縛る**（リクエスト中は 1 セッション、終了時に確実にクローズ）。
2. **「いまのリクエストはどのテナントか」を一度だけ解決し、`g` に載せて以降使い回す**。

```python
# db.py — リクエストスコープの DB セッション
from flask import g, current_app
from werkzeug.local import LocalProxy
from sqlalchemy.orm import Session

def _get_session() -> Session:
    if "db_session" not in g:
        # current_app 経由でエンジンを取得（app を import しない）
        g.db_session = current_app.config["SESSION_FACTORY"]()
    return g.db_session

# LocalProxy で「変数のように」使えるセッション
db_session = LocalProxy(_get_session)

def init_teardown(app):
    @app.teardown_appcontext
    def close_session(exception):
        session = g.pop("db_session", None)
        if session is None:
            return
        try:
            # 例外で終わったならロールバック、正常ならコミットは
            # ビュー側で済ませる方針なら、ここは確実な close に徹する
            if exception is not None:
                session.rollback()
        finally:
            session.close()  # ← teardown は「閉じる」を最優先で確実に
```

```python
# tenancy.py — 現在テナントを g に解決する
from flask import g, request, abort

def current_tenant():
    if "tenant" not in g:
        tenant_id = request.headers.get("X-Tenant-ID")
        if not tenant_id:
            abort(400, description="X-Tenant-ID header is required")
        tenant = db_session.get(Tenant, tenant_id)  # 上の db_session を利用
        if tenant is None:
            abort(404, description="unknown tenant")
        g.tenant = tenant   # このリクエスト内で 1 回だけ解決し、以降使い回す
    return g.tenant
```

ビュー側はこれらを「変数のように」使うだけです。`app` の import も、セッションの引き回しも、テナント解決の重複もありません。

```python
@bp.get("/orders")
def list_orders():
    tenant = current_tenant()  # 初回だけ解決、2 回目以降は g からヒット
    orders = db_session.scalars(
        select(Order).where(Order.tenant_id == tenant.id)
    ).all()
    return {"data": [o.to_dict() for o in orders]}
```

この設計が効く理由を、コンテキストの観点で整理します。

- **`g` がリクエストスコープであること**が、「テナントをリクエスト中 1 回だけ解決する」を自然に表現する。リクエストが終われば `g.tenant` は消えるので、別テナントのリクエストへ漏れない。
- **`teardown_appcontext` が例外時にも呼ばれること**が、「どんな終わり方をしてもセッションを必ず閉じる」を保証する。接続リークは本番で最も静かに効いてくる障害なので、ここを teardown に置くのは設計上の要です。
- **`current_app` 経由でエンジンを取ること**が、この `db.py` を**どのアプリにも import 依存させず**、テストでは別のセッションファクトリを持つアプリに差し替え可能にする。

> ⚠️ **`g` にテナントを載せる設計の落とし穴**：`g.tenant` は**リクエスト間で共有されない**ことが安全性の根拠です。もし「パフォーマンスのため」とモジュールグローバルにテナントをキャッシュしようものなら、別テナントのリクエストに前のテナントが漏れる——マルチテナントで最悪のデータ混線になります。テナントやユーザーのような**リクエスト固有・かつ取り違えが許されない値**は、必ずコンテキストスコープ（`g`）に置いてください。「速くするためのグローバルキャッシュ」がセキュリティ境界を壊す典型例です。SQLAlchemy 側のセッション/エンジン設計の詳細は [SQLAlchemy 2.0 実践ガイド](/blog/sqlalchemy-2-typed-orm-production-guide) を併読してください。

---

## **まとめ：コンテキストは「アプリを引き回さない」ための装置である**

Flask の 2 つのコンテキストは、突き詰めれば**「`app` というグローバルを撲滅し、いま動いているアプリ／リクエストを実行時に解決する」**ための装置です。この記事の要点を再掲します。

1. **2 つのコンテキスト**——`current_app` / `g` はアプリケーションコンテキスト、`request` / `session` はリクエストコンテキスト。リクエスト処理時と CLI 実行時に Flask が自動 push する。
2. **`current_app`** は循環 import と app-factory 問題を同時に解く唯一の正しいアプリ参照手段。型チェックやシグナルには `_get_current_object()` で実体を取り出す。
3. **`g`** はコンテキスト内グローバルであって**リクエスト間の保存先ではない**（公式の Note を厳守）。`get_db()` + `teardown_appcontext` がリクエストスコープ資源の正準パターン。
4. **コンテキストローカルは `contextvars` + `LocalProxy`** で実装され、thread-local ではない。だから async でも正しく動き、`request` を別スレッドへ渡すと壊れる。
5. **`teardown_appcontext` は例外時にも呼ばれ、戻り値は無視され、自身は例外を投げてはならない**。CLI でも走るので DB 後始末はここに置く。
6. **`Working outside of application/request context` は「コンテキストの無い場所でプロキシに触れた」サイン**。`app_context()`（スクリプト/初期化）、`test_request_context()`（テスト）、あるいはビュー関数への移動で直す。
7. **バックグラウンドは `copy_current_request_context`**——ただし第一選択は「必要なデータを引数で渡す」設計。

コンテキストを正しく理解すると、Flask アプリの「どこから何が使えるか」が一枚の地図になります。`current_app` で設定へ、`g` でリクエストスコープ資源へ、`request` / `session` で入力とセッションへ——アプリを一切引き回さずに届く。この明示性こそ、適切に設計された Flask の強さです。全体像と他の設計対象（構成・設定・エラー処理・デプロイ・テスト）は [Flask 本番運用ガイド](/blog/flask-production-guide) に戻って俯瞰してください。
