Python + FastAPI マニュアル(VS Code / Windows 11 / Python初心者向け)

対象:Android Java は分かるが Python は初心者(=「目的→最短で動く→デバッグ→配布」まで一本道)

0. Python + FastAPI の目的と概要

FastAPI は 「HTTP API サーバを素早く作る」ための Python フレームワークです。
Android(Java)で言うと、ざっくり Spring Boot の “最小・高速版” みたいな立ち位置です(依存を減らして軽く始める)。

FastAPI が向いていること

このマニュアルのゴール

✅ ゴール
  1. Windows 11 に Python + FastAPI の開発環境を作る
  2. VS Code で新規プロジェクトを作って起動できる
  3. ブレークポイントで止めてデバッグできる
  4. 配布(= 運用先で動かす)手順を理解して実行できる
Android と違うポイント:
Python は venv(仮想環境)を毎プロジェクト作るのが基本です。
Gradle の依存がプロジェクトごとに閉じているのと同じ感覚で、pip 依存をプロジェクト内に閉じ込めるためです。

1. Windows 11 で Python / FastAPI の環境構築

1-1. まず入れるもの(最小)

名前役割ポイント
Python 3.x 実行環境(ランタイム) 開発は原則 最新寄りの安定版(例:3.12系など)がおすすめ
VS Code エディタ / デバッグ 拡張で補完・フォーマット・デバッグを揃える
Git(任意) バージョン管理 配布や共同開発で便利(無くても開始は可能)

1-2. Python インストール(Windows)

  1. Python をインストール(公式インストーラ or Microsoft Store など)
  2. PowerShell を開き、バージョン確認:
    python --version
    pip --version
重要:Windows では py コマンドが使える場合があります。
もし python が見つからない場合は、まず py -V を試してください。

1-3. 仮想環境(venv)を作る

FastAPI は プロジェクトごとに venv を作るのが基本です。
これをやらないと、別プロジェクトの依存と衝突してハマります。
# 作業フォルダへ移動(例)
cd C:\work

# プロジェクト作成
mkdir fastapi-sample
cd fastapi-sample

# venv 作成(.venv という名前にするのがおすすめ)
python -m venv .venv

1-4. venv を有効化(Activate)

# PowerShell
.\.venv\Scripts\Activate.ps1

# コマンドプロンプト(cmd)
.\.venv\Scripts\activate.bat
有効化できると、プロンプトに (.venv) のような表示が出ます。

1-5. FastAPI / Uvicorn をインストール

python -m pip install --upgrade pip
pip install fastapi "uvicorn[standard]"
用語メモ:
FastAPI = API の枠組み、
Uvicorn = ASGI サーバ(HTTP サーバ役)。
つまり「FastAPI アプリを Uvicorn が起動して HTTP を受ける」構造です。

2. 標準的なフォルダ構成(迷わないための地図)

最初はシンプルでOKです。規模が大きくなったら分割していきます。

2-1. 最小構成(まず動かす)

fastapi-sample/
  .venv/
  main.py
  requirements.txt
  README.md

2-2. 実務寄りの標準構成(おすすめ)

fastapi-sample/
  .venv/
  app/
    __init__.py
    main.py              # FastAPI app の入口
    routers/             # ルーティング(Controller相当)
      health.py
      users.py
    schemas/             # リクエスト/レスポンス型(DTO相当)
      user.py
    services/            # 業務ロジック
      user_service.py
    db/                  # DB接続・Repository層
      session.py
      user_repo.py
    core/                # 設定・共通部品
      config.py
  tests/
    test_health.py
  requirements.txt
  .env.example
  .gitignore
Android(Java) に例えると
  • routers/:Controller(HTTPの入口)
  • schemas/:DTO(Request/Responseの型)
  • services/:UseCase / Service(業務ロジック)
  • db/:Repository / DAO(DBアクセス)
  • core/:Config / Utils
Windows注意:.venv/ は Git に入れません(容量も環境依存も大きい)。
代わりに requirements.txt(依存一覧)を必ず残します。

3. VS Code に入れておくといい Extension

優先度拡張機能用途
必須 Python(Microsoft) 実行・仮想環境・デバッグ・テストの中心
必須 Pylance 補完・型ヒント解析(“PythonをJavaっぽく安全に書く”)
推奨 Ruff 高速Lint(文法/スタイル/簡易バグ検出)
推奨 Black Formatter 自動整形(チームで差分が減る)
便利 REST Client VS Code から HTTP リクエストを送って動作確認できる
便利 Docker コンテナ配布をする場合に便利
初心者向けの最短:
まずは PythonPylance だけ入れて、動かす。
余裕が出たら RuffBlack を追加、が事故りにくいです。

4. VS Code でのプロジェクト新規作成

4-1. フォルダを作って VS Code で開く

  1. 例:C:\work\fastapi-sample を作成
  2. VS Code → FileOpen Folder... で開く
  3. VS Code → Ctrl + Shift + @ でターミナルを開く

4-2. venv を作って有効化(VS Code 内ターミナル)

python -m venv .venv
.\.venv\Scripts\Activate.ps1
VS Code の右下やコマンドパレットで Python Interpreter(実行環境)を選ぶ画面が出たら、
必ず .venv を選択してください。

4-3. 依存を入れる

python -m pip install --upgrade pip
pip install fastapi "uvicorn[standard]"
pip freeze > requirements.txt

4-4. 入口ファイル(main.py)を作る

from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "ok"}
これで「API が1本ある」状態です。まずはここまでを最短ゴールにします。

5. 開発サーバの起動・動作確認

5-1. Uvicorn で起動(リロード有効)

uvicorn main:app --reload --host 127.0.0.1 --port 8000

5-2. ブラウザで確認

✅ 最初の動作確認チェック
  1. ターミナルに「起動ログ」が出ている
  2. /health{"status":"ok"} を返す
  3. /docs が表示され、GET /health を実行できる
よくある詰まり:
ポート 8000 を別アプリが使っている → --port 8001 などに変えて起動。

6. VS Code を使用した Debug 方法(ブレークポイント)

仕組みはシンプルです:VS Code が Python を “デバッグ起動” する だけ。
Java のようにブレークポイントを置いて、変数を見て、ステップ実行できます。

6-1. デバッグ設定(launch.json)を作る

  1. 左の「実行とデバッグ」 → 「create a launch.json file
  2. 環境は Python を選ぶ
  3. 以下の例のように uvicorn を module 起動する設定にする

例:.vscode/launch.json(最小)

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "FastAPI (uvicorn)",
      "type": "python",
      "request": "launch",
      "module": "uvicorn",
      "args": [
        "main:app",
        "--reload",
        "--host", "127.0.0.1",
        "--port", "8000"
      ],
      "jinja": false
    }
  ]
}

6-2. ブレークポイントで止める

  1. main.pyreturn 行にブレークポイント
  2. VS Code で「FastAPI (uvicorn)」を選んで ▶ を押す
  3. ブラウザで /health を叩く
  4. 止まったら、Variables / Watch / Call Stack で状態確認
💡 reload とデバッグ --reload は便利ですが、裏でプロセスを再起動するため挙動が分かりにくいことがあります。
「止まらない/変な挙動」のときは一度 --reload を外して試すと切り分けしやすいです。

7. リリース版の作り方と配布方法(実務で困らないために)

FastAPI は「完成したら exe を配る」より、サーバで常時起動して API を提供する形が一般的です。
ここでは Windows 前提で、現実的な配布パターンを3つ示します。

7-A. いちばんシンプル:ソース + requirements.txt を配布

  1. 配布物を zip にする(例):
    app/(または main.py)
    requirements.txt
    README.md
    .env.example
  2. 配布先で venv 作成 → 依存インストール:
    python -m venv .venv
    .\.venv\Scripts\Activate.ps1
    pip install -r requirements.txt
  3. 起動(本番は reload なし):
    uvicorn main:app --host 0.0.0.0 --port 8000
小規模・社内用途なら、この方式がいちばん早く、運用コストも低いです。

7-B. Docker 配布(環境差分を消す)

おすすめ条件:複数台に配布する / チームで同じ動作を保証したい / Linux 本番運用を見据える。
この場合は Dockerfile を用意し、イメージとして配布します。

(最小イメージ例:参考)

# Dockerfile(例)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

7-C. Windows サービス化(常駐運用)

ポイント:「PC起動と同時に API を自動起動したい」ならサービス化が便利です。
代表例:NSSM などのツールで uvicorn をサービス登録(運用ルールが必要)。

7-D. “exe に固める” はアリ?

⚠ 原則おすすめしません(理由)
  • FastAPI は本来サーバ常駐型。exe 化しても結局「HTTPサーバを常駐させる」必要がある
  • 依存(C拡張等)でハマりやすく、サイズも大きくなる
  • 配布の手軽さより、更新・監視・ログ運用が大変になりがち
どうしても必要なら PyInstaller などが選択肢ですが、まずは 7-A / 7-B / 7-C のどれかが現実的です。

7-1. 本番向けの最低チェックリスト

✅ 迷ったら まずは 7-A(ソース + requirements) で配布し、
台数が増えたら 7-B(Docker) を検討するのが安全です。

2. 標準的なフォルダ構成(FastAPI版)

ポイント:FastAPI は Laravel のように「正解の置き場」が厳密に固定されていません。
なので最初に “自分のチームの地図(フォルダ構成)” を決めておくと、迷子になりません。

2-1. FastAPI プロジェクトフォルダ構成(代表)

パス 役割 ここに何を書く?
app/ アプリ本体のコード API の入口(router)/ ロジック(service)/ 型(schema)/ 設定など、実装の中心
app/main.py エントリポイント app = FastAPI() を作り、ルーター登録・ミドルウェア登録・起動設定を書く
app/routers/ ルーティング(HTTPの入口) URL と関数を紐づける。入力を受け、service を呼び、レスポンスを返す(Controller相当)
app/schemas/ データ型(DTO) Pydantic の BaseModel を使い、リクエスト/レスポンスの型、バリデーション定義を書く
app/services/ 業務ロジック層 注文計算・請求判定など「API入口から分離したい処理」。テストもしやすくなる
app/db/ DBアクセス層 DB接続、セッション、Repository(DAO相当)、SQL/ORM操作をまとめる
app/models/(任意) ORMモデル置き場 SQLAlchemy/SQLModel を使う場合のテーブル対応クラス(LaravelのEloquent相当)
app/core/ 設定・共通部品 設定読み込み(環境変数/.env)、共通例外、共通関数、依存注入の土台など
tests/ テスト pytest で Unit / API テスト。routersservices のテストが中心
requirements.txt 依存一覧 pip install -r requirements.txt 用。配布・CI・再現性のために必須
.env(任意) 環境変数ファイル DB接続先、APIキー等。機密なので Git には入れない(代わりに .env.example を置く)
.venv/ 仮想環境(venv) プロジェクト専用のPython環境。Gitに入れない(環境依存・巨大)

2-2. 初心者が最初に触る場所(FastAPI)

✅ 最初の3点セット
  • app/main.py(アプリ起動の入口 / ルーター登録)
  • app/routers/(URLの入口 / APIを増やす場所)
  • app/services/(業務ロジック:ここを作ると理解しやすい)
Android(Java) に例えると
  • routers:Controller(HTTPの入口)
  • schemas:DTO(Request/Response)
  • services:UseCase / Service(業務ロジック)
  • db/models:Repository / Entity(DBアクセス)
  • core:Config / 共通部品

3. Python の変数・型と基本文法

ポイント:Python は 型宣言を書かない 言語ですが、
実務では「型を意識して書く」ことで Java 出身者でも安全に扱えます。

3-1. Python の主な型

説明
int 整数 x = 10
float 浮動小数 pi = 3.14
str 文字列 name = "Alice"
bool 真偽値 is_active = True
list 配列(可変) nums = [1, 2, 3]
tuple 配列(不変) point = (10, 20)
dict 連想配列 user = {"id":1, "name":"Bob"}
None null 相当 value = None
Java / PHP との対応
  • PHP $a = 1; → Python a = 1
  • PHP null → Python None
  • PHP 配列 → Python list / dict

3-2. if / switch / while / for / foreach

if(条件分岐)

if x > 10:
    print("large")
elif x > 5:
    print("middle")
else:
    print("small")

switch 相当(match:Python 3.10+)

match status:
    case 200:
        print("OK")
    case 404:
        print("Not Found")
    case _:
        print("Other")

while

i = 0
while i < 3:
    print(i)
    i += 1

for

for i in range(3):
    print(i)

foreach(Pythonでは for)

users = ["Alice", "Bob", "Carol"]

for user in users:
    print(user)
Python は { } を使わず、インデントが構文 です。
インデントがズレるとエラーになる点は、最初に必ず慣れましょう。

6. Python の変数定義とスコープ(外部変数 / クラス変数 / ローカル変数 / 定数)

重要:Python では「変数を宣言する」という概念はありません。
代入した瞬間に変数が定義される、という点が Java / PHP と決定的に違います。

6-1. Python における「変数定義」とは

x = 10
name = "Alice"
is_active = True

上記のように、= で値を代入した時点で変数が作られます。
型は 値の型 によって自動的に決まります。

Java との違い
  • Java:int x = 10;(型+宣言が必要)
  • Python:x = 10(代入だけ)

6-2. スコープの種類(LEGBルール)

LEGB ルール:
Python は変数を探すとき、次の順でスコープを見に行きます。
Local → Enclosing → Global → Built-in

6-3. ローカル変数(Local)

def sample():
    x = 10  # ローカル変数
    print(x)

sample()
# print(x)  # ← エラー(関数の外からは見えない)

6-4. 外部変数(グローバル変数 / Global)

count = 0  # 外部(グローバル)変数

def increment():
    global count
    count += 1
注意:
関数内で外部変数を書き換える場合は global 宣言が必要です。
ただし 実務ではグローバル変数の書き換えは原則NG です。
FastAPI 実務視点
  • グローバル変数は スレッド安全でない
  • 設定値・定数の参照用途に限定する
  • 状態を持つなら DB / キャッシュ / クラスに寄せる

6-5. クラス変数(Class Variable)

class User:
    role = "guest"  # クラス変数

    def __init__(self, name):
        self.name = name  # インスタンス変数
注意点 クラス変数をミュータブル(list, dict)にすると、
全インスタンスで共有されて事故る ことがあります。

6-6. インスタンス変数(参考)

u = User("Alice")
print(u.name)  # インスタンス変数
print(User.role)  # クラス変数

6-7. 定数の書き方(Python流)

Python には const キーワードは存在しません
代わりに「大文字の変数名は定数扱い」という慣習を使います。
API_TIMEOUT = 30
DEFAULT_ROLE = "guest"
MAX_RETRY_COUNT = 3
定数の置き場所(実務)
  • app/core/constants.py
  • app/core/config.py(環境変数と併用)
  • Enum(状態・種別)は enum.Enum を使う

6-8. まとめ(事故らないための指針)

12. 文字型・数値型の相互変換(Python)

ポイント:
Python では「暗黙の型変換」は ほとんど行われません
API / DB / 外部入力では 明示的な変換が必須です。

12-1. 文字列 → 数値型

int への変換

value = "123"
num = int(value)

print(num)        # 123
print(type(num))  # <class 'int'>

float への変換

value = "3.14"
num = float(value)

print(num)        # 3.14
print(type(num))  # <class 'float'>
注意:
数値として解釈できない文字列は ValueError になります。
value = "abc"
num = int(value)  # ValueError

12-2. 数値型 → 文字列

count = 10
text = str(count)

print(text)       # "10"
print(type(text)) # <class 'str'>

文字列連結時の注意

# NG
# print("count=" + count)  # TypeError

# OK
print("count=" + str(count))
print(f"count={count}")
実務では f-string が基本 f"{変数}" は型を自動で文字列化してくれます。

12-3. float → int(切り捨て・丸め)

切り捨て(truncate)

value = 3.99
num = int(value)

print(num)  # 3

四捨五入

value = 3.5
num = round(value)

print(num)  # 4
注意:Python の round() は銀行丸めです。
round(2.5)2

12-4. 文字列 → 数値(安全に変換する)

def to_int(value: str, default: int = 0) -> int:
    try:
        return int(value)
    except ValueError:
        return default

print(to_int("10"))     # 10
print(to_int("abc"))    # 0
実務ポイント
  • ユーザー入力は 必ず失敗する前提
  • 例外処理 or バリデーションを必ず入れる

12-5. bool との変換(注意点)

bool → 文字列

flag = True
text = str(flag)

print(text)  # "True"

文字列 → bool(注意)

bool("False")  # True(非空文字列だから)
超重要:
bool("False")True になります。
文字列から bool を作る場合は明示的に判定してください。
def to_bool(value: str) -> bool:
    return value.lower() in ("true", "1", "yes")

12-6. FastAPI / Pydantic との関係

from pydantic import BaseModel

class User(BaseModel):
    age: int
    height: float

FastAPI では、リクエストJSONの文字列を
Pydantic が自動で数値に変換してくれます。

結論
  • 手動変換は「ロジック内」だけ
  • API入出力は Pydantic に任せる
  • 例外を握り潰さず、意味のあるエラーにする

13. Python の文字列フォーマット(sprintf / String.format 相当)

結論:
Python では f-string(フォーマット文字列) が最も推奨される書き方です。
可読性・安全性・性能のバランスが最も良く、実務の主流です。

13-1. f-string(最推奨・実務標準)

name = "Alice"
age = 20

text = f"name={name}, age={age}"
print(text)
# 出力
name=Alice, age=20
対応関係
  • C:sprintf(buf, "x=%d", x)
  • Java:String.format("x=%d", x)
  • Python:f"x={x}"

13-2. 数値フォーマット(桁数・小数)

小数点以下の桁数指定

value = 3.14159
text = f"{value:.2f}"
print(text)  # 3.14

ゼロ埋め

num = 7
text = f"{num:03d}"
print(text)  # 007

カンマ区切り

price = 1234567
text = f"{price:,}"
print(text)  # 1,234,567

13-3. 文字列幅・寄せ

name = "Bob"

print(f"|{name:<10}|")  # 左寄せ
print(f"|{name:>10}|")  # 右寄せ
print(f"|{name:^10}|")  # 中央寄せ

13-4. 式を直接書ける(f-stringの強み)

x = 10
y = 3

text = f"{x} / {y} = {x / y:.2f}"
print(text)
実務メリット 計算結果・条件式・関数呼び出しを
そのまま書けるため、ログが非常に書きやすい。

13-5. format()(旧来方式・参考)

text = "name={}, age={}".format("Alice", 20)
print(text)

Java の String.format に近い感覚ですが、
新規コードでは f-string 推奨です。

13-6. % フォーマット(C sprintf 互換・非推奨)

name = "Alice"
age = 20

text = "name=%s, age=%d" % (name, age)
print(text)
注意:
C の sprintf に似ていますが、
可読性・安全性の面で 現在は非推奨 です。

13-7. ログ出力での使い分け(重要)

# OK(f-string)
logger.info(f"user_id={user_id}, status={status}")
# logging 推奨(遅延評価)
logger.info("user_id=%s, status=%s", user_id, status)
実務ルール
  • ログ以外:f-string
  • logging:プレースホルダ形式(パフォーマンス向上)

13-8. まとめ

15. datetime(日付・時刻処理)

ポイント:Python の日付時刻は標準ライブラリ datetime が基本です。
実務(FastAPI)では「タイムゾーン」「ISO8601」「文字列⇔datetime」が頻出です。

15-1. 現在時刻を取得する

from datetime import datetime, timezone

now_local = datetime.now()                 # ローカル時刻(タイムゾーンなし)
now_utc   = datetime.now(timezone.utc)     # UTC(タイムゾーンあり)

print(now_local)
print(now_utc)
注意:
datetime.now() は「タイムゾーン情報なし(naive)」です。
API では UTC(timezone付き)を基本にすると事故りにくいです。

15-2. 文字列 ⇔ datetime(ISO 8601)

datetime → 文字列

from datetime import datetime, timezone

dt = datetime.now(timezone.utc)
text = dt.isoformat()   # 例: 2026-02-17T12:34:56.123456+00:00
print(text)

文字列 → datetime

from datetime import datetime

text = "2026-02-17T12:34:56+00:00"
dt = datetime.fromisoformat(text)
print(dt)

15-3. 任意フォーマット(strftime / strptime)

datetime → 文字列(strftime)

from datetime import datetime

dt = datetime(2026, 2, 17, 9, 30, 0)
print(dt.strftime("%Y/%m/%d %H:%M:%S"))  # 2026/02/17 09:30:00

文字列 → datetime(strptime)

from datetime import datetime

text = "2026/02/17 09:30:00"
dt = datetime.strptime(text, "%Y/%m/%d %H:%M:%S")
print(dt)

15-4. 日付の加減算(timedelta)

from datetime import datetime, timedelta

dt = datetime(2026, 2, 17, 9, 0, 0)
tomorrow = dt + timedelta(days=1)
after_90m = dt + timedelta(minutes=90)

print(tomorrow)
print(after_90m)

15-5. タイムゾーン(ZoneInfo)

from datetime import datetime
from zoneinfo import ZoneInfo

tokyo = ZoneInfo("Asia/Tokyo")
dt_tokyo = datetime.now(tokyo)

print(dt_tokyo)
実務の基本方針(おすすめ)
  • DB保存やAPI内部は UTC + timezone付き datetime
  • 表示だけユーザーのタイムゾーン(例:Asia/Tokyo)に変換
  • API入出力は ISO8601(isoformat())で統一

16. None / 空文字 / 0 の扱い(truthy / falsy)

ポイント:Python では if x: のような条件式に「真偽値として扱える値」が入ります。
これを truthy / falsy と呼び、実務でバグの原因にもなるので理解必須です。

16-1. falsy(条件式で False 扱いになる代表)

values = [None, "", 0, [], {}, set(), False]

for v in values:
    if v:
        print("truthy", v)
    else:
        print("falsy ", v)

16-2. None 判定は is を使う(最重要)

x = None

if x is None:
    print("x is None")

if x is not None:
    print("x has value")
注意:
「値が無い」を判定したいのに if not x: を使うと、
空文字や 0 もまとめて False 扱いになり、バグりやすいです。

16-3. 「None と空文字と 0」を区別したい例

def classify(x):
    if x is None:
        return "None"
    if x == "":
        return "empty string"
    if x == 0:
        return "zero"
    return "other"

print(classify(None))  # None
print(classify(""))    # empty string
print(classify(0))     # zero

16-4. 実務でよくあるNG例(0が消える)

# NG:0 も False 扱いで消える
age = 0
if not age:
    print("age is missing")  # ← 本当は age=0 が正しいかもしれない

OK(意図を明確にする):

age = 0
if age is None:
    print("age is missing")
結論
  • None 判定は is None
  • if x: は「空/0/None をまとめて弾きたい」時だけ
  • API入力の未指定は None を基本にする(Pydanticと相性が良い)

19. 配列(tuple)・list・set・dict の作成方法と使い方

ポイント:
Python には Java のような「配列クラス」はありませんが、
用途に応じて tuple / list / set / dict を使い分けます。

19-1. 配列(固定長っぽい用途): tuple

tupleイミュータブル(変更不可) な配列です。
「要素数・内容を変えたくない」用途で使います。

作成方法

# 括弧で作成
point = (10, 20)

# 括弧省略も可
color = 255, 128, 0

# 要素1個の tuple(カンマ必須)
single = (10,)

取り出し

x = point[0]
y = point[1]
print(x, y)

for による取り出し

for v in point:
    print(v)

分解代入(tuple の強み)

x, y = point
print(x, y)
使いどころ
  • 座標・RGB・戻り値が複数ある関数
  • 「変更させたくない」データ

19-2. list(可変長の配列)

作成方法

nums = [1, 2, 3]
names = list(["Alice", "Bob"])

追加・変更・削除

nums.append(4)      # 追加
nums[0] = 10        # 変更
nums.remove(2)      # 値で削除
last = nums.pop()   # 末尾を取り出して削除

for(foreach)で取り出す

for n in nums:
    print(n)

インデックス付き

for i, n in enumerate(nums):
    print(i, n)
list の特徴
  • 順序あり
  • 重複OK
  • 最もよく使うコレクション

19-3. set(集合:重複なし)

作成方法

names = {"Alice", "Bob", "Alice"}   # 重複は自動削除
empty = set()                       # 空集合({} は dict)

追加・削除

names.add("Carol")
names.discard("Bob")

for で取り出す

for name in names:
    print(name)

集合演算

a = {"A", "B", "C"}
b = {"B", "C", "D"}

print(a | b)  # 和集合
print(a & b)  # 積集合
print(a - b)  # 差集合
注意:set は順序を前提にしないでください。

19-4. dict(map:キー → 値)

作成方法

user = {
    "id": 1,
    "name": "Alice"
}

user2 = dict(id=2, name="Bob")

追加・更新・削除

user["age"] = 20
user["name"] = "Alicia"
del user["age"]

取り出し(for)

キーだけ:

for k in user:
    print(k)

キーと値:

for k, v in user.items():
    print(k, v)

安全に取得

age = user.get("age")        # 無ければ None
age2 = user.get("age", 0)    # 無ければ 0

19-5. 作成・用途まとめ

用途 使う型 理由
固定データ tuple 変更不可・安全
順序付きリスト list 柔軟・最頻出
重複排除 set 一意性保証
キー検索 dict 高速アクセス
覚え方
  • 変えない → tuple
  • 並べる → list
  • 重複消す → set
  • 対応付け → dict

17. コレクション(配列 / list / set / map(dict))の基本・取り出し・相互変換

ポイント:Python は Java のように「配列 / List / Set / Map」に相当するものがあります。
ただし Python は「for は foreach」が基本で、イテレーションが非常に強いです。

17-1. Python に存在する代表的なコレクション

種類 Python 特徴 Java相当
配列(固定長っぽい用途) tuple イミュータブル(変更不可) 配列(final的)
リスト list 順序あり・重複OK・可変 ArrayList
集合 set 重複なし・順序は本質ではない HashSet
マップ dict キー→値、順序保持(Python3.7+の仕様) HashMap/LinkedHashMap

17-2. list(順序あり)

作成・追加・削除

nums = [1, 2, 3]
nums.append(4)      # 末尾追加
nums.insert(0, 0)   # 先頭へ挿入
nums.remove(2)      # 値 2 を削除(最初の1つ)
last = nums.pop()   # 末尾を取り出して削除

print(nums)
print(last)

取り出し(for = foreach)

users = ["Alice", "Bob", "Carol"]

for u in users:
    print(u)

インデックス付き(enumerate)

for i, u in enumerate(users):
    print(i, u)

17-3. tuple(固定・変更しない)

point = (10, 20)
x, y = point  # 分解代入
print(x, y)
使いどころ 関数の戻り値を複数返す、座標、変更したくないデータに向きます。

17-4. set(重複を消す / 集合演算)

names = ["Alice", "Bob", "Alice", "Carol"]
unique = set(names)       # 重複除去
print(unique)

集合演算

a = {"A", "B", "C"}
b = {"B", "C", "D"}

print(a | b)  # 和集合
print(a & b)  # 積集合
print(a - b)  # 差集合

取り出し(for)

for x in unique:
    print(x)
注意:set は「順序が保証されない」と考えるのが安全です(表示順に意味を持たせない)。

17-5. dict(map:キー→値)

作成・追加・更新・削除

user = {"id": 1, "name": "Alice"}

user["age"] = 20           # 追加
user["name"] = "Alicia"    # 更新
del user["age"]            # 削除

print(user)

取り出し(キーだけ)

for k in user:
    print(k)  # key

キーと値(items)

for k, v in user.items():
    print(k, v)

安全に取得(get)

age = user.get("age")          # 無ければ None
age2 = user.get("age", 0)      # 無ければ 0
print(age, age2)

17-6. 相互変換(list / set / tuple / dict)

list → set(重複除去)

nums = [1, 2, 2, 3]
unique = set(nums)
print(unique)  # {1, 2, 3}

set → list(並べ替えてから使うのが定石)

unique = {3, 1, 2}
sorted_list = sorted(unique)
print(sorted_list)  # [1, 2, 3]

list ↔ tuple

nums = [1, 2, 3]
t = tuple(nums)
l = list(t)
print(t, l)

dict → list(キー、値、キー値ペア)

user = {"id": 1, "name": "Alice"}

keys = list(user.keys())
values = list(user.values())
pairs = list(user.items())

print(keys)
print(values)
print(pairs)

list(ペア)→ dict

pairs = [("id", 1), ("name", "Alice")]
user = dict(pairs)
print(user)

17-7. よく使う標準メソッドまとめ

list

set

dict

実務のコツ
  • 取り出しは基本 for(foreach)でOK
  • dict は items() を使うと一気に読みやすくなる
  • set は「重複除去」と「集合演算」に最強
  • 順序が必要なら list / tuple を使う

7. ミュータブル / イミュータブル完全理解

結論:Python では「変数=箱」ではなく、
変数はオブジェクトへの参照です。
ミュータブルかどうかで「書き換え時の挙動」が激変します。

7-1. ミュータブル / イミュータブルとは

分類 特徴
イミュータブル int, float, str, tuple, bool 値を変更できない(変更=新しいオブジェクト)
ミュータブル list, dict, set 中身を直接変更できる

7-2. イミュータブルの例

a = 10
b = a
b += 1

print(a)  # 10
print(b)  # 11

b += 1新しい int オブジェクト を作るだけ。
a は一切影響を受けません。

7-3. ミュータブルの例(危険)

a = [1, 2]
b = a
b.append(3)

print(a)  # [1, 2, 3]
print(b)  # [1, 2, 3]
ポイント:
ab同じ list オブジェクト を指しています。
片方を変更すると、もう片方も変わります。

7-4. 実務での鉄則

4. Python の演算子まとめ

4-1. 算術演算子

演算子意味
+加算1 + 2
-減算5 - 3
*乗算2 * 3
/除算(float)5 / 2
//除算(整数)5 // 2
%余り5 % 2

4-2. 論理演算子

if a > 0 and b < 10:
    print("OK")

if not is_valid:
    print("NG")
演算子意味
and論理AND
or論理OR
not否定

4-3. ビット演算子

a = 0b1010
b = 0b1100

print(a & b)  # AND
print(a | b)  # OR
print(a ^ b)  # XOR

4-4. 代入演算子(網羅)

ポイント:Python の代入演算子は「算術・ビット・シフト」を = と組み合わせたものが一通り揃っています。
Java / PHP 経験者には見慣れたものがほとんどです。
演算子 意味 等価な書き方
= 代入 x = 10 -
+= 加算して代入 x += 1 x = x + 1
-= 減算して代入 x -= 1 x = x - 1
*= 乗算して代入 x *= 2 x = x * 2
/= 除算して代入 x /= 2 x = x / 2
//= 整数除算して代入 x //= 2 x = x // 2
%= 剰余して代入 x %= 3 x = x % 3
**= 累乗して代入 x **= 2 x = x ** 2
&= ビットANDして代入 x &= 0b1010 x = x & 0b1010
|= ビットORして代入 x |= 0b0101 x = x | 0b0101
^= ビットXORして代入 x ^= 0b0011 x = x ^ 0b0011
<<= 左シフトして代入 x <<= 1 x = x << 1
>>= 右シフトして代入 x >>= 1 x = x >> 1
実務でよく使うもの
  • API系:+=, -=, *=
  • 数値処理://=, %=
  • フラグ処理:|=, &=
注意点(初心者が混乱しやすい)
  • /= は必ず float になる(整数に戻らない)
  • Python には ++ / -- は存在しない
  • ミュータブル(list, dict)では += が破壊的になる

4-2. 比較演算子(条件判定の基本)

演算子 意味
== 等しい a == b
!= 等しくない a != b
> より大きい a > b
>= 以上 a >= b
< より小さい a < b
<= 以下 a <= b
is 同一オブジェクトか a is None
is not 同一オブジェクトでない a is not None
in 含まれているか x in [1,2,3]
not in 含まれていないか x not in [1,2,3]
PHP / Java 出身者がハマりやすい点
  • None 判定は == ではなく is を使う
  • 文字列比較は == でOK(Javaの equals() 不要)
  • in は配列・文字列・dictキーに使える(超頻出)

4-5. 三項演算子(条件式)

result = "OK" if status == 200 else "NG"
PHP との違い
  • PHP && || ! → Python and / or / not
  • PHP 三項 ?: → Python a if cond else b

5. Python で正規表現(Regex)を扱う方法(実務向け)

Python の正規表現は re モジュールを使います。
FastAPI では「入力チェック」「ログ解析」「簡易パース」で頻出です。

5-1. 基本的な使い方

import re

pattern = r"\d{4}-\d{2}-\d{2}"
text = "2026-02-01"

if re.match(pattern, text):
    print("日付形式OK")

5-2. よく使う関数

関数用途
re.match()先頭から一致
re.search()途中一致
re.findall()全一致をリストで取得
re.sub()置換

5-3. 実務で多い例

メールアドレスチェック(簡易)

email_pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"

if re.match(email_pattern, email):
    print("valid")

ログからID抽出

log = "user_id=12345 action=login"
m = re.search(r"user_id=(\d+)", log)

if m:
    user_id = m.group(1)

5-4. re.compile を使う理由(重要)

pattern = re.compile(r"\d+")

pattern.findall("id=10 id=20")
✅ 実務ポイント
  • 同じ正規表現を何度も使う → re.compile()
  • 完璧なメール正規表現は作らない(壊れる)
  • FastAPI では Pydantic の型チェック + Regex の組み合わせが最強
PHP との対応
  • PHP preg_match → Python re.match / re.search
  • PHP preg_replace → Python re.sub
実務の定番フロー:DBレコード → dict → Pydantic → JSON

FastAPI では、DBの生データをそのまま返すことはほとんどありません。
安全性・型保証・JSON変換 のため、以下の流れを取るのが定石です。

① DBレコード(生データ)

DBアクセス層(Repository)では、
RDB / ORM / sqlite などから 行データ を取得します。

# Repository層(例:sqlite3)
def find_user_by_id(user_id: int):
    # DBから1行取得した想定
    return (1, "Alice", 20)   # ← タプル(列順)

② dict(意味のある構造に変換)

Service層で DBレコードを dict に変換します。
ここで「データに意味」を与えます。

# Service層
def to_user_dict(row):
    return {
        "id": row[0],
        "name": row[1],
        "age": row[2],
    }
dict にする理由:
  • キー名で意味が分かる
  • JSON変換しやすい
  • Pydanticに渡しやすい

③ Pydantic(型保証・バリデーション)

APIの入出力は Pydanticモデル を通します。
これにより、型不正・欠損・余計な項目 を自動で防げます。

from pydantic import BaseModel

class UserResponse(BaseModel):
    id: int
    name: str
    age: int | None = None
# dict → Pydantic
user_dict = {
    "id": 1,
    "name": "Alice",
    "age": 20
}

user = UserResponse(**user_dict)

④ JSON(FastAPIが自動変換)

FastAPI では、Pydanticモデルを return するだけで、
自動的に JSON に変換されます。

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
    row = find_user_by_id(user_id)
    data = to_user_dict(row)
    return UserResponse(**data)
# HTTPレスポンス(JSON)
{
  "id": 1,
  "name": "Alice",
  "age": 20
}

なぜこの流れがベストプラクティスなのか

設計の役割分担
  • Repository:DB → 生データ
  • Service:生データ → dict(意味付け)
  • Pydantic:dict → 型保証されたモデル
  • FastAPI:モデル → JSON

10. Python の命名規則(クラス・関数・変数・定数・モジュール)

結論:Python では PEP 8(公式スタイルガイド)に従います。
Java の camelCase 文化とは違い、snake_case が基本です。

10-1. 命名規則の全体像

対象 命名規則
クラス PascalCase UserService, OrderStatus
関数 snake_case get_user, create_order
変数 snake_case user_id, is_active
定数 UPPER_SNAKE_CASE MAX_RETRY_COUNT
モジュール(.py) snake_case user_service.py
パッケージ(フォルダ) snake_case routers, user_service

10-2. クラス命名(PascalCase)

class UserService:
    pass

class OrderStatus:
    pass

10-3. 関数・メソッド命名(snake_case)

def get_user(user_id: int):
    pass

def create_order(data):
    pass

10-4. 変数命名(snake_case)

user_name = "Alice"
is_active = True
order_count = 3

10-5. 定数命名(UPPER_SNAKE_CASE)

API_TIMEOUT = 30
DEFAULT_PAGE_SIZE = 20
Python に const はありません。
大文字=定数扱い は慣習(書き換えない約束)です。

10-6. インターフェース相当(Protocol / ABC)

Python には Java の interface はありませんが、
役割としてのインターフェース は存在します。

Protocol(推奨・Pythonic)

from typing import Protocol

class UserRepository(Protocol):
    def find_by_id(self, user_id: int): ...

ABC(抽象基底クラス)

from abc import ABC, abstractmethod

class UserRepository(ABC):
    @abstractmethod
    def find_by_id(self, user_id: int):
        pass

10-7. 非公開(internal)を表す命名

def _internal_function():
    pass

class Service:
    def _helper(self):
        pass

10-8. FastAPI 実務向け命名指針

実務での統一ルール例
  • URL:/users/{user_id}(snake_case)
  • 関数名:get_user
  • Schema:UserCreate, UserResponse
  • Service:UserService
  • Router:users.py

10-9. Java / PHP との違いまとめ

言語 関数 変数 クラス
Java camelCase camelCase PascalCase
PHP camelCase camelCase PascalCase
Python snake_case snake_case PascalCase
覚え方 Python は「クラスだけ Camel(Pascal)、それ以外は snake」。

11. Python のファイル分け(モジュール分割の考え方)

結論:Python には Java のような「1クラス1ファイル」強制ルールはありません。
しかし 実務では“役割単位でファイルを分ける”のが定石です。

11-1. Python におけるファイル分けの基本思想

Java との考え方の違い
  • Java:型(クラス)中心で分ける
  • Python:役割・機能中心で分ける

11-2. インターフェース相当のファイル分け

Python には interface キーワードはありません。
実務では以下のどちらかを使い、専用ファイルに分けます。

Protocol(推奨)

# app/repositories/user_repository.py
from typing import Protocol

class UserRepository(Protocol):
    def find_by_id(self, user_id: int): ...

11-3. 抽象クラス(ABC)のファイル分け

# app/services/base_service.py
from abc import ABC, abstractmethod

class BaseService(ABC):

    @abstractmethod
    def execute(self):
        pass

11-4. 具象(public)クラスのファイル分け

# app/services/user_service.py
from app.repositories.user_repository import UserRepository

class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo

    def get_user(self, user_id: int):
        return self.repo.find_by_id(user_id)

11-5. 実装クラス(Repository / Adapter)の分け方

# app/repositories/user_repository_impl.py
from app.repositories.user_repository import UserRepository

class UserRepositoryImpl(UserRepository):
    def find_by_id(self, user_id: int):
        return {"id": user_id, "name": "Alice"}
注意:
ファイル名に impl を付けるかはチーム次第。
Python では inmemory_user_repository.py のように
実装の特徴を書く方が好まれます。

11-6. 関数中心ファイルの分け方

# app/utils/date_utils.py
def parse_date(value: str):
    ...

def format_date(dt):
    ...

11-7. ファイル分けの実務テンプレ(FastAPI)

app/
  routers/
    users.py          # API入口
  services/
    user_service.py   # 業務ロジック
  repositories/
    user_repository.py        # Protocol
    inmemory_user_repository.py
  schemas/
    user.py           # Pydantic
  core/
    config.py

11-8. やりがちなアンチパターン

⚠ よくある失敗
  • 1ファイルに全部書く(main.py 肥大化)
  • 役割の違うクラスを同居させる
  • 循環 import が発生する構成
  • Java流で package を深く掘りすぎる

11-9. 判断基準(迷ったら)

分けるかどうかの基準
  • ファイルが 300行を超えたら分割検討
  • 「責務が2つ以上」なら分ける
  • import が見通せなくなったら分ける

20. 関数・メソッドの定義と使い方(戻り値・引数・ラムダ)

ポイント:
Python では「関数」と「メソッド」は文法的には同じです。
クラスの中に定義された関数を メソッド と呼びます。

20-1. 関数の定義方法

def add(a, b):
    return a + b

result = add(2, 3)
print(result)  # 5

20-2. メソッドの定義方法(クラス内)

class Calculator:
    def add(self, a, b):
        return a + b

calc = Calculator()
print(calc.add(2, 3))
self は「自分自身(インスタンス)」を指します。
Java の this に相当します。

20-3. 戻り値(return)の指定

1つの値を返す

def get_name():
    return "Alice"

複数の値を返す(tuple)

def get_point():
    return 10, 20   # tuple を返す

x, y = get_point()
print(x, y)

return が無い場合

def do_nothing():
    pass

result = do_nothing()
print(result)  # None

20-4. 戻り値の型指定(型ヒント)

def add(a: int, b: int) -> int:
    return a + b

20-5. 引数の種類

① 通常の引数

def greet(name):
    print(f"Hello {name}")

② 初期値(デフォルト引数)

def greet(name="Guest"):
    print(f"Hello {name}")
注意:
デフォルト引数に listdict を使わない(地雷)。

③ 可変長引数(*args)

def total(*args):
    return sum(args)

print(total(1, 2, 3))  # 6

*args は tuple として受け取ります。

④ キーワード引数(**kwargs)

def print_user(**kwargs):
    for k, v in kwargs.items():
        print(k, v)

print_user(name="Alice", age=20)

**kwargs は dict として受け取ります。

⑤ 名前付き引数(キーワード引数)

def connect(host, port):
    print(host, port)

connect(port=5432, host="localhost")

20-6. 引数の順序ルール(重要)

def func(a, b=0, *args, c=1, **kwargs):
    pass

引数の順序は以下の通りです:

  1. 通常引数
  2. デフォルト引数
  3. *args
  4. キーワード専用引数
  5. **kwargs

20-7. ラムダ関数(無名関数)

# 通常の関数
def square(x):
    return x * x

# ラムダ関数
square_lambda = lambda x: x * x

print(square(3))
print(square_lambda(3))

よく使う場面(sorted / map / filter)

nums = [1, 2, 3, 4]

# key にラムダ
sorted_nums = sorted(nums, key=lambda x: -x)

# map
squares = list(map(lambda x: x * x, nums))

# filter
evens = list(filter(lambda x: x % 2 == 0, nums))
ラムダの指針
  • 1行で終わる簡単な処理だけ
  • 複雑になったら def に戻す
  • 可読性を最優先

20-8. まとめ

21. with 文(リソース管理)

with 文(リソース管理)

with 文は、リソースを安全に確実に解放するための構文です。
Java の try-with-resources に相当します。

典型例:ファイル操作

# with を使わない場合(危険)
f = open("data.txt", "r")
content = f.read()
f.close()
# with を使う場合(安全)
with open("data.txt", "r") as f:
    content = f.read()
# ここで自動的に close() される

例外が起きても安全

with open("data.txt", "r") as f:
    for line in f:
        print(line)
        raise RuntimeError("エラー発生")  # それでも close される

DB / ロック / ネットワークでも使われる

# DB接続のイメージ
with db.connect() as conn:
    conn.execute("SELECT * FROM users")

自作クラスで with を使えるようにする

class Resource:
    def __enter__(self):
        print("open")
        return self

    def __exit__(self, exc_type, exc, tb):
        print("close")

with Resource():
    print("using resource")
実務指針:
  • open / lock / 接続系は必ず with
  • finally より with を優先
  • FastAPIでもファイル・DB・一時リソースに多用

21. Javaの interface / abstract class / class / enum / record 相当を Python でどう定義するか(違いとサンプル)

結論:Python には Java の構文(interface / record など)がそのままはありません。
ただし「同じ目的」を達成するための仕組み(Protocol / ABC / dataclass / Enum)があり、実務ではそれを使います。

21-1. interface 相当:Protocol(推奨) / ABC(厳格)

① Protocol(Pythonic・実務推奨:構造的サブタイピング)

from typing import Protocol

class UserRepository(Protocol):
    """
    Javaのinterface相当。
    実装クラスが '同じメソッドを持っていれば' それでOK(duck typing + 型チェック)。
    """
    def find_by_id(self, user_id: int) -> dict: ...
class InMemoryUserRepository:
    """interfaceをimplementsしなくても、メソッドがあればOK。"""
    def find_by_id(self, user_id: int) -> dict:
        return {"id": user_id, "name": "Alice"}
Javaとの違い
  • Java:implements を宣言して契約を強制
  • Python(Protocol):宣言しなくても「形が合えばOK」(構造型)
  • IDE / mypy で型安全を得る(実行時には強制されない)

② ABC(抽象基底クラス:実行時にもある程度“強制”したい場合)

from abc import ABC, abstractmethod

class UserRepositoryABC(ABC):
    """
    Javaのinterface + abstract method に近い。
    抽象メソッドを未実装だとインスタンス化できない。
    """
    @abstractmethod
    def find_by_id(self, user_id: int) -> dict:
        pass

21-2. abstract class:ABC を使う

from abc import ABC, abstractmethod

class BaseService(ABC):
    """
    Javaのabstract class相当。
    共通処理(具体メソッド)と、子で実装すべき抽象メソッドを混在できる。
    """
    def log_start(self) -> None:
        print("start")  # 共通処理(具体メソッド)

    @abstractmethod
    def execute(self) -> dict:
        """子クラスで必ず実装してほしい処理"""
        pass
class UserService(BaseService):
    def execute(self) -> dict:
        self.log_start()
        return {"result": "ok"}
Javaとの違い
  • Java:アクセス修飾子や抽象メソッドが言語で厳密に守られる
  • Python:ABCで“近いこと”はできるが、スタイルはより柔軟
  • FastAPI実務では「継承」より「依存注入(DI)+合成」が多い

21-3. class:通常クラス(最頻出)

class User:
    """
    Javaのclass相当。
    Pythonはフィールド宣言が必須ではなく、__init__ で属性を作る。
    """
    def __init__(self, user_id: int, name: str):
        self.user_id = user_id
        self.name = name

    def greet(self) -> str:
        return f"Hello, {self.name}"
u = User(1, "Alice")
print(u.greet())
Javaとの違い(重要)
  • Java:フィールド宣言がクラス定義に書かれる
  • Python:self.xxx = した瞬間に属性が生成される(宣言不要)
  • 型ヒントは任意(ただしIDE・レビュー・保守のため実務では書く)

21-4. enum:Enum クラス(標準ライブラリ)

from enum import Enum

class OrderStatus(Enum):
    """
    Javaのenum相当。
    値はシンボルとして扱われ、比較は '==' でOK。
    """
    NEW = "new"
    PAID = "paid"
    SHIPPED = "shipped"
status = OrderStatus.NEW

if status == OrderStatus.NEW:
    print("new order")

print(status.value)  # "new"
Javaとの違い
  • Java:enumはクラスに近く、フィールドやメソッドも持てる
  • Python:Enumもメソッドを持てるが、通常は「区分値の安全化」が主目的
  • FastAPIではレスポンスやバリデーションにも使える

21-5. record 相当:dataclass / NamedTuple(目的別に使い分け)

Javaのrecordは「不変データの入れ物(自動生成)」です。
Pythonでは主に dataclassNamedTuple で同じ目的を達成します。

① dataclass(実務で最も使う“データの入れ物”)

from dataclasses import dataclass

@dataclass(frozen=True)
class UserRecord:
    """
    Java record に近い用途。
    frozen=True にすると不変(イミュータブル)にできる。
    """
    user_id: int
    name: str
u = UserRecord(1, "Alice")
print(u.user_id, u.name)
# u.name = "Bob"  # frozen=True ならエラー(不変)

② NamedTuple(より“タプル寄り”の不変データ)

from typing import NamedTuple

class Point(NamedTuple):
    """
    recordというより「名前付きtuple」。
    軽量で不変、分解代入も得意。
    """
    x: int
    y: int
p = Point(10, 20)
print(p.x, p.y)

x, y = p  # 分解代入
print(x, y)
Java record との違いまとめ
  • Java record:言語機能として固定(equals/hashCode/toStringなど自動)
  • Python:用途で選ぶ(dataclass / NamedTuple)
  • FastAPIの入出力は record ではなく Pydantic を使うのが定石

21-6. FastAPI視点:Pydantic(record“っぽい”が目的が違う)

from pydantic import BaseModel

class UserResponse(BaseModel):
    """
    Java record のように「データの入れ物」に見えるが、目的は違う。
    API入力/出力の型保証とバリデーションが主目的。
    """
    id: int
    name: str
    age: int | None = None
# dict -> Pydantic -> JSON(FastAPIが自動)
user = UserResponse(id=1, name="Alice", age=20)
print(user.model_dump())  # dict化(Pydantic v2)
実務の使い分け(超重要)
  • APIの入出力:Pydantic
  • 内部データの入れ物:dataclass(必要なら frozen)
  • 区分値:Enum
  • 契約(interface相当):Protocol(必要なら ABC)

21-7. まとめ

22. Protocol(構造型):継承しなくても「形が合えばOK」とはどういう意味か

結論:
Python の Protocol は Java の interface と違い、
「implements しているか」ではなく「必要なメソッドを持っているか」 で判定されます。
これを 構造的サブタイピング(構造型) と呼びます。

22-1. 前提:Protocol(契約)を定義する

from typing import Protocol

class UserRepository(Protocol):
    """
    Javaのinterface相当。
    「find_by_id(int) -> dict を持っていること」が契約。
    """
    def find_by_id(self, user_id: int) -> dict:
        ...

この時点では、
UserRepository を継承しなければならない」 とは 一切書かれていません

22-2. 継承していないが「形が合っている」例(構造型)

class InMemoryUserRepository:
    """
    UserRepository を継承していない。
    しかし、同じ名前・引数・戻り値のメソッドを持っている。
    """
    def find_by_id(self, user_id: int) -> dict:
        return {"id": user_id, "name": "Alice"}
def get_user_name(repo: UserRepository, user_id: int) -> str:
    """
    引数 repo は UserRepository として扱われる。
    """
    user = repo.find_by_id(user_id)
    return user["name"]


repo = InMemoryUserRepository()
print(get_user_name(repo, 1))  # Alice
ここで何が起きているか
  • InMemoryUserRepositoryProtocolを継承していない
  • しかし find_by_id() を持っている
  • そのため 「UserRepositoryの形を満たしている」と判定される
  • これが 構造型(structural typing)

22-3. 継承している例(名義的に契約を示す)

class SqlUserRepository(UserRepository):
    """
    UserRepository を明示的に継承している。
    Javaの implements に近い書き方。
    """
    def find_by_id(self, user_id: int) -> dict:
        return {"id": user_id, "name": "Bob"}
repo = SqlUserRepository()
print(get_user_name(repo, 1))  # Bob
この書き方は「必須」ではありませんが、
クラス定義を見ただけで役割が分かるため、
チーム開発では好まれることがあります。

22-4. 「形が合っていない」とどうなるか

class BadRepository:
    """
    メソッド名が契約と違う。
    """
    def find(self, user_id: int) -> dict:
        return {"id": user_id}
# repo = BadRepository()
# get_user_name(repo, 1)
# 実行時:AttributeError(find_by_id が無い)

22-5. Javaとの決定的な違い

観点 Java Python Protocol
契約の成立条件 implements しているか 必要なメソッドを持っているか
型の考え方 名義型(名前重視) 構造型(形重視)
強制力 コンパイル時に強制 主に型チェック時(実行時は柔軟)

22-6. なぜ Python はこの設計なのか

実務での指針
  • 「差し替えたい依存」には Protocol を使う
  • 継承は必須にしない(柔軟性を保つ)
  • 型安全は IDE / mypy に任せる

23. Python における「構造体(struct)」相当の考え方

結論:
Python には C / Java の struct は存在しません。
代わりに 目的別に複数の「構造体的な仕組み」 が用意されています。

23-1. dataclass(最も一般的な構造体相当)

dataclass は「データを保持するだけのクラス」を
簡単・安全に定義するための仕組みです。
実務では struct の代わりにこれを使うのが基本です。

from dataclasses import dataclass

@dataclass
class User:
    """
    Cのstruct / JavaのDTO・record相当
    """
    id: int
    name: str
    age: int
u = User(1, "Alice", 20)
print(u.id, u.name, u.age)

不変(immutable)な構造体にする

@dataclass(frozen=True)
class Point:
    x: int
    y: int
p = Point(10, 20)
# p.x = 30  # エラー(不変)
使いどころ
  • 業務データの入れ物
  • 内部処理用DTO
  • Java record に最も近い

23-2. NamedTuple(軽量・不変な構造体)

NamedTuple は「名前付き tuple」です。
軽量・高速・不変という特徴があります。

from typing import NamedTuple

class Point(NamedTuple):
    x: int
    y: int
p = Point(10, 20)
print(p.x, p.y)

x, y = p   # 分解代入
print(x, y)

23-3. SimpleNamespace(動的な構造体)

実行時にフィールドを自由に追加したい場合は
SimpleNamespace が使えます。

from types import SimpleNamespace

user = SimpleNamespace()
user.id = 1
user.name = "Alice"
user.age = 20

print(user.name)
注意:
業務ロジックやAPIデータでは
dataclass / Pydantic の方が安全です。

23-4. dict(最も原始的な構造体)

user = {
    "id": 1,
    "name": "Alice",
    "age": 20
}

23-5. ctypes.Structure(C互換struct・特殊用途)

C言語の structメモリレイアウト互換が必要な場合、
ctypes.Structure を使います。

from ctypes import Structure, c_int, c_char_p

class CUser(Structure):
    _fields_ = [
        ("id", c_int),
        ("name", c_char_p),
    ]

23-6. どれを使うべきか(まとめ)

目的 使うもの
業務データ・DTO dataclass
不変データ・値オブジェクト NamedTuple / dataclass(frozen)
一時的まとめ SimpleNamespace
API入出力 Pydantic
C連携 ctypes.Structure
実務の鉄則
  • まず dataclass を検討する
  • API境界では Pydantic
  • dict を構造体代わりに使い続けない

23. Python における「構造体(struct)」相当の考え方

結論:
Python には C / Java の struct は存在しません。
代わりに 目的別に複数の「構造体的な仕組み」 が用意されています。

23-1. dataclass(最も一般的な構造体相当)

dataclass は「データを保持するだけのクラス」を
簡単・安全に定義するための仕組みです。
実務では struct の代わりにこれを使うのが基本です。

from dataclasses import dataclass

@dataclass
class User:
    """
    Cのstruct / JavaのDTO・record相当
    """
    id: int
    name: str
    age: int
u = User(1, "Alice", 20)
print(u.id, u.name, u.age)

不変(immutable)な構造体にする

@dataclass(frozen=True)
class Point:
    x: int
    y: int
p = Point(10, 20)
# p.x = 30  # エラー(不変)
使いどころ
  • 業務データの入れ物
  • 内部処理用DTO
  • Java record に最も近い

23-2. NamedTuple(軽量・不変な構造体)

NamedTuple は「名前付き tuple」です。
軽量・高速・不変という特徴があります。

from typing import NamedTuple

class Point(NamedTuple):
    x: int
    y: int
p = Point(10, 20)
print(p.x, p.y)

x, y = p   # 分解代入
print(x, y)

23-3. SimpleNamespace(動的な構造体)

実行時にフィールドを自由に追加したい場合は
SimpleNamespace が使えます。

from types import SimpleNamespace

user = SimpleNamespace()
user.id = 1
user.name = "Alice"
user.age = 20

print(user.name)
注意:
業務ロジックやAPIデータでは
dataclass / Pydantic の方が安全です。

23-4. dict(最も原始的な構造体)

user = {
    "id": 1,
    "name": "Alice",
    "age": 20
}

23-5. ctypes.Structure(C互換struct・特殊用途)

C言語の structメモリレイアウト互換が必要な場合、
ctypes.Structure を使います。

from ctypes import Structure, c_int, c_char_p

class CUser(Structure):
    _fields_ = [
        ("id", c_int),
        ("name", c_char_p),
    ]

23-6. どれを使うべきか(まとめ)

目的 使うもの
業務データ・DTO dataclass
不変データ・値オブジェクト NamedTuple / dataclass(frozen)
一時的まとめ SimpleNamespace
API入出力 Pydantic
C連携 ctypes.Structure
実務の鉄則
  • まず dataclass を検討する
  • API境界では Pydantic
  • dict を構造体代わりに使い続けない

Pythonにおけるイテレータとジェネレータ

1. まず結論(超重要)

  • イテレータ次の要素を1つずつ取り出せるオブジェクト
  • ジェネレータイテレータを簡単に作るための仕組み
  • for文が回せるものは、ほぼすべてイテレータ
  • ジェネレータは メモリ効率が非常に良い

2. イテレータとは何か

イテレータとは、
「次の要素を1つずつ返すことができるオブジェクト」です。

2-1. イテレータの条件

2-2. listはイテレータ?

nums = [1, 2, 3]

it = iter(nums)      # イテレータを取得
print(next(it))      # 1
print(next(it))      # 2
print(next(it))      # 3
# print(next(it))    # StopIteration
list自体はイテラブル(iterable)であり、
iter(list) した結果が「イテレータ」です。

3. 自作イテレータ(クラスで書く場合)

Javaでいう Iterator を自分で実装する感覚に近いです。

# 0 から max-1 まで返すイテレータ
class CounterIterator:
    def __init__(self, max_value: int):
        self.max = max_value
        self.current = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.max:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

for i in CounterIterator(3):
    print(i)
# 0, 1, 2
正直な話:
この書き方は「仕組みを理解するため用」。
実務では次に説明する ジェネレータを使うことがほぼ100%です。

4. ジェネレータとは何か

ジェネレータは、
yield を使って「途中で止まりながら値を返す関数」です。

def counter(max_value: int):
    current = 0
    while current < max_value:
        yield current
        current += 1

for i in counter(3):
    print(i)
# 0, 1, 2
yield がある関数は、呼び出した瞬間には実行されない
→ next() や for で回された時に少しずつ実行される

5. ジェネレータの動作イメージ

  1. 関数呼び出し → ジェネレータオブジェクトが返る
  2. next() が呼ばれる
  3. yield まで処理が進む
  4. 値を返して 状態を保持したまま停止
  5. 次の next() で続きから再開

6. ジェネレータ式(generator expression)

list内包表記とほぼ同じ見た目ですが、
() を使うとジェネレータになります。

# list(すべてメモリに載る)
lst = [i * 2 for i in range(5)]

# generator(1件ずつ生成)
gen = (i * 2 for i in range(5))

print(lst)        # [0, 2, 4, 6, 8]
print(next(gen))  # 0
print(next(gen))  # 2
違いの本質:
list内包表記 → 結果を全部作ってから返す
ジェネレータ式 → 必要になった時に1件ずつ作る

7. メモリ効率の違い(実務で重要)

# 巨大ファイルの行を処理する例

# 悪い例(全行をメモリに読む)
lines = open("big.txt").readlines()
for line in lines:
    process(line)

# 良い例(1行ずつ処理)
def read_lines(path):
    with open(path) as f:
        for line in f:
            yield line

for line in read_lines("big.txt"):
    process(line)
FastAPI やバッチ処理で 巨大データを扱う場合は必須知識です。

8. イテレータ vs ジェネレータ まとめ

項目 イテレータ ジェネレータ
作り方 __iter__ / __next__ を実装 yield を書くだけ
実装量 多い 少ない
可読性 低い 高い
実務利用 ほぼ使わない 非常によく使う

9. Java経験者向け一言まとめ

Javaの Iterator を毎回クラスで書く代わりに、
Pythonでは yield で書けると思ってください。

FastAPI / DB / ファイル / ストリーミングでは、
ジェネレータが自然に使われています。

sorted / key / lambda の使い方(Python並び替えの完全理解)

1. まず結論(ここだけ読めば8割分かる)

  • sorted():新しい並び替え済みリストを返す
  • key:比較に使う「基準値を取り出す関数」
  • lambda:その場で書く小さな関数
  • 「keyで何を取り出すか」= 並び順のルール

2. sorted() の基本

sorted()元のデータを壊さず
並び替えた 新しい list を返します。

nums = [5, 1, 3, 2]
result = sorted(nums)

print(result)  # [1, 2, 3, 5]
print(nums)    # [5, 1, 3, 2](元のまま)
破壊的に並び替えたい場合は list.sort() を使います。

3. key とは何か(ここが一番大事)

key「各要素から、並び替え用の値を取り出す関数」です。

3-1. keyなしの場合

words = ["banana", "apple", "cherry"]
print(sorted(words))
# ['apple', 'banana', 'cherry']

→ 要素そのもの(文字列)が比較される


3-2. keyを使う場合(文字列長でソート)

words = ["banana", "apple", "cherry"]

result = sorted(words, key=len)
print(result)
# ['apple', 'banana', 'cherry']
重要:
sorted は len("banana") のような値で並び替えているが、
返ってくる要素は元の要素

4. lambda を使った key の書き方

lambda1行で書ける無名関数です。

lambda x: x * 2
# ↑ 引数 x を受け取って x*2 を返す関数

4-1. dict の特定キーで並び替える

users = [
    {"id": 3, "name": "Bob"},
    {"id": 1, "name": "Alice"},
    {"id": 2, "name": "Carol"},
]

result = sorted(users, key=lambda u: u["id"])
print(result)

id の昇順で並ぶ


4-2. オブジェクト(Entity / DTO)で並び替える

# UserEntity(id, name)
result = sorted(entities, key=lambda e: e.id)

e.id を基準に並ぶ


5. 複数条件で並び替える(超実務)

Pythonでは タプル を返せば、
左から順に比較されます。

# age 昇順 → name 昇順
result = sorted(users, key=lambda u: (u["age"], u["name"]))
SQLの ORDER BY age, name と同じ発想です。

6. 降順で並び替える

# id を降順
result = sorted(users, key=lambda u: u["id"], reverse=True)

7. key に関数名を渡す vs lambda

def get_id(u):
    return u["id"]

sorted(users, key=get_id)      # OK
sorted(users, key=lambda u: u["id"])  # もっと一般的
使い分けの目安:
  • 1行で済む → lambda
  • 処理が長い・再利用したい → 通常の関数

8. よくある勘違い(初心者ポイント)


9. Java経験者向け対応表

Python Java
sorted(list, key=lambda x: x.id) list.stream().sorted(Comparator.comparing(x -> x.id))
reverse=True Comparator.reversed()

10. 実務でのまとめ

  • 並び替えは sorted + key + lambda が基本セット
  • 複雑な並び替えでも keyで値を組み立てる
  • 比較関数を書く発想は捨てる

24. Pydantic の概要と使い方 → dataclass との変換(相互変換)

ポイント:
FastAPI 実務では「API境界(入出力)」は Pydantic
「内部処理のデータの入れ物」は dataclass を使うことが多いです。
そのため Pydantic ⇔ dataclass の変換が頻出になります。

24-1. Pydantic とは(概要・目的)

Pydantic は「型ヒント」をもとに、
データのバリデーション(検証)変換を行うライブラリです。
FastAPI の Request / Response は Pydantic で表現するのが定石です。

24-2. Pydantic の基本的な使い方(定義・生成・バリデーション)

from pydantic import BaseModel

class UserRequest(BaseModel):
    """
    APIの入力(例:POST /users のリクエストBody)
    """
    name: str
    age: int | None = None

生成(dict から)

payload = {"name": "Alice", "age": "20"}  # ageが文字列でも…
user_req = UserRequest(**payload)

print(user_req.name)  # Alice
print(user_req.age)   # 20 (intに変換される)
Pydantic がやっていること
  • ageint に変換できるか試す
  • 変換できなければ例外(ValidationError)
  • 型安全なオブジェクトとして以後扱える

24-3. FastAPI での利用例(呼び出し例)

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserResponse(BaseModel):
    id: int
    name: str
    age: int | None = None

@app.post("/users", response_model=UserResponse)
def create_user(req: UserRequest):
    # req は既にバリデーション済み
    # 本来はDB登録してIDを発行するが、ここではダミー
    return UserResponse(id=1, name=req.name, age=req.age)

FastAPI は リクエストJSON → Pydantic → Pythonオブジェクト
Pydantic → JSONレスポンス を自動で行います。

24-4. Pydantic と dataclass の役割分担(実務)

主役 理由
API入出力(境界) Pydantic バリデーション・JSON変換が強い
業務ロジック(内部) dataclass 軽量・読みやすい・意図が明確

24-5. dataclass の定義(内部用データ)

from dataclasses import dataclass

@dataclass
class UserEntity:
    """
    内部処理用(DB/サービス層)
    """
    id: int
    name: str
    age: int | None = None

24-6. 変換①:Pydantic → dataclass(API入力を内部データへ)

API入力(Pydantic)を、内部データ(dataclass)へ渡すときの定番は
Pydanticをdict化 → dataclassへ詰め替えです。

def to_entity(req: UserRequest, new_id: int) -> UserEntity:
    data = req.model_dump()        # Pydantic v2: dict化
    return UserEntity(id=new_id, **data)

req = UserRequest(name="Alice", age=20)
entity = to_entity(req, new_id=100)

print(entity)  # UserEntity(id=100, name='Alice', age=20)
ポイント:
model_dump() は Pydantic v2 の dict化メソッドです。
(v1 では dict()

24-7. 変換②:dataclass → Pydantic(内部データをAPI出力へ)

DBや業務ロジックで作った dataclass を、レスポンス用 Pydantic に変換します。

from dataclasses import asdict

def to_response(entity: UserEntity) -> UserResponse:
    data = asdict(entity)            # dataclass → dict
    return UserResponse(**data)      # dict → Pydantic

entity = UserEntity(id=100, name="Alice", age=20)
res = to_response(entity)

print(res.model_dump())  # {'id': 100, 'name': 'Alice', 'age': 20}

24-8. 変換③:リスト(複数件)の変換例

entities = [
    UserEntity(id=1, name="Alice", age=20),
    UserEntity(id=2, name="Bob", age=None),
]

responses = [UserResponse(**asdict(e)) for e in entities]
print([r.model_dump() for r in responses])

24-9. 実務での落とし穴と対策

⚠ よくある落とし穴
  • DBの余計なカラム(password等)をそのまま返す
  • 日時型(datetime)を文字列化せずに返して混乱
  • None 許可の設計が曖昧(Optionalが付いていない)
✅ 対策
  • レスポンス用 Pydantic は「公開してよい項目」だけ定義する
  • 日時は Pydantic に任せる(ISO8601化される)
  • 未指定を許すなら int | None のように明示する

24-10. まとめ

25. Python(FastAPI)における DTO / Entity / Collection / Service / Repository の概念と Spring Boot との違い

結論:
Python(FastAPI)にも DTO / Entity / Service / Repository といった「設計概念」はあります。
ただし Spring Boot のように フレームワークが強制・自動配線(DI)・アノテーション中心ではなく、
Python は シンプルなクラス/関数+慣習で実現することが多い点が大きな違いです。

25-1. 各概念の役割(対応表)

概念 Spring Boot(典型) FastAPI / Python(典型) 主な目的
DTO Request/Response DTO(Jackson) Pydantic Model API境界の入出力を安全化
Entity JPA Entity(@Entity) ORM Model(SQLAlchemy等)or dataclass DBとドメインの対応
Collection List/Set/Map + Stream list/set/dict + 内包表記 複数データの保持と操作
Service @Service 通常クラス(Service層) 業務ロジックの中心
Repository JpaRepository(@Repository) Protocol/ABC + 実装クラス データアクセスの隠蔽・差し替え
DI Springが自動で注入 FastAPI Depends / 手動注入 依存関係の分離

25-2. DTO(FastAPIではPydanticが主役)

Spring Boot では DTO を自作クラスで作り、Jackson が JSON 変換します。
FastAPI では DTO に相当するものは Pydanticモデルです(型保証+検証+JSON変換が一体)。

from pydantic import BaseModel

class UserCreateRequest(BaseModel):
    """
    DTO(Request)相当:
    クライアントから来るJSONを受け取る型
    """
    name: str
    age: int | None = None

class UserResponse(BaseModel):
    """
    DTO(Response)相当:
    クライアントへ返すJSONの型
    """
    id: int
    name: str
    age: int | None = None
Spring Bootとの違い(DTO)
  • Spring:DTOはただのPOJO、検証は @Valid/@NotNull など別仕組み
  • FastAPI:DTO(Pydantic)が 検証・型変換・JSON化までまとめて担当
  • DTO定義 = API仕様書(OpenAPI)が自動生成される

25-3. Entity(DBモデル / ドメインモデル)

Spring Boot では JPA Entity が中心になりがちです。
Python では用途で分かれます:

from dataclasses import dataclass

@dataclass
class UserEntity:
    """
    Entity相当(内部モデル):
    DB由来でもよいし、業務ロジック用に整形した形でもよい。
    """
    id: int
    name: str
    age: int | None = None
Spring Bootとの違い(Entity)
  • Spring:Entity = DBテーブルと密結合(@Entity, @Column)になりやすい
  • Python:Entityを「DBの形」と「業務の形」で分けやすい(ORMとdataclassの併用)
  • 結果として「DB変更がAPI/業務ロジックに波及しにくい」設計が取りやすい

25-4. Repository(データアクセス層:Protocolで契約を作る)

Spring Boot は JpaRepository が強力で、DIで自動注入されます。
Python では Protocol(構造型)で「契約」を作り、実装を差し替えられるようにするのが実務的です。

from typing import Protocol

class UserRepository(Protocol):
    """
    Repository契約(interface相当):
    find/save など「データアクセスAPI」を定義する。
    """
    def find_by_id(self, user_id: int) -> UserEntity | None: ...
    def save(self, user: UserEntity) -> UserEntity: ...
class InMemoryUserRepository:
    """
    Repository実装例(テストや試作で便利):
    Protocolを継承しなくても「形が合えばOK」。
    """
    def __init__(self):
        self._store: dict[int, UserEntity] = {}

    def find_by_id(self, user_id: int) -> UserEntity | None:
        return self._store.get(user_id)

    def save(self, user: UserEntity) -> UserEntity:
        self._store[user.id] = user
        return user
Spring Bootとの違い(Repository)
  • Spring:Repositoryはフレームワークが生成・注入(@Repository/JpaRepository)
  • Python:Repositoryは自分でクラスを用意し、必要に応じてDepends等で注入
  • その代わり「魔法」が少なく、流れが追いやすい

25-5. Service(業務ロジック層)

Spring の @Service と同じく、Pythonでも Service は「業務ロジックの中心」です。
ただし Python はアノテーションではなく 普通のクラスとして作ります。

class UserService:
    """
    Service層:
    Controller(Router)から呼ばれ、Repositoryを使って処理する。
    """
    def __init__(self, repo: UserRepository):
        self.repo = repo

    def create_user(self, new_id: int, name: str, age: int | None) -> UserEntity:
        # 例:業務チェック
        if not name:
            raise ValueError("name is required")

        user = UserEntity(id=new_id, name=name, age=age)
        return self.repo.save(user)
Spring Bootとの違い(Service / DI)
  • Spring:@Service + @Autowired で自動DI
  • FastAPI:Depends で注入するか、手動で渡す(明示的)
  • 明示的なので「どこから依存が来たか」が追いやすい

25-6. Controller相当(FastAPIではRouter/Endpoint関数)

from fastapi import FastAPI

app = FastAPI()

# DIの代わりに “組み立て” する例(手動注入)
repo = InMemoryUserRepository()
service = UserService(repo)

@app.post("/users", response_model=UserResponse)
def create_user(req: UserCreateRequest):
    """
    Controller相当:
    Springの@RestControllerメソッドに近い役割。
    """
    entity = service.create_user(new_id=1, name=req.name, age=req.age)
    return UserResponse(id=entity.id, name=entity.name, age=entity.age)

25-7. Collection(複数件の扱い:list/set/dict + 内包表記)

Spring Boot では List/Set/Map に Stream を組み合わせることが多いです。
Python では list/set/dict に加えて 内包表記が強力です。

entities = [
    UserEntity(id=1, name="Alice", age=20),
    UserEntity(id=2, name="Bob", age=None),
]

# Collection変換(Entity -> Response DTO): 内包表記
responses = [UserResponse(id=e.id, name=e.name, age=e.age) for e in entities]
Spring Bootとの違い(Collection操作)
  • Java:Streamでmap/filter/collect
  • Python:内包表記(list/dict/set comprehension)で簡潔に書ける
  • 読みやすさ重視で、複雑なら for に戻すのがPython流

25-8. まとめ:Python(FastAPI)設計の特徴

26. Pydanticモデル

26-1. 概要

Pydanticモデルは、Pythonの型ヒントを使って
データの検証(バリデーション)・型変換・JSON変換を行うためのモデルです。
FastAPIでは DTO(Request / Response) として使われるのが基本です。

26-2. 目的

26-3. サンプルコード

from pydantic import BaseModel

class UserResponse(BaseModel):
    """
    APIレスポンス用DTO
    """
    id: int
    name: str
    age: int | None = None
# dict から生成
data = {"id": 1, "name": "Alice", "age": "20"}
user = UserResponse(**data)

print(user.age)          # 20(intに変換)
print(user.model_dump()) # dict化(Pydantic v2)

26-4. 詳細説明

27. SQLAlchemyモデル

27-1. 概要

SQLAlchemyモデルは、
Pythonクラスとデータベースのテーブルを対応付ける ORM(Object Relational Mapping) です。

27-2. 目的

27-3. サンプルコード

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String

class Base(DeclarativeBase):
    pass

class UserModel(Base):
    """
    users テーブルに対応する ORMモデル
    """
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String)
    age: Mapped[int | None] = mapped_column(Integer, nullable=True)
# DBから取得したレコードのイメージ
user = UserModel(id=1, name="Alice", age=20)
print(user.name)

27-4. 詳細説明

28. 内包表記(list / dict / set comprehension)

28-1. 概要

内包表記は、
コレクション(list / dict / set)を 簡潔に生成・変換するためのPython構文です。

28-2. 目的

28-3. サンプルコード

list内包表記

nums = [1, 2, 3, 4]

squares = [n * n for n in nums]
evens = [n for n in nums if n % 2 == 0]

dict内包表記

users = [
    {"id": 1, "name": "Alice"},
    {"id": 2, "name": "Bob"},
]

user_map = {u["id"]: u["name"] for u in users}

set内包表記

names = ["Alice", "Bob", "Alice"]

unique_names = {name for name in names}

28-4. 詳細説明

実務例:
[UserResponse(**asdict(e)) for e in entities]

29. FastAPI の DI(依存性注入 / Depends)

29-1. 概要

FastAPI の DI は Depends を使って、
エンドポイント関数(Controller相当)へ 必要なオブジェクトを自動で注入する仕組みです。
Spring Boot の @Autowired のように「コンテナが勝手に注入」するのではなく、
“この引数はDependsで解決する” と明示したものだけがDIされるのが大きな特徴です。

29-2. 目的

29-3. どの場合「DIされる」か(Depends がある場合)

FastAPI で DI されるのは、基本的に以下のケースです:

例:依存(DBセッション)をDIする

from fastapi import FastAPI, Depends
from typing import Generator

app = FastAPI()

class DbSession:
    def __init__(self):
        self.connected = True

    def close(self):
        self.connected = False

def get_db() -> Generator[DbSession, None, None]:
    """
    DIで注入される依存(DBセッション)。
    yield を使うと「最後に必ず後始末」できる。
    """
    db = DbSession()
    try:
        yield db
    finally:
        db.close()

@app.get("/users/{user_id}")
def get_user(user_id: int, db: DbSession = Depends(get_db)):
    """
    db は FastAPI が get_db() を呼んで注入する。
    """
    return {"user_id": user_id, "db_connected": db.connected}

例:Service をDIする(RepositoryもDIする)

from typing import Protocol

class UserRepository(Protocol):
    def find_name(self, user_id: int) -> str: ...

class InMemoryUserRepository:
    def find_name(self, user_id: int) -> str:
        return "Alice"

def get_repo() -> UserRepository:
    return InMemoryUserRepository()

class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo

    def get_name(self, user_id: int) -> str:
        return self.repo.find_name(user_id)

def get_service(repo: UserRepository = Depends(get_repo)) -> UserService:
    """
    Service生成にも Depends を使える(依存の連鎖)。
    """
    return UserService(repo)

@app.get("/names/{user_id}")
def get_name(user_id: int, service: UserService = Depends(get_service)):
    return {"name": service.get_name(user_id)}
DIされる(Dependsが効く)ポイント
  • Depends(get_db) と書いた引数は FastAPI が自動で作って渡す
  • 依存は「連鎖」できる(serviceの依存としてrepoを注入など)
  • yield を使う依存は with的に後始末できる(finallyが確実に動く)

29-4. どの場合「DIされない」か(Dependsが無い場合)

Spring Boot ではフィールドに @Autowired が付くなどで注入されますが、
FastAPI は Depends を書かない限り注入しません

例:Dependsが無いのでDIされない(よくある誤解)

from fastapi import FastAPI

app = FastAPI()

class Service:
    def hello(self) -> str:
        return "hello"

@app.get("/bad")
def bad(service: Service):
    """
    NG:
    service は Depends ではないので FastAPI は生成しない。
    → リクエストのパラメータとして解釈され、422エラーになりがち。
    """
    return {"msg": service.hello()}

上の例では FastAPI は service を「クエリパラメータ等の入力」とみなします。
つまり DIではなく入力扱いになり、リクエスト時にエラーになります。

例:手動で組み立てた場合(DIではなく手動注入)

service = Service()  # アプリ起動時に自分で作る(手動)

@app.get("/manual")
def manual():
    return {"msg": service.hello()}
これは「DI」ではなく、ただの「グローバル変数(手動注入)」です。
小規模ならOKですが、テスト差し替えやスコープ管理が難しくなります。

29-5. 「入力(Request data)」と「DI」の見分け方

FastAPI は引数を以下のどれかとして解釈します:

from fastapi import Header, Body, Depends

def get_token() -> str:
    return "server-generated-token"

@app.post("/mix")
def mix(
    user_id: int,                          # パス/クエリの入力
    x_token: str = Header(...),            # ヘッダ入力
    payload: dict = Body(...),             # ボディ入力
    token: str = Depends(get_token),       # ← DI(サーバ側が注入)
):
    return {"user_id": user_id, "x_token": x_token, "payload": payload, "token": token}

29-6. 詳しい説明(Spring Bootとの違い・スコープ感)

Spring Bootとの主な違い
  • Spring:DIコンテナがクラスを管理し、Beanを自動注入
  • FastAPI:Dependsで指定された関数を呼び出し、値を渡す(関数DI)
  • Spring:Singleton/Requestスコープなどが体系化
  • FastAPI:依存関数の書き方(yield等)で「リクエスト単位の後始末」を表現する

リクエストごとに作られる依存(典型:DBセッション)

def get_db():
    db = DbSession()
    try:
        yield db          # リクエスト中は同じdbを使う
    finally:
        db.close()        # リクエスト終了時に必ず閉じる

アプリ全体で1回だけ作る依存(手動 or キャッシュ)

# 例:設定読み込みなど(概念例)
settings = load_settings_once()

def get_settings():
    return settings
実務でDIを使うべき代表例
  • DBセッション / トランザクション
  • 認証(ユーザー取得、権限チェック)
  • Service / Repository の差し替え
  • 共通ログ・トレーシングIDの付与

29-7. まとめ

30. SQLite3 を例にしたDBアクセス(Connection / CRUD / Transaction / ORM)

30-1. 概要

Python では標準ライブラリの sqlite3 を使うことで、追加インストールなしで SQLite を扱えます。
配布が簡単(DBファイル1つ)なので、学習・小規模ツール・PCローカルアプリでよく使われます。

30-2. 目的(SQLiteを使う理由)

30-3. 接続(Connection)とテーブル作成

import sqlite3

DB_PATH = "app.db"

def init_db():
    # DBファイルに接続(無ければ作成される)
    conn = sqlite3.connect(DB_PATH)

    try:
        # SQL実行(テーブル作成)
        conn.execute("""
        CREATE TABLE IF NOT EXISTS users (
            id   INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            age  INTEGER
        )
        """)
        conn.commit()  # 変更を確定
    finally:
        conn.close()   # 必ず閉じる

init_db()
重要ポイント
  • sqlite3.connect() が Connection を返す
  • 更新系(INSERT/UPDATE/DELETE/DDL)は commit() が必要
  • 閉じ忘れ防止に with(後述)を使うのが定石

30-4. CRUD(SQLアクセス:生SQL)

30-4-1. Create(INSERT)

import sqlite3

def create_user(name: str, age: int | None) -> int:
    conn = sqlite3.connect(DB_PATH)
    try:
        cur = conn.cursor()
        # プレースホルダ "?" を必ず使う(SQLインジェクション対策)
        cur.execute("INSERT INTO users(name, age) VALUES(?, ?)", (name, age))
        conn.commit()
        return cur.lastrowid  # AUTOINCREMENTで発行されたID
    finally:
        conn.close()

new_id = create_user("Alice", 20)
print(new_id)

30-4-2. Read(SELECT)

import sqlite3

def find_user_by_id(user_id: int) -> dict | None:
    conn = sqlite3.connect(DB_PATH)
    try:
        # row_factory を使うと結果を辞書っぽく扱える
        conn.row_factory = sqlite3.Row
        cur = conn.cursor()

        cur.execute("SELECT id, name, age FROM users WHERE id = ?", (user_id,))
        row = cur.fetchone()
        if row is None:
            return None

        # sqlite3.Row -> dict
        return dict(row)
    finally:
        conn.close()

print(find_user_by_id(1))

30-4-3. Update(UPDATE)

import sqlite3

def update_user_age(user_id: int, age: int | None) -> int:
    conn = sqlite3.connect(DB_PATH)
    try:
        cur = conn.cursor()
        cur.execute("UPDATE users SET age = ? WHERE id = ?", (age, user_id))
        conn.commit()
        return cur.rowcount  # 更新された行数
    finally:
        conn.close()

print(update_user_age(1, 21))

30-4-4. Delete(DELETE)

import sqlite3

def delete_user(user_id: int) -> int:
    conn = sqlite3.connect(DB_PATH)
    try:
        cur = conn.cursor()
        cur.execute("DELETE FROM users WHERE id = ?", (user_id,))
        conn.commit()
        return cur.rowcount
    finally:
        conn.close()

print(delete_user(1))

30-5. トランザクション(commit / rollback)

トランザクションは「複数SQLをまとめて成功/失敗させる」仕組みです。
途中で例外が起きたら rollback() で取り消します。

import sqlite3

def transfer_example():
    conn = sqlite3.connect(DB_PATH)
    try:
        cur = conn.cursor()

        # ここから同一トランザクション(commitまで)
        cur.execute("INSERT INTO users(name, age) VALUES(?, ?)", ("Bob", 30))
        cur.execute("INSERT INTO users(name, age) VALUES(?, ?)", ("Carol", 25))

        # 例外が無ければ確定
        conn.commit()

    except Exception:
        # 途中で失敗したら全部取り消し
        conn.rollback()
        raise

    finally:
        conn.close()

with(コンテキストマネージャ)で安全に書く

import sqlite3

def create_two_users_with_with():
    # with sqlite3.connect は close を保証する
    with sqlite3.connect(DB_PATH) as conn:
        cur = conn.cursor()
        cur.execute("INSERT INTO users(name, age) VALUES(?, ?)", ("Dave", 40))
        cur.execute("INSERT INTO users(name, age) VALUES(?, ?)", ("Eve", None))
        # with を抜けるとき、例外がなければ commit、あれば rollback される(sqlite3の仕様)
トランザクション実務ルール
  • 更新系は「まとめて commit」する
  • 例外時は rollback する(with を使うと安全)
  • SQLは必ずプレースホルダ(?)を使う

30-6. ORM はあるのか?(ある:SQLAlchemyなど)

SQLite を ORM で扱うなら SQLAlchemy が定番です。
Spring Boot の JPA Entity に近い感覚で、テーブルをクラスとして扱えます。

注意:SQLAlchemy は標準ライブラリではありません(pipで追加が必要)。
学習用はまず sqlite3 で「SQLとトランザクション」を理解してから ORM に進むのが安全です。

30-7. ORM版サンプル(SQLAlchemy + SQLite)

30-7-1. モデル定義(テーブル = クラス)

# pip install sqlalchemy
from sqlalchemy import create_engine, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker

DB_URL = "sqlite:///app.db"

engine = create_engine(DB_URL, echo=False)  # echo=True でSQLログが見える
SessionLocal = sessionmaker(bind=engine)

class Base(DeclarativeBase):
    pass

class UserModel(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String, nullable=False)
    age: Mapped[int | None] = mapped_column(Integer, nullable=True)

Base.metadata.create_all(engine)  # テーブル作成

30-7-2. CRUD(ORM)

def orm_create_user(name: str, age: int | None) -> int:
    with SessionLocal() as session:
      user = UserModel(name=name, age=age)
      session.add(user)
      session.commit()        # INSERT確定
      session.refresh(user)   # DB採番IDを反映
      return user.id
def orm_find_user(user_id: int) -> UserModel | None:
    with SessionLocal() as session:
      return session.get(UserModel, user_id)
def orm_update_age(user_id: int, age: int | None) -> bool:
    with SessionLocal() as session:
      user = session.get(UserModel, user_id)
      if user is None:
        return False
      user.age = age
      session.commit()
      return True
def orm_delete_user(user_id: int) -> bool:
    with SessionLocal() as session:
      user = session.get(UserModel, user_id)
      if user is None:
        return False
      session.delete(user)
      session.commit()
      return True

30-7-3. トランザクション(ORM)

def orm_transaction_example():
    with SessionLocal() as session:
      try:
        session.add(UserModel(name="T1", age=10))
        session.add(UserModel(name="T2", age=20))
        session.commit()
      except Exception:
        session.rollback()
        raise

30-8. SQLアクセスとORM、どっちを使う?

31. 実務で多い「ORMを基本にしつつ、難しいクエリだけ生SQLを併用」パターン

31-1. 説明(なぜ併用するのか)

実務では、CRUD(単純な登録・更新・削除・単純検索)は ORM(SQLAlchemy)で書くことが多いです。
しかし、次のような「難しいクエリ」は ORM だけだと読みにくくなったり、実装が複雑になりがちです。

併用の基本方針(おすすめ)
  • 基本はORM:保守性・型・オブジェクト操作が楽
  • 難しい部分だけ生SQL:読みやすさ・性能・確実性を優先
  • 生SQLも プレースホルダ を使いSQLインジェクションを防ぐ
  • 返すのはORMモデルではなく DTO(Pydantic) に変換してAPIへ

31-2. サンプル構成(Repositoryで「ORM CRUD」と「Raw SQL集計」を両方持つ)

ここでは SQLite を例に、以下の方針で書きます:

31-3. サンプルコード(SQLAlchemy + Raw SQL 併用)

31-3-1. ORMモデル定義(テーブル = クラス)

# --- models.py ---
# ORMモデル定義(SQLAlchemy)
from sqlalchemy import Integer, String, ForeignKey, create_engine, text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker

DB_URL = "sqlite:///app.db"

# --- DBエンジン(SQLite) ---
engine = create_engine(DB_URL, echo=False)

# --- セッション生成(DB接続の単位) ---
SessionLocal = sessionmaker(bind=engine)

class Base(DeclarativeBase):
    pass

class UserModel(Base):
    """
    UserModel(ORM Entity相当)
    users テーブルに対応
    """
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String, nullable=False)

    orders: Mapped[list["OrderModel"]] = relationship(back_populates="user")

class OrderModel(Base):
    """
    OrderModel(ORM Entity相当)
    orders テーブルに対応
    """
    __tablename__ = "orders"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
    amount: Mapped[int] = mapped_column(Integer, nullable=False)

    user: Mapped[UserModel] = relationship(back_populates="orders")

# テーブル作成
Base.metadata.create_all(engine)

31-3-2. Pydantic DTO(APIレスポンス用)

# --- dto.py ---
from pydantic import BaseModel

class UserResponse(BaseModel):
    """
    UserResponse(API出力DTO)
    ORMモデルを直接返さないための型
    """
    id: int
    name: str

class UserOrderSummaryResponse(BaseModel):
    """
    集計結果用DTO(難しいクエリの戻り値)
    userごとの注文数と合計金額
    """
    user_id: int
    user_name: str
    order_count: int
    total_amount: int

31-3-3. Repository(ORM CRUD + Raw SQL集計)

# --- repository.py ---
# Repositoryは「DBアクセスの窓口」
# ここに CRUD(ORM)と 集計(Raw SQL)をまとめると、Service/Controllerがシンプルになる。

from sqlalchemy import text
from models import SessionLocal, UserModel, OrderModel
from dto import UserOrderSummaryResponse

class UserRepository:
    """
    UserRepository(Repository層)
    - 単純CRUDはORMで書く
    - 複雑/集計はRaw SQLを使う
    """

    # --- ORM: Create ---
    def create_user(self, name: str) -> UserModel:
        """ユーザー作成(ORMでINSERT)"""
        with SessionLocal() as session:
            user = UserModel(name=name)
            session.add(user)
            session.commit()
            session.refresh(user)
            return user

    # --- ORM: Create(注文作成) ---
    def create_order(self, user_id: int, amount: int) -> OrderModel:
        """注文作成(ORMでINSERT)"""
        with SessionLocal() as session:
            order = OrderModel(user_id=user_id, amount=amount)
            session.add(order)
            session.commit()
            session.refresh(order)
            return order

    # --- ORM: Read ---
    def find_user(self, user_id: int) -> UserModel | None:
        """ユーザー取得(ORMでSELECT)"""
        with SessionLocal() as session:
            return session.get(UserModel, user_id)

    # --- Raw SQL: 集計(難しいクエリはSQLで書く) ---
    def get_user_order_summary(self) -> list[UserOrderSummaryResponse]:
        """
        userごとの注文数(COUNT)と合計金額(SUM)を返す
        - JOIN + GROUP BY の集計は ORM より SQL の方が読みやすいことが多い
        """
        sql = text("""
        SELECT
            u.id   AS user_id,
            u.name AS user_name,
            COUNT(o.id) AS order_count,
            COALESCE(SUM(o.amount), 0) AS total_amount
        FROM users u
        LEFT JOIN orders o ON o.user_id = u.id
        GROUP BY u.id, u.name
        ORDER BY total_amount DESC
        """)

        with SessionLocal() as session:
            rows = session.execute(sql).mappings().all()
            # mappings() により row["user_id"] のように dict で扱える
            return [
                UserOrderSummaryResponse(
                    user_id=r["user_id"],
                    user_name=r["user_name"],
                    order_count=r["order_count"],
                    total_amount=r["total_amount"],
                )
                for r in rows
            ]

31-3-4. 呼び出し例(Service/Controller側のイメージ)

# --- usage_example.py ---
# ここでは FastAPI ではなく、利用イメージをシンプルに示す

from repository import UserRepository

repo = UserRepository()

# --- ORMでデータ作成 ---
u1 = repo.create_user("Alice")
u2 = repo.create_user("Bob")

repo.create_order(u1.id, 1000)
repo.create_order(u1.id, 500)
repo.create_order(u2.id, 200)

# --- 集計はRaw SQLで取得 ---
summary = repo.get_user_order_summary()
for s in summary:
    print(s.model_dump())

31-4. 詳しい説明(設計の意図)

なぜ Repository で併用するのか
  • Service/Controllerが「DB都合」を知らずに済む(責務分離)
  • CRUDはORMで書くと簡潔で保守しやすい
  • 集計SQLはSQLの方が意図が明確(レビューもしやすい)
  • 「難しい部分だけSQL」で、全体の可読性が上がる
実務ルール(おすすめ)
  • 生SQLはRepository内に閉じる(散らさない)
  • 生SQLでも text() とバインド変数を使う(インジェクション対策)
  • 返却は ORMモデルではなく DTO(Pydantic)にする(情報漏えい防止)
  • SQLが長い場合は別ファイル化(.sql)しても良い

31-5. まとめ

32. ファイルアクセス(小さいテキスト / バイナリ / 大きいテキスト)・アップロード/ダウンロード・テンポラリーファイル

32-1. 概要

Python のファイルI/Oは open()with が基本です。
小さいファイルは一括読み込み大きいファイルは逐次読み込み(ストリーム)が定石です。
Web(FastAPI)ではアップロード/ダウンロードが頻出なので、合わせて整理します。

32-2. 小さいテキストファイル(read / write)

設定ファイルや数KB〜数MB程度の小さいテキストなら、一括読み込みで問題ありません。
文字コードは日本語なら utf-8 を明示するのがおすすめです。

読み込み(全部読む)

from pathlib import Path

path = Path("config.txt")

# with を使うと、例外が起きても必ず close される
with path.open("r", encoding="utf-8") as f:
    text = f.read()

print(text)

書き込み(全部書く)

from pathlib import Path

path = Path("output.txt")
content = "Hello\nPython\n"

with path.open("w", encoding="utf-8", newline="\n") as f:
    f.write(content)
ポイント(テキスト)
  • encoding を明示(Windowsの既定文字コード問題を避ける)
  • newline="\n" を指定すると改行が安定
  • 小さいファイルは read() / write() でOK

32-3. バイナリファイル(画像・PDFなど)

画像やPDFなどは "rb"/"wb" で扱います。
テキストと違い、encoding は使いません

バイナリコピー(全体が分かる例)

from pathlib import Path

src = Path("image.jpg")
dst = Path("image_copy.jpg")

with src.open("rb") as fin, dst.open("wb") as fout:
    data = fin.read()      # 小さいバイナリなら一括読み込みでもOK
    fout.write(data)

大きめバイナリはチャンクコピー(安全)

from pathlib import Path

def copy_binary_chunked(src_path: str, dst_path: str, chunk_size: int = 1024 * 1024):
    src = Path(src_path)
    dst = Path(dst_path)

    with src.open("rb") as fin, dst.open("wb") as fout:
        while True:
            chunk = fin.read(chunk_size)
            if not chunk:
                break
            fout.write(chunk)

copy_binary_chunked("video.mp4", "video_copy.mp4")

32-4. 大きいテキストファイル(逐次読み込み)

ログ(数百MB〜GB)などは 一括読み込みしないのが鉄則です。
1行ずつ読む、またはチャンクで読む方法を使います。

1行ずつ処理する(最も標準)

from pathlib import Path

log_path = Path("big.log")

error_count = 0

with log_path.open("r", encoding="utf-8", errors="replace") as f:
    for line in f:
        if "ERROR" in line:
            error_count += 1

print("ERROR lines:", error_count)
errors="replace" を付けると、文字化け混入ログでも止まりにくくなります。

チャンク読み(巨大ファイル・高速化向け)

from pathlib import Path

def count_keyword_chunked(path: str, keyword: str, chunk_size: int = 1024 * 1024) -> int:
    p = Path(path)
    count = 0

    # 大きいテキストでもバイナリチャンクで読み、最後にデコードする設計もある
    with p.open("rb") as f:
        buffer = b""
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            buffer += chunk
            # 行単位にするため最後の改行までを処理し、残りを次へ
            lines = buffer.split(b"\n")
            buffer = lines.pop()  # 最後の中途半端行
            for bline in lines:
                line = bline.decode("utf-8", errors="replace")
                if keyword in line:
                    count += 1

        # 残り(最後の行)
        if buffer:
            line = buffer.decode("utf-8", errors="replace")
            if keyword in line:
                count += 1

    return count

print(count_keyword_chunked("big.log", "ERROR"))

32-5. FastAPIでのアップロード(UploadFile)

FastAPI ではアップロードは UploadFile を使うのが定石です。
大きいファイルでもメモリに載せずストリーム処理できます。

# --- app_upload.py ---
from fastapi import FastAPI, UploadFile, File
from pathlib import Path
import shutil

app = FastAPI()
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)

@app.post("/upload")
async def upload(file: UploadFile = File(...)):
    """
    アップロードされたファイルを uploads/ に保存する。
    """
    save_path = UPLOAD_DIR / file.filename

    # UploadFile.file は file-like object(バイナリストリーム)
    # shutil.copyfileobj でストリームコピー(メモリ節約)
    with save_path.open("wb") as f:
        shutil.copyfileobj(file.file, f)

    return {"filename": file.filename, "saved_to": str(save_path)}
アップロード実務ポイント
  • 保存先ディレクトリは事前に作る
  • ファイル名はそのまま信用しない(実務はサニタイズ/UUID化推奨)
  • 巨大ファイルは copyfileobj のようなストリームコピーを使う

32-6. FastAPIでのダウンロード(FileResponse / StreamingResponse)

ファイルをそのまま返す(FileResponse)

# --- app_download.py ---
from fastapi import FastAPI
from fastapi.responses import FileResponse
from pathlib import Path

app = FastAPI()
DOWNLOAD_DIR = Path("uploads")

@app.get("/download/{filename}")
def download(filename: str):
    """
    指定ファイルをダウンロードさせる(Content-Disposition付き)。
    """
    path = DOWNLOAD_DIR / filename
    return FileResponse(
        path=str(path),
        filename=filename,  # ダウンロード時のファイル名
        media_type="application/octet-stream",
    )

巨大ファイルをストリーミング(StreamingResponse)

from fastapi.responses import StreamingResponse

def iter_file(path: Path, chunk_size: int = 1024 * 1024):
    with path.open("rb") as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            yield chunk

@app.get("/download-stream/{filename}")
def download_stream(filename: str):
    path = DOWNLOAD_DIR / filename
    return StreamingResponse(
        iter_file(path),
        media_type="application/octet-stream",
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )
ダウンロード実務ポイント
  • 小〜中サイズは FileResponse が簡単
  • 巨大ファイルは StreamingResponse でチャンク送信
  • 実務はパス検証(ディレクトリトラバーサル対策)が必須

32-7. テンポラリーファイル(tempfile)作成と使い方

一時ファイルは標準ライブラリ tempfile が安全です。
OSが安全な場所に作り、名前衝突も避けられます。

一時ファイル(処理後に自動削除される)

import tempfile
from pathlib import Path

# delete=True(デフォルト)なら with を抜けると自動削除される
with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8") as tf:
    tf.write("temporary data\n")
    tf.seek(0)
    print(tf.read())
    print("temp path:", tf.name)  # 途中経過でパスが必要な場合に使える

一時ファイルを「残したい」場合(Windowsで便利)

import tempfile
from pathlib import Path

tf = tempfile.NamedTemporaryFile(mode="wb", delete=False)
try:
    tf.write(b"\x00\x01\x02")
    tf.close()
    path = Path(tf.name)
    print("created:", path)
    # ここで別処理に渡す(外部ツールに渡す等)
finally:
    # 実務では最後に削除する(例外時も)
    if Path(tf.name).exists():
        Path(tf.name).unlink()

一時ディレクトリ(複数ファイルの作業に便利)

import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as d:
    workdir = Path(d)
    (workdir / "a.txt").write_text("A", encoding="utf-8")
    (workdir / "b.txt").write_text("B", encoding="utf-8")

    # 作業ディレクトリ内で処理
    for p in workdir.iterdir():
        print(p.name, p.read_text(encoding="utf-8"))

# with を抜けたらディレクトリごと削除される
テンポラリの実務指針
  • 基本は TemporaryDirectory / NamedTemporaryFile を使う
  • Windowsは「開いたまま他プロセスが触れない」問題があるため、必要なら delete=False
  • 削除漏れ防止のため try/finally を徹底

32-8. まとめ

33. FastAPIで「www-form形式(フォーム送信)」のデータは受け取れる?(Collectionで受け取る方法も含む)

33-1. 結論

受け取れます。
FastAPI は JSON だけでなく、HTMLフォームが送る www-form形式(フォーム形式)も受け取れます。
さらに、配列(list)や辞書(dict)のようなCollectionっぽいデータも工夫すれば受け取れます。


33-2. そもそも「www-form形式」とは?(専門用語の解説)

ブラウザのHTMLフォーム(<form>)から送られるデータ形式の代表が2つあります:

用語:
Content-Type(コンテンツタイプ)とは、「このHTTPリクエストの中身は何形式か」を示すヘッダです。
application/json ならJSON、application/x-www-form-urlencoded ならフォーム形式、という意味です。

33-3. まず基本:フォームの単項目を受け取る(迷わない最短ルート)

FastAPI でフォームを受け取るには、引数に Form(...) を付けます。
これを書かないとJSON扱いになり、初心者がハマります。

# pip install "fastapi[standard]"
from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/login")
def login(
    username: str = Form(...),
    password: str = Form(...)
):
    """
    Form(...) を付けると www-form形式 を受け取れる
    """
    return {"username": username, "password_len": len(password)}
Form(...) の意味(専門用語の解説)
  • Form(...) は「この値はフォームから来る」と FastAPI に教える指定
  • ... は「必須(required)」という意味(省略不可)
  • 省略可能にしたい場合は Form(None) のように書く

省略可能(optional)の例

from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/profile")
def profile(
    name: str = Form(...),
    age: int | None = Form(None)  # 省略可能
):
    return {"name": name, "age": age}

33-4. Collection(list)として受け取れる? → 受け取れる(同じキーを複数回送る)

フォーム形式で配列(list)を送る定番方法は、
同じキーを複数回送ることです。

送信例(フォームのイメージ):

これを FastAPI 側で list[str] として受け取れます。

from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/tags")
def receive_tags(tags: list[str] = Form(...)):
    """
    tags がフォームで複数回送られると list として受け取れる
    """
    return {"count": len(tags), "tags": tags}
初心者向け補足:
JSON なら {"tags": ["python","fastapi"]} と送れますが、
www-form形式では「同じキーを複数回」が配列表現の基本です。

33-5. Collection(dict)として受け取れる? → 原則は工夫が必要

www-form形式は基本が 「キー = 値」の平坦(フラット)構造です。
そのため ネストしたdict(入れ子構造)をそのまま自然に送るのは苦手です。

ただし実務では次のどちらかで解決します:

  1. JSON文字列として1項目に入れて送る(おすすめ)
  2. キーを工夫して送る(例:user[name] など)

方法A:JSON文字列として送って、サーバでdict化(おすすめ)

送信例:

meta={"role":"admin","level":3}
from fastapi import FastAPI, Form, HTTPException
import json

app = FastAPI()

@app.post("/meta")
def receive_meta(meta: str = Form(...)):
    """
    meta はフォームでは文字列として届くので、json.loads() で dict に変換する
    """
    try:
        meta_dict = json.loads(meta)
        if not isinstance(meta_dict, dict):
            raise ValueError("meta must be JSON object")
        return {"meta": meta_dict}
    except Exception:
        raise HTTPException(status_code=400, detail="meta must be a JSON object string")
json.loads の意味(用語解説)
  • loads は「文字列(string)を JSON として解析する」関数
  • 解析に失敗すると例外が出るので try/except で囲む

方法B:キー名を工夫する(フロントと約束が必要)

例として、以下のように送る設計がありえます:

user_name=Alice
user_age=20

サーバ側でdictにまとめる:

from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/user-form")
def receive_user_form(
    user_name: str = Form(...),
    user_age: int = Form(...)
):
    user = {"name": user_name, "age": user_age}
    return {"user": user}

33-6. 「フォーム全部まとめて受け取りたい」場合(FormDataを読む)

「項目が多い」「何が来るか動的」などの場合は、リクエストからフォーム全体を読めます。
これは dictのように扱えるフォームデータです。

from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/form-all")
async def form_all(request: Request):
    """
    request.form() でフォーム全体を読む
    - 返り値は FormData(dictっぽい)
    - 同じキーが複数ある場合は getlist() が使える
    """
    form = await request.form()
    data = dict(form)  # 単純なキーは dict化できる

    # 複数値(list)があり得るキーは getlist
    tags = form.getlist("tags")  # tags=... が複数回来るケース

    return {"data": data, "tags": tags}
FormData / getlist の意味(用語解説)
  • FormData:フォームの中身を保持するオブジェクト(辞書っぽい)
  • getlist("tags"):同じキーが複数回送られた場合に、全部をlistで取り出す

33-7. まとめ(初心者が迷わない判断基準)

34. collection → DB取得 → レスポンス(ページング)までの一括フロー

34-1. 仕様(やりたいこと)

34-2. 注意点(初心者がハマる所)

注意点まとめ
  • ルート(router)は「HTTP入口」:DBやSQLを書かない
  • DTO(Pydantic)は「API境界」:入力/出力の型を固定する
  • Entityは「内部データ」:業務処理(name様付け)をここでやるかServiceでやる
  • Repositoryは「DBアクセス窓口」:SQL/ORMを閉じ込める
  • Interface(Protocol)は「差し替え契約」:Repoの偽物を簡単に作れる
  • DI(Depends)は「必要なものだけ注入」:Dependsが無いとDIされない
  • ページングは「LIMIT/OFFSET + total件数」:total取得SQLが別に必要

34-3. 標準的なフォルダ構成(例)

app/
  main.py                  # FastAPI起動点
  routers/
    users.py               # ルート設定(HTTP入口)
  dto/
    users_dto.py           # Request/Response DTO(Pydantic)
  entities/
    user_entity.py         # 内部モデル(dataclass)
  repositories/
    user_repository.py     # Repository Interface + 実装(DBアクセス)
  services/
    user_service.py        # Service(業務ロジック)
  db/
    sqlite.py              # SQLite接続ユーティリティ

34-4. サンプルコード一式(ページング:users一覧)

34-4-1. DB接続(SQLite): app/db/sqlite.py

# app/db/sqlite.py
import sqlite3
from typing import Generator

DB_PATH = "app.db"

def get_conn() -> Generator[sqlite3.Connection, None, None]:
    """
    【関数】DBコネクションを提供するDI用関数
    - yield で返す間だけ有効(リクエスト中に使用)
    - 終了時に必ず close される
    """
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row  # 【設定】SELECT結果を dict風に扱える
    try:
        yield conn
    finally:
        conn.close()

34-4-2. Entity(内部モデル): app/entities/user_entity.py

# app/entities/user_entity.py
from dataclasses import dataclass

@dataclass
class UserEntity:
    """
    【クラス】UserEntity(内部モデル / Entity相当)
    - DBから取った値を内部処理で扱いやすい形にする
    - APIのDTOとは分離する(情報漏えい防止・変更耐性)
    """
    id: int            # 【プロパティ】ユーザーID
    name: str          # 【プロパティ】ユーザー名(素の名前)
    email: str         # 【プロパティ】メール(例として)
    
    def display_name(self) -> str:
        """
        【メソッド】画面表示用の名前を返す
        - 仕様:name + '様'
        """
        return f"{self.name}様"

34-4-3. DTO(Pydantic): app/dto/users_dto.py

# app/dto/users_dto.py
from pydantic import BaseModel, Field

class UsersPageQuery(BaseModel):
    """
    【クラス】ページング用クエリDTO(Request DTO)
    - page/size をクエリとして受け取る想定
    """
    page: int = Field(1, ge=1, description="ページ番号(1始まり)")     # 【プロパティ】page
    size: int = Field(20, ge=1, le=200, description="1ページ件数")      # 【プロパティ】size

class UserRowResponse(BaseModel):
    """
    【クラス】ユーザー1行のレスポンスDTO(Response DTO)
    """
    id: int                 # 【プロパティ】ユーザーID
    name: str               # 【プロパティ】表示名(name様)
    email: str              # 【プロパティ】メール

class UsersPageResponse(BaseModel):
    """
    【クラス】ユーザー一覧(ページング)レスポンスDTO(Response DTO)
    - items: コレクション(list)
    - total: 総件数
    - page/size: ページ情報
    """
    items: list[UserRowResponse]  # 【プロパティ】ユーザー行のコレクション
    total: int                   # 【プロパティ】総件数
    page: int                    # 【プロパティ】ページ番号(1始まり)
    size: int                    # 【プロパティ】1ページ件数

34-4-4. Repository Interface + 実装(SQLite SQL): app/repositories/user_repository.py

# app/repositories/user_repository.py
from typing import Protocol
import sqlite3
from app.entities.user_entity import UserEntity

class IUserRepository(Protocol):
    """
    【インターフェース】UserRepositoryの契約(Protocol)
    - 実装差し替え(SQLite ↔ PostgreSQL / FakeRepo)が容易
    """
    def count_all(self) -> int:
        """【メソッド】総件数を返す"""
        ...

    def find_page(self, offset: int, limit: int) -> list[UserEntity]:
        """【メソッド】ページングでユーザー一覧を返す"""
        ...

class SqliteUserRepository:
    """
    【クラス】SQLite実装のRepository
    - DBアクセス(SQL)はこのクラスに閉じ込める
    """
    def __init__(self, conn: sqlite3.Connection):
        """【コンストラクタ】DIされたConnectionを保持する"""
        self.conn = conn  # 【プロパティ】SQLiteコネクション

    def count_all(self) -> int:
        """【メソッド】総件数取得(COUNT)"""
        cur = self.conn.cursor()
        cur.execute("SELECT COUNT(*) AS cnt FROM users")
        row = cur.fetchone()
        return int(row["cnt"])

    def find_page(self, offset: int, limit: int) -> list[UserEntity]:
        """
        【メソッド】ページング取得(LIMIT/OFFSET)
        - offset は 0始まり
        - limit は 1ページ件数
        """
        cur = self.conn.cursor()
        cur.execute(
            "SELECT id, name, email FROM users ORDER BY id LIMIT ? OFFSET ?",
            (limit, offset)
        )
        rows = cur.fetchall()

        # Row -> Entity(内部モデル)に変換して返す
        return [UserEntity(id=r["id"], name=r["name"], email=r["email"]) for r in rows]

34-4-5. Service(業務ロジック): app/services/user_service.py

# app/services/user_service.py
from app.repositories.user_repository import IUserRepository
from app.dto.users_dto import UsersPageResponse, UserRowResponse

class UserService:
    """
    【クラス】UserService(Service層)
    - ここで業務仕様(name様)を適用する
    - ルートから呼ばれ、Repositoryを利用する
    """
    def __init__(self, repo: IUserRepository):
        """【コンストラクタ】Repositoryを受け取り保持する"""
        self.repo = repo  # 【プロパティ】Repository(Interface)

    def get_users_page(self, page: int, size: int) -> UsersPageResponse:
        """
        【メソッド】ユーザー一覧(ページング)を返す
        - total 件数も返す(表のページングに必要)
        """
        # page(1始まり) -> offset(0始まり) に変換
        offset = (page - 1) * size

        total = self.repo.count_all()
        entities = self.repo.find_page(offset=offset, limit=size)

        # Entity -> DTO(Response)へ変換(表示仕様:name様)
        items = [
            UserRowResponse(
                id=e.id,
                name=e.display_name(),   # ← 仕様:name + '様'
                email=e.email,
            )
            for e in entities
        ]

        return UsersPageResponse(items=items, total=total, page=page, size=size)

34-4-6. DI組み立て関数: app/main.py(依存の生成)

# app/main.py
from fastapi import FastAPI, Depends
import sqlite3

from app.db.sqlite import get_conn
from app.repositories.user_repository import SqliteUserRepository, IUserRepository
from app.services.user_service import UserService
from app.routers.users import router as users_router

app = FastAPI()
app.include_router(users_router)

def get_user_repo(conn: sqlite3.Connection = Depends(get_conn)) -> IUserRepository:
    """
    【関数】RepositoryをDIするための依存
    - ConnectionをDependsで受け取り、Repository実装を返す
    """
    return SqliteUserRepository(conn)

def get_user_service(repo: IUserRepository = Depends(get_user_repo)) -> UserService:
    """
    【関数】ServiceをDIするための依存
    - RepoをDependsで受け取り、Serviceを返す
    """
    return UserService(repo)

34-4-7. ルート設定(Router/Endpoint): app/routers/users.py

# app/routers/users.py
from fastapi import APIRouter, Depends
from app.dto.users_dto import UsersPageResponse
from app.services.user_service import UserService
from app.main import get_user_service  # ※循環import回避は実務では工夫(後述)

router = APIRouter(prefix="/users", tags=["users"])

@router.get("", response_model=UsersPageResponse)
def list_users(
    page: int = 1,
    size: int = 20,
    service: UserService = Depends(get_user_service),
):
    """
    【ルート】GET /users?page=1&size=20
    - ルートはHTTP入口。DBやSQLを書かない。
    - Serviceに委譲するだけにする。
    """
    return service.get_users_page(page=page, size=size)
補足(循環importについて):
上の例は説明のために app.main から依存関数を import しています。
実務では dependencies.py のような専用ファイルにDI関数をまとめて、循環importを避けるのが定石です。

34-5. 全体の流れ(キーワード対応)

  1. ルート設定(Router):/users を定義し、page/size を受け取る
  2. DIDepends(get_user_service) で Service を注入
  3. Service:ページング計算(offset)+ Repository 呼び出し
  4. Repository:SQLでDB取得(count / page)
  5. Entity:DB行を内部モデルへ(UserEntity
  6. DTO:EntityをレスポンスDTOへ変換(name様の仕様を反映)
  7. コレクションitems: list[UserRowResponse] を返す

34. collection → DB取得 → レスポンス(ページング)までの一括フロー

34-1. 仕様(やりたいこと)

34-2. 注意点(初心者がハマる所)

注意点まとめ
  • ルート(router)は「HTTP入口」:DBやSQLを書かない
  • DTO(Pydantic)は「API境界」:入力/出力の型を固定する
  • Entityは「内部データ」:業務処理(name様付け)をここでやるかServiceでやる
  • Repositoryは「DBアクセス窓口」:SQL/ORMを閉じ込める
  • Interface(Protocol)は「差し替え契約」:Repoの偽物を簡単に作れる
  • DI(Depends)は「必要なものだけ注入」:Dependsが無いとDIされない
  • ページングは「LIMIT/OFFSET + total件数」:total取得SQLが別に必要

34-3. 標準的なフォルダ構成(例)

app/
  main.py                  # FastAPI起動点
  routers/
    users.py               # ルート設定(HTTP入口)
  dto/
    users_dto.py           # Request/Response DTO(Pydantic)
  entities/
    user_entity.py         # 内部モデル(dataclass)
  repositories/
    user_repository.py     # Repository Interface + 実装(DBアクセス)
  services/
    user_service.py        # Service(業務ロジック)
  db/
    sqlite.py              # SQLite接続ユーティリティ

34-4. サンプルコード一式(ページング:users一覧)

34-4-1. DB接続(SQLite): app/db/sqlite.py

# app/db/sqlite.py
import sqlite3
from typing import Generator

DB_PATH = "app.db"

def get_conn() -> Generator[sqlite3.Connection, None, None]:
    """
    【関数】DBコネクションを提供するDI用関数
    - yield で返す間だけ有効(リクエスト中に使用)
    - 終了時に必ず close される
    """
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row  # 【設定】SELECT結果を dict風に扱える
    try:
        yield conn
    finally:
        conn.close()

34-4-2. Entity(内部モデル): app/entities/user_entity.py

# app/entities/user_entity.py
from dataclasses import dataclass

@dataclass
class UserEntity:
    """
    【クラス】UserEntity(内部モデル / Entity相当)
    - DBから取った値を内部処理で扱いやすい形にする
    - APIのDTOとは分離する(情報漏えい防止・変更耐性)
    """
    id: int            # 【プロパティ】ユーザーID
    name: str          # 【プロパティ】ユーザー名(素の名前)
    email: str         # 【プロパティ】メール(例として)
    
    def display_name(self) -> str:
        """
        【メソッド】画面表示用の名前を返す
        - 仕様:name + '様'
        """
        return f"{self.name}様"

34-4-3. DTO(Pydantic): app/dto/users_dto.py

# app/dto/users_dto.py
from pydantic import BaseModel, Field

class UsersPageQuery(BaseModel):
    """
    【クラス】ページング用クエリDTO(Request DTO)
    - page/size をクエリとして受け取る想定
    """
    page: int = Field(1, ge=1, description="ページ番号(1始まり)")     # 【プロパティ】page
    size: int = Field(20, ge=1, le=200, description="1ページ件数")      # 【プロパティ】size

class UserRowResponse(BaseModel):
    """
    【クラス】ユーザー1行のレスポンスDTO(Response DTO)
    """
    id: int                 # 【プロパティ】ユーザーID
    name: str               # 【プロパティ】表示名(name様)
    email: str              # 【プロパティ】メール

class UsersPageResponse(BaseModel):
    """
    【クラス】ユーザー一覧(ページング)レスポンスDTO(Response DTO)
    - items: コレクション(list)
    - total: 総件数
    - page/size: ページ情報
    """
    items: list[UserRowResponse]  # 【プロパティ】ユーザー行のコレクション
    total: int                   # 【プロパティ】総件数
    page: int                    # 【プロパティ】ページ番号(1始まり)
    size: int                    # 【プロパティ】1ページ件数

34-4-4. Repository Interface + 実装(SQLite SQL): app/repositories/user_repository.py

# app/repositories/user_repository.py
from typing import Protocol
import sqlite3
from app.entities.user_entity import UserEntity

class IUserRepository(Protocol):
    """
    【インターフェース】UserRepositoryの契約(Protocol)
    - 実装差し替え(SQLite ↔ PostgreSQL / FakeRepo)が容易
    """
    def count_all(self) -> int:
        """【メソッド】総件数を返す"""
        ...

    def find_page(self, offset: int, limit: int) -> list[UserEntity]:
        """【メソッド】ページングでユーザー一覧を返す"""
        ...

class SqliteUserRepository:
    """
    【クラス】SQLite実装のRepository
    - DBアクセス(SQL)はこのクラスに閉じ込める
    """
    def __init__(self, conn: sqlite3.Connection):
        """【コンストラクタ】DIされたConnectionを保持する"""
        self.conn = conn  # 【プロパティ】SQLiteコネクション

    def count_all(self) -> int:
        """【メソッド】総件数取得(COUNT)"""
        cur = self.conn.cursor()
        cur.execute("SELECT COUNT(*) AS cnt FROM users")
        row = cur.fetchone()
        return int(row["cnt"])

    def find_page(self, offset: int, limit: int) -> list[UserEntity]:
        """
        【メソッド】ページング取得(LIMIT/OFFSET)
        - offset は 0始まり
        - limit は 1ページ件数
        """
        cur = self.conn.cursor()
        cur.execute(
            "SELECT id, name, email FROM users ORDER BY id LIMIT ? OFFSET ?",
            (limit, offset)
        )
        rows = cur.fetchall()

        # Row -> Entity(内部モデル)に変換して返す
        return [UserEntity(id=r["id"], name=r["name"], email=r["email"]) for r in rows]

34-4-5. Service(業務ロジック): app/services/user_service.py

# app/services/user_service.py
from app.repositories.user_repository import IUserRepository
from app.dto.users_dto import UsersPageResponse, UserRowResponse

class UserService:
    """
    【クラス】UserService(Service層)
    - ここで業務仕様(name様)を適用する
    - ルートから呼ばれ、Repositoryを利用する
    """
    def __init__(self, repo: IUserRepository):
        """【コンストラクタ】Repositoryを受け取り保持する"""
        self.repo = repo  # 【プロパティ】Repository(Interface)

    def get_users_page(self, page: int, size: int) -> UsersPageResponse:
        """
        【メソッド】ユーザー一覧(ページング)を返す
        - total 件数も返す(表のページングに必要)
        """
        # page(1始まり) -> offset(0始まり) に変換
        offset = (page - 1) * size

        total = self.repo.count_all()
        entities = self.repo.find_page(offset=offset, limit=size)

        # Entity -> DTO(Response)へ変換(表示仕様:name様)
        items = [
            UserRowResponse(
                id=e.id,
                name=e.display_name(),   # ← 仕様:name + '様'
                email=e.email,
            )
            for e in entities
        ]

        return UsersPageResponse(items=items, total=total, page=page, size=size)

34-4-6. DI組み立て関数: app/main.py(依存の生成)

# app/main.py
from fastapi import FastAPI, Depends
import sqlite3

from app.db.sqlite import get_conn
from app.repositories.user_repository import SqliteUserRepository, IUserRepository
from app.services.user_service import UserService
from app.routers.users import router as users_router

app = FastAPI()
app.include_router(users_router)

def get_user_repo(conn: sqlite3.Connection = Depends(get_conn)) -> IUserRepository:
    """
    【関数】RepositoryをDIするための依存
    - ConnectionをDependsで受け取り、Repository実装を返す
    """
    return SqliteUserRepository(conn)

def get_user_service(repo: IUserRepository = Depends(get_user_repo)) -> UserService:
    """
    【関数】ServiceをDIするための依存
    - RepoをDependsで受け取り、Serviceを返す
    """
    return UserService(repo)

34-4-7. ルート設定(Router/Endpoint): app/routers/users.py

# app/routers/users.py
from fastapi import APIRouter, Depends
from app.dto.users_dto import UsersPageResponse
from app.services.user_service import UserService
from app.main import get_user_service  # ※循環import回避は実務では工夫(後述)

router = APIRouter(prefix="/users", tags=["users"])

@router.get("", response_model=UsersPageResponse)
def list_users(
    page: int = 1,
    size: int = 20,
    service: UserService = Depends(get_user_service),
):
    """
    【ルート】GET /users?page=1&size=20
    - ルートはHTTP入口。DBやSQLを書かない。
    - Serviceに委譲するだけにする。
    """
    return service.get_users_page(page=page, size=size)
補足(循環importについて):
上の例は説明のために app.main から依存関数を import しています。
実務では dependencies.py のような専用ファイルにDI関数をまとめて、循環importを避けるのが定石です。

34-5. 全体の流れ(キーワード対応)

  1. ルート設定(Router):/users を定義し、page/size を受け取る
  2. DIDepends(get_user_service) で Service を注入
  3. Service:ページング計算(offset)+ Repository 呼び出し
  4. Repository:SQLでDB取得(count / page)
  5. Entity:DB行を内部モデルへ(UserEntity
  6. DTO:EntityをレスポンスDTOへ変換(name様の仕様を反映)
  7. コレクションitems: list[UserRowResponse] を返す

呼び出しの流れまとめ(どこから、何が呼ばれ、どこで「様」が付くか)

  1. ① クライアント(ブラウザ/フロント)
    一覧表を表示したいので API を呼ぶ
    GET /users?page=1&size=20
  2. ② Router(URLと関数の紐付け)
    FastAPIがURLに一致するエンドポイント関数を呼ぶ
    さらに Depends(get_user_service) によりDIが開始される
    # app/routers/users_routes.py
    @router.get("", response_model=UsersPageResponse)
    def list_users(page: int = 1, size: int = 20,
                   service: UserService = Depends(get_user_service)):
        return controller.list_users(page=page, size=size, service=service)
    • この時点でDIされるもの:
      get_user_service() が呼ばれる
        └ get_user_repo() が呼ばれる
            └ get_conn() が呼ばれる(DBコネクション生成)
  3. ③ dependencies(DIで必要な部品を組み立てる)
    DBコネクション → Repository → Service の順で生成される
    # app/dependencies.py
    def get_user_repo(conn = Depends(get_conn)):
        return SqliteUserRepository(conn)
    
    def get_user_service(repo = Depends(get_user_repo)):
        return UserService(repo)
  4. ④ Controller(HTTP入口の処理)
    Serviceを呼ぶ。業務例外/DB例外をHTTPエラーへ変換する
    # app/controllers/users_controller.py
    def list_users(self, page, size, service):
        try:
            return service.get_users_page(page=page, size=size)
        except PaginationError as e:
            raise HTTPException(status_code=400, detail=str(e))
        except DataAccessError:
            raise HTTPException(status_code=503, detail="DB is unavailable")
    ここではまだ「様」は付かない(Controllerは変換しない)
  5. ⑤ Service(業務ロジック)
    ページング計算 → Repositoryから取得 → Entity→DTO変換 → 返却
    # app/services/user_service.py
    total = self.repo.count_all()
    entities = self.repo.find_page(offset=(page-1)*size, limit=size)
    
    items = [
      UserRowResponse(
        id=e.id,
        name=e.display_name(),  # ★ここで「様」付きの表示名を使う
        email=e.email
      )
      for e in entities
    ]
    「name + '様'」は Service が DTO に詰める直前に決まる
    実際の「様付け処理」は Entity の display_name() が行う
  6. ⑥ Repository(DBアクセス)
    SQLでDBから行を取り、Entityを作って返す
    # app/repositories/user_repository.py
    rows = cur.fetchall()
    return [UserEntity(id=r["id"], name=r["name"], email=r["email"]) for r in rows]
    Repositoryは加工しない(nameは「Alice」のままEntityへ)
  7. ⑦ Entity(内部モデル)
    表示仕様のメソッドを持つ(ここで「様」を付ける)
    # app/entities/user_entity.py
    def display_name(self) -> str:
        return f"{self.name}様"
    「様付け」はここ(display_nameメソッド)
  8. ⑧ FastAPIがDTOをJSONとして返す(レスポンス)
    結果は UsersPageResponse として返却される
    {
      "items": [
        {"id": 1, "name": "Alice様", "email": "alice@example.com"},
        {"id": 2, "name": "Bob様",   "email": "bob@example.com"}
      ],
      "total": 123,
      "page": 1,
      "size": 20
    }

FastAPIでメッセージ多言語化(i18n)する標準的な方法

1. まず結論(FastAPIに「標準内蔵のi18n」はある?)

FastAPI 自体には Laravel のような「標準で用意された言語ファイル機構」は ありません
その代わり実務では、次のどれかが「標準的なやり方」として使われます。

  1. Accept-Language(HTTPヘッダ)で言語を決める
  2. Middleware で「今回のリクエストの言語」を確定して保持する
  3. 翻訳辞書(JSON/YAML) または gettext(.po/.mo) を使う
  4. エラーは 独自のエラーコード(例:USER_NOT_FOUND)を返し、文言はフロントで翻訳する(APIでは特に多い)
用語(初心者向け)
  • i18n:internationalization(多言語対応)の略
  • Accept-Language:ブラウザ/アプリが「希望言語」を送るHTTPヘッダ
  • Middleware:ルート処理の前後に割り込んで共通処理を行う仕組み
  • gettext:昔からある標準的な翻訳方式(poファイルなど)

2. 実務で一番おすすめ:APIは「エラーコード+必要ならメッセージ」

APIはクライアント(Web/Android/iOS)が複数あることが多いため、
文言をAPI側で確定しすぎるより、エラーコードを返す方が保守しやすいです。

# 例:APIレスポンス(言語に依存しない)
{
  "error_code": "USER_NOT_FOUND",
  "message": "User not found"  // optional(開発用に付ける場合も)
}

ただし「管理画面」などAPI利用者が固定で、API側でメッセージを返したい場合は 次の方法が定石です。


3. 定石パターン:Accept-Languageで言語を決めて、翻訳辞書で返す

3-1. 例:簡易辞書(Pythonのdict)で翻訳する(最小構成)

小規模なら、まずはこれで十分です。後でgettext等へ移行もできます。

# app/i18n/messages.py
MESSAGES = {
    "ja": {
        "USER_NOT_FOUND": "ユーザーが見つかりません",
        "WELCOME": "{name}様、ようこそ",
    },
    "en": {
        "USER_NOT_FOUND": "User not found",
        "WELCOME": "Welcome, {name}",
    },
}

def t(lang: str, key: str, **params) -> str:
    """
    【関数】翻訳関数
    - lang: 'ja' / 'en' など
    - key : メッセージキー
    - params: {name} のような埋め込み用
    """
    table = MESSAGES.get(lang) or MESSAGES["en"]       # fallback
    template = table.get(key) or MESSAGES["en"].get(key) or key
    return template.format(**params)

3-2. Middlewareで言語を確定し、request.stateに保存する

request.state は「このリクエスト処理中だけ使える入れ物」です。
Controller/Service からも参照できるようにするのが目的です。

# app/middlewares/locale.py
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware

def pick_lang(accept_language: str | None) -> str:
    """
    【関数】Accept-Languageから言語を決める(簡易版)
    - 'ja,en-US;q=0.9,en;q=0.8' みたいな値が来る
    - ここでは「jaが含まれてたらja、それ以外はen」にする
    """
    if not accept_language:
        return "en"
    lower = accept_language.lower()
    if "ja" in lower:
        return "ja"
    return "en"

class LocaleMiddleware(BaseHTTPMiddleware):
    """
    【クラス】言語決定ミドルウェア
    - ルートに入る前に lang を決めて request.state.lang に入れる
    """
    async def dispatch(self, request: Request, call_next):
        accept = request.headers.get("accept-language")
        request.state.lang = pick_lang(accept)
        response = await call_next(request)
        return response

3-3. FastAPIにミドルウェアを登録する

# app/main.py
from fastapi import FastAPI
from app.middlewares.locale import LocaleMiddleware

app = FastAPI()
app.add_middleware(LocaleMiddleware)

3-4. Controllerで翻訳して返す例(業務例外→翻訳→HTTPレスポンス)

# app/controllers/users_controller.py
from fastapi import Request, HTTPException
from app.i18n.messages import t

class UsersController:
    """
    【クラス】UsersController
    - 例外をHTTPに変換するときに、langに合わせたmessageを返す
    """

    def raise_not_found(self, request: Request):
        """
        【メソッド】ユーザー未存在エラーを多言語で返す例
        """
        lang = getattr(request.state, "lang", "en")
        msg = t(lang, "USER_NOT_FOUND")
        raise HTTPException(status_code=404, detail=msg)

    def welcome(self, request: Request, name: str) -> dict:
        """
        【メソッド】表示メッセージを多言語で返す例
        """
        lang = getattr(request.state, "lang", "en")
        msg = t(lang, "WELCOME", name=name)
        return {"message": msg}
ポイント:
「翻訳はControllerでやる」のが分かりやすいです。
Serviceまで翻訳を持ち込むと、HTTP(Accept-Language)に業務層が引っ張られます。

4. 本格運用(gettext / Babel)を使う場合

文言が増える・翻訳者が関わる・差分管理が必要、という場合は gettext(.po/.mo) が定番です。
ただし導入が少し重いので、最初は「辞書方式」で十分 → 必要になったら移行、が現実的です。

gettextの特徴
  • 翻訳ファイルを専門ツールで編集できる(翻訳者と分業しやすい)
  • キー管理がしっかりできる
  • 導入と運用は辞書方式より重い

5. 実務のおすすめ指針(迷わない結論)

Python / FastAPI のログ出力:デバッグ用(コンソール)と本番用(ログファイル)の使い分け

1. まず結論(使い分けの基本)

  • デバッグ用(開発中):コンソール(ターミナル)に出す。最短で状況を確認する。
  • 本番用(運用中):ログファイル(またはログ基盤)に残す。障害調査・監査・追跡のため。
  • print は原則禁止:本番で消える/混ざる/構造化できない/レベル管理できない。
  • Pythonでは 標準logging を使うのが標準的。

2. ログレベル(重要:何をどこまで出すか)

レベル 用途
DEBUG 開発・詳細調査 SQLや入力値の詳細、分岐の通過ログ
INFO 通常運用の記録 API起動、処理成功、件数など
WARNING 軽微な問題 リトライ、非推奨、入力の怪しさ
ERROR 失敗(復旧可能) DB接続失敗、外部API失敗など
CRITICAL 致命的(即対応) 起動不能、データ破壊の恐れ

3. 開発用:コンソールに出す(最小構成)

開発時は コンソール出力(StreamHandler) が基本です。
VS Code のターミナル / デバッグコンソールで見えます。

# app/logging_config.py
import logging

def setup_logging_dev() -> None:
    """
    【関数】開発用ロギング設定
    - コンソールに DEBUG 以上を出す
    - まずはこれでOK
    """
    logging.basicConfig(
        level=logging.DEBUG,  # 【設定】DEBUG以上を表示
        format="%(asctime)s %(levelname)s %(name)s - %(message)s",
    )
# app/main.py
import logging
from fastapi import FastAPI
from app.logging_config import setup_logging_dev

setup_logging_dev()

logger = logging.getLogger("app")  # 【変数】アプリ用logger
app = FastAPI()

@app.get("/ping")
def ping():
    """
    【ルート】動作確認
    """
    logger.debug("debug: ping called")   # 【ログ】デバッグ
    logger.info("info: ping ok")         # 【ログ】通常情報
    return {"ok": True}
開発中は DEBUG を出して良い(ただし個人情報は出さない)。

4. 本番用:ログファイルに残す(ローテーション付き)

本番はログが増え続けるため、ローテーション(一定サイズや日付で分割)が必須です。

# app/logging_config.py
import logging
from logging.handlers import RotatingFileHandler
import os

def setup_logging_prod(log_dir: str = "logs") -> None:
    """
    【関数】本番用ロギング設定
    - ログファイルに INFO 以上を出す
    - ローテーションあり(サイズで分割)
    """
    os.makedirs(log_dir, exist_ok=True)  # 【処理】ログフォルダ作成
    log_path = os.path.join(log_dir, "app.log")

    handler = RotatingFileHandler(
        filename=log_path,         # 【設定】ログファイル
        maxBytes=5 * 1024 * 1024,  # 【設定】5MBで分割
        backupCount=10,            # 【設定】古いログを10世代保持
        encoding="utf-8",          # 【設定】文字化け防止
    )

    formatter = logging.Formatter(
        "%(asctime)s %(levelname)s %(name)s - %(message)s"
    )
    handler.setFormatter(formatter)

    root = logging.getLogger()     # 【取得】root logger
    root.setLevel(logging.INFO)    # 【設定】INFO以上
    root.handlers.clear()          # 【注意】二重
::contentReference[oaicite:0]{index=0}
# app/main.py  
import logging
from fastapi import FastAPI
from app.logging_config import setup_logging_prod
setup_logging_prod()  # 【呼び出し】本番用ロギング設定
logger = logging.getLogger("app")  # 【変数】アプリ用logger
app = FastAPI()
@app.get("/ping")
def ping():
    """
    【ルート】動作確認
    """
    logger.debug("debug: ping called")   # 【ログ】デバッグ(本番では出ない)
    logger.info("info: ping ok")         # 【ログ】通常情報
    return {"ok": True}
本番では INFO 以上を出す(DEBUGは出さない)。
ただし障害調査のために WARNING 以上は必ず残す のがポイントです。

Python + FastAPI で「Windowsのiniファイル」的な設定はどこに置き、いつ、どう読み込む?(サンプル付き)

1. まず結論(実務での標準パターン)

  • 置き場所:プロジェクト直下の config/settings/ に置くのが定番
  • 読み込むタイミング
    • 小規模:アプリ起動時に1回だけ(import時 or startupイベント)
    • 大規模:DI(Depends)で必要な場所に注入(ただし読み込み自体はキャッシュ)
  • 読み込み方法:Python標準の configparser を使うのが王道
  • 注意:Webアプリは「リクエスト毎にiniを読む」のは遅いので、基本は1回読み→メモリに保持

2. 推奨フォルダ構成(iniを置く場所)

app/
  main.py
  dependencies.py
  ...
config/
  app.ini          # アプリ設定(DB接続、外部API、機能フラグなど)
  app.dev.ini      # 開発用(任意)
  app.prod.ini     # 本番用(任意)
Gitに入れるもの:テンプレ(例:app.ini.example)
Gitに入れないもの:本番の秘密情報入りini(パスワード等)

3. iniファイル例(config/app.ini)

[app]
env = dev
debug = true

[db]
driver = sqlite
path = app.db

[external_api]
base_url = https://example.com
timeout_seconds = 10

4. 読み込みコード(標準:configparser)

4-1. 設定読み込みモジュールを作る(1回読み+キャッシュ)

# app/config_loader.py
from __future__ import annotations
import os
from pathlib import Path
import configparser
from functools import lru_cache

@lru_cache(maxsize=1)
def load_config() -> configparser.ConfigParser:
    """
    【関数】ini設定を読み込んで返す(キャッシュされる)
    - maxsize=1 なので最初の1回だけファイルを読む
    - 以降はメモリ上のConfigParserが返る(高速)
    """
    # 【環境変数】iniのパスを切り替えられるようにする(本番で便利)
    ini_path = os.getenv("APP_INI_PATH", "config/app.ini")

    path = Path(ini_path)
    if not path.exists():
        raise FileNotFoundError(f"ini file not found: {path}")

    config = configparser.ConfigParser()
    config.read(path, encoding="utf-8")  # 【処理】iniを読む(UTF-8推奨)
    return config
なぜ @lru_cache を使う?
  • Webはリクエストが多いので、毎回ファイルを読むと遅い
  • 起動後は設定は基本変わらない(変えるなら再起動)
  • DIで何度呼ばれても、読み込みは1回だけになる

5. 「いつ読む?」の標準的な2パターン

パターンA:import時に読む(最も簡単)

小規模アプリならこれで十分です。
ただし import 時に例外が出ると起動できないので、運用では好みが分かれます。

# app/settings.py
from app.config_loader import load_config

CONFIG = load_config()  # 【処理】起動時(import時)に1回読む

APP_ENV = CONFIG.get("app", "env", fallback="dev")
DB_PATH = CONFIG.get("db", "path", fallback="app.db")

パターンB:startupイベントで読む(FastAPIらしい)

FastAPI起動時に確実に読ませたいならこちら。
startupで読み込みが失敗したらアプリ起動を止められます。

# app/main.py
import logging
from fastapi import FastAPI
from app.config_loader import load_config

logger = logging.getLogger("app")
app = FastAPI()

@app.on_event("startup")
def on_startup():
    """
    【イベント】アプリ起動時に1回呼ばれる
    - ここでiniが読めることを確認する
    """
    cfg = load_config()
    env = cfg.get("app", "env", fallback="dev")
    logger.info("config loaded. env=%s", env)
どちらでも load_config() はキャッシュされるので、
複数回呼んでもファイル読み込みは1回です。

6. FastAPIで「必要な場所に設定を渡す」(DIで注入)

SpringのDIに近い形で、Controller/Serviceに設定を渡せます。

# app/dependencies.py
import configparser
from fastapi import Depends
from app.config_loader import load_config

def get_config() -> configparser.ConfigParser:
    """
    【DI関数】設定を返す
    - load_config() はキャッシュされるので高速
    """
    return load_config()
# app/routers/ping.py
from fastapi import APIRouter, Depends
import configparser
from app.dependencies import get_config

router = APIRouter()

@router.get("/ping")
def ping(cfg: configparser.ConfigParser = Depends(get_config)):
    """
    【ルート】ini設定を参照する例
    """
    env = cfg.get("app", "env", fallback="dev")
    timeout = cfg.getint("external_api", "timeout_seconds", fallback=10)
    return {"ok": True, "env": env, "timeout_seconds": timeout}

7. 本番での切り替え(iniファイルのパスを変える)

本番では ini の場所が変わることが多いので、
環境変数でiniパスを渡すのが定石です。

# Windows(PowerShell例)
$env:APP_INI_PATH="C:\app\config\app.prod.ini"
uvicorn app.main:app --host 0.0.0.0 --port 8000
よくある運用
  • コードは同じ
  • iniだけ環境ごとに差し替える
  • 設定変更は「ini変更→再起動」で反映(Webではこれが普通)

8. さらに実務っぽい注意(iniを使う場合)

秘密情報は ini ではなく環境変数へ:設定方法(Windows/Linux)・読み込み方・いつ読むか

1. なぜ秘密情報は環境変数に寄せるのか(実務理由)

  • Gitに載せない:iniに書くと誤ってコミットしやすい
  • 環境ごとに差し替えが簡単:開発/本番で値だけ変える
  • 運用が定石:Docker / CI / 本番サーバでも環境変数注入が一般的
  • 漏えい時の影響を減らせる:設定ファイル配布物に秘密が混ざらない

2. Windowsでの環境変数の設定方法

2-1. PowerShell(このターミナルだけ有効:一時)

# DBパスワード / APIキー例
$env:DB_PASSWORD = "secret_password"
$env:API_KEY     = "secret_api_key"

# 起動
uvicorn app.main:app --reload
特徴:このPowerShellを閉じると消える(開発で便利)

2-2. CMD(このウィンドウだけ有効:一時)

set DB_PASSWORD=secret_password
set API_KEY=secret_api_key

uvicorn app.main:app --reload

2-3. Windowsの「システム環境変数」に登録(永続)

  1. スタート → 「環境変数」と検索 → 「システム環境変数を編集」
  2. 「環境変数(N)...」
  3. 「ユーザー環境変数」または「システム環境変数」で追加
  4. 追加後、新しく開いたターミナルから有効
VS Codeのターミナルは、登録後に開き直してください(開きっぱなしは反映されない)。

3. Linuxでの環境変数の設定方法

3-1. シェルで一時設定(このセッションだけ)

export DB_PASSWORD="secret_password"
export API_KEY="secret_api_key"

uvicorn app.main:app --host 0.0.0.0 --port 8000

3-2. .bashrc / .zshrc に書く(ユーザー永続)

# ~/.bashrc 例
export DB_PASSWORD="secret_password"
export API_KEY="secret_api_key"
# 反映(例)
source ~/.bashrc

3-3. systemdサービスで設定(本番の定石)

本番でFastAPIをsystemdで常駐させる場合は、Unitファイルに環境変数を渡します。

# /etc/systemd/system/myapp.service(例)
[Service]
Environment="DB_PASSWORD=secret_password"
Environment="API_KEY=secret_api_key"
ExecStart=/usr/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
さらに定石としては EnvironmentFile(別ファイルに切り出し)を使います。
# /etc/systemd/system/myapp.service(例:EnvironmentFile)
[Service]
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/usr/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
# /etc/myapp/myapp.env(例)
DB_PASSWORD=secret_password
API_KEY=secret_api_key

4. Pythonでの読み込み方(os.getenv が基本)

Pythonでは標準で os.getenv()os.environ を使います。

# app/secrets.py
import os

def get_db_password() -> str:
    """
    【関数】DBパスワードを環境変数から取得する
    - 無い場合は起動できないので例外にする(秘密はデフォルト値を持たない)
    """
    value = os.getenv("DB_PASSWORD")
    if not value:
        raise RuntimeError("DB_PASSWORD is not set")
    return value

def get_api_key() -> str:
    """
    【関数】APIキーを環境変数から取得する
    """
    value = os.getenv("API_KEY")
    if not value:
        raise RuntimeError("API_KEY is not set")
    return value
重要:秘密情報に fallback(デフォルト値)を付けない
  • 付けると「設定漏れ」に気づかず本番で事故る
  • なので 無ければ例外で落とすのが定石

5. いつ読むのが正しいか(タイミングの標準)

5-1. 起動時に1回読む(おすすめ)

起動時に「必須の秘密情報が揃っているか」検証して、無ければ起動を止めるのが安全です。
Webアプリは「途中で設定が変わる」運用は基本しないため、起動時読みが定石です。

# app/main.py
import logging
from fastapi import FastAPI
from app.secrets import get_db_password, get_api_key

logger = logging.getLogger("app")
app = FastAPI()

@app.on_event("startup")
def on_startup():
    """
    【イベント】アプリ起動時に1回呼ばれる
    - 必須の秘密情報が揃っているか検証する
    """
    _db_pass = get_db_password()
    _api_key = get_api_key()
    logger.info("secrets loaded (lengths only): db_password=%s api_key=%s",
                len(_db_pass), len(_api_key))
注意:ログに秘密情報そのものを出さない(長さや有無だけ出す)

5-2. 必要になった時に読む(DIで注入する方式)

「必要な箇所でだけ使いたい」「テストで差し替えたい」場合はDIにします。
ただし実務では、内部でキャッシュして「毎回環境変数を読む」ようにはしません。

# app/dependencies.py
from fastapi import Depends
from functools import lru_cache
from app.secrets import get_api_key

@lru_cache(maxsize=1)
def load_api_key_once() -> str:
    """
    【関数】APIキーを1回だけ読み込んでキャッシュする
    """
    return get_api_key()

def get_api_key_dep() -> str:
    """
    【DI関数】APIキーを注入する
    - 実体はキャッシュされているので高速
    """
    return load_api_key_once()
# app/routers/external.py
from fastapi import APIRouter, Depends
from app.dependencies import get_api_key_dep

router = APIRouter()

@router.get("/call-external")
def call_external(api_key: str = Depends(get_api_key_dep)):
    """
    【ルート】外部API呼び出し(例)
    """
    # ここで api_key を使って外部APIを叩く(例)
    return {"ok": True}

6. ini と環境変数を混ぜる定石(iniは非秘密、秘密はenv)

実務では以下の分離が分かりやすいです。

# 例:DB接続情報の組み立て(疑似例)
db_user = ini_cfg.get("db", "user")
db_pass = os.getenv("DB_PASSWORD")  # 秘密はenv
dsn = f"postgresql://{db_user}:{db_pass}@{host}:{port}/{db_name}"

7. よくある落とし穴(初心者向け)

ユーザーテーブルにユーザーを追加する(FastAPI + ORM / 業務レベル例外処理)

1. 仕様(やりたいこと)

2. 注意事項(初心者がハマるポイント)

  • 形式チェックはDTO(Pydantic)で行うと、Controller/Serviceが綺麗になる
  • email重複チェックは2段構え
    • ① 事前にSELECTして重複判定(分かりやすい)
    • ② それでも同時実行で競合するので、DBのUNIQUE制約IntegrityErrorも捕まえる
  • 「業務レベル」= 例外を握りつぶさず、HTTPステータスとエラーコードを揃えて返す
  • updated_at は「更新日時」なので、INSERT時にも自動で入るようにする(ORM側で自動設定)

3. フォルダ構成(例)

app/
  main.py
  dependencies.py
  routers/
    users_routes.py
  controllers/
    users_controller.py
  services/
    users_service.py
  repositories/
    users_repository.py
  db/
    session.py
    models.py
  dto/
    users_dto.py
  responses/
    error_response.py
  errors/
    app_errors.py

4. コード一式(Router / Controller / Service / Repository / DB / DTO / Response)

4-1. エラー型(業務例外): app/errors/app_errors.py

# app/errors/app_errors.py

class BusinessError(Exception):
    """
    【例外】業務例外の基底
    - ControllerがHTTPエラーへ変換する対象
    """
    def __init__(self, error_code: str, message: str, details: dict | None = None):
        super().__init__(message)
        self.error_code = error_code          # 【プロパティ】業務エラーコード
        self.message = message                # 【プロパティ】ユーザー向けメッセージ(必要なら)
        self.details = details or {}          # 【プロパティ】追加情報(フィールド名など)

class RequestValidationError(BusinessError):
    """
    【例外】入力値の業務的な不正(400)
    """
    pass

class DuplicateEmailError(BusinessError):
    """
    【例外】email重複(400)
    """
    pass

class DbInsertConflictError(BusinessError):
    """
    【例外】DBのINSERT競合/制約違反など(409)
    """
    pass

4-2. エラーレスポンスDTO: app/responses/error_response.py

# app/responses/error_response.py
from pydantic import BaseModel

class ErrorResponse(BaseModel):
    """
    【DTO】エラーレスポンス統一形式
    - error_code: 機械判定用
    - message: 人間向け
    - details: フィールド名や原因など追加情報
    """
    error_code: str
    message: str
    details: dict

4-3. DTO(Request/Response): app/dto/users_dto.py

# app/dto/users_dto.py
from pydantic import BaseModel, Field, EmailStr, constr

# tel形式(例):数字とハイフンのみ、10〜13文字程度(例:090-1234-5678 / 0312345678)
TelStr = constr(pattern=r"^[0-9-]{10,13}$")

class CreateUserRequest(BaseModel):
    """
    【DTO】ユーザー作成リクエスト(POST Body)
    - name/tel/email 必須
    - tel/email 形式チェックあり
    """
    name: str = Field(..., min_length=1, max_length=100, description="ユーザー名(必須)")
    tel: TelStr = Field(..., description="電話番号(必須:数字とハイフン、10〜13文字)")
    email: EmailStr = Field(..., description="メールアドレス(必須:形式チェックあり)")

class CreateUserResponse(BaseModel):
    """
    【DTO】ユーザー作成レスポンス
    """
    id: int
    name: str
    tel: str
    email: str
    isactive: bool
    updated_at: str

4-4. DBセッション(SQLAlchemy): app/db/session.py

# app/db/session.py
from typing import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session

# SQLite例(実務では環境変数で切り替え)
DATABASE_URL = "sqlite:///./app.db"

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False},  # SQLiteのスレッド制約回避(FastAPI用)
    pool_pre_ping=True,                         # 接続死活チェック
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db() -> Generator[Session, None, None]:
    """
    【DI関数】DBセッションを提供する
    - リクエスト中は同じSessionを使う
    - 最後に必ずcloseする
    """
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

4-5. ORMモデル: app/db/models.py

# app/db/models.py
from datetime import datetime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Integer, Boolean, DateTime, UniqueConstraint

class Base(DeclarativeBase):
    """
    【ベース】SQLAlchemy Declarative Base
    """
    pass

class UserModel(Base):
    """
    【ORM】usersテーブル
    - emailはUNIQUE(重複登録禁止)
    - updated_at は INSERT/UPDATEで自動更新
    """
    __tablename__ = "users"
    __table_args__ = (
        UniqueConstraint("email", name="uq_users_email"),
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)  # 【PK】id
    name: Mapped[str] = mapped_column(String(100), nullable=False)                   # 【必須】name
    tel: Mapped[str] = mapped_column(String(20), nullable=False)                     # 【必須】tel
    email: Mapped[str] = mapped_column(String(255), nullable=False)                  # 【必須】email(UNIQUE)
    isactive: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)    # 【必須】isactive
    updated_at: Mapped[datetime] = mapped_column(
        DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow
    )

4-6. DI(Repository/Serviceを組み立て): app/dependencies.py

# app/dependencies.py
from fastapi import Depends
from sqlalchemy.orm import Session

from app.db.session import get_db
from app.repositories.users_repository import UsersRepository
from app.services.users_service import UsersService

def get_users_repository(db: Session = Depends(get_db)) -> UsersRepository:
    """
    【DI関数】UsersRepositoryを生成する
    """
    return UsersRepository(db)

def get_users_service(repo: UsersRepository = Depends(get_users_repository)) -> UsersService:
    """
    【DI関数】UsersServiceを生成する
    """
    return UsersService(repo)

4-7. Repository(ORMアクセス): app/repositories/users_repository.py

# app/repositories/users_repository.py
from sqlalchemy.orm import Session
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError, SQLAlchemyError

from app.db.models import UserModel

class UsersRepository:
    """
    【Repository】users テーブルアクセス(ORM)
    - SQLAlchemy Sessionを使ってDB操作を行う
    """

    def __init__(self, db: Session):
        """
        【コンストラクタ】DBセッションを受け取って保持する
        """
        self.db = db  # 【プロパティ】SQLAlchemy Session

    def exists_by_email(self, email: str) -> bool:
        """
        【メソッド】emailが既に存在するか確認する
        """
        stmt = select(UserModel.id).where(UserModel.email == email)
        row = self.db.execute(stmt).first()
        return row is not None

    def insert_user(self, name: str, tel: str, email: str) -> UserModel:
        """
        【メソッド】ユーザーをINSERTして、作成されたUserModelを返す
        - isactive は true を強制
        - commit/rollbackはここで行う(業務レベルの責務分担として)
        """
        user = UserModel(
            name=name,
            tel=tel,
            email=email,
            isactive=True,     # 【仕様】insert時は必ず true
        )

        try:
            self.db.add(user)        # 【処理】INSERT対象として登録
            self.db.commit()         # 【処理】トランザクション確定
            self.db.refresh(user)    # 【処理】DB採番IDなどを反映
            return user              # 【返却】作成されたユーザー

        except IntegrityError:
            # 【例外】UNIQUE制約違反など(email重複を含む可能性)
            self.db.rollback()
            raise

        except SQLAlchemyError:
            # 【例外】その他DB例外
            self.db.rollback()
            raise

4-8. Service(業務ロジック+業務例外): app/services/users_service.py

# app/services/users_service.py
from sqlalchemy.exc import IntegrityError, SQLAlchemyError

from app.repositories.users_repository import UsersRepository
from app.errors.app_errors import DuplicateEmailError, DbInsertConflictError

class UsersService:
    """
    【Service】ユーザー作成の業務ロジック
    - email重複チェック
    - RepositoryでINSERT
    - DB例外を業務例外へ変換
    """

    def __init__(self, repo: UsersRepository):
        """
        【コンストラクタ】Repositoryを受け取り保持する
        """
        self.repo = repo  # 【プロパティ】UsersRepository

    def create_user(self, name: str, tel: str, email: str):
        """
        【メソッド】ユーザーを作成する
        - email重複なら 400
        - INSERT失敗なら 409
        """

        # 【業務】email重複チェック(分かりやすい事前チェック)
        if self.repo.exists_by_email(email):
            raise DuplicateEmailError(
                error_code="DUPLICATE_EMAIL",
                message="email is already registered",
                details={"field": "email", "email": email},
            )

        # 【DB】INSERT(Repositoryがcommitまで実施)
        try:
            return self.repo.insert_user(name=name, tel=tel, email=email)

        except IntegrityError:
            # 【例外】同時実行での競合などでUNIQUE違反が起きた場合もここに来る
            # → 要件では「email登録済みは400」なので、ここも400として扱う
            raise DuplicateEmailError(
                error_code="DUPLICATE_EMAIL",
                message="email is already registered",
                details={"field": "email", "email": email, "reason": "unique_constraint"},
            )

        except SQLAlchemyError as e:
            # 【例外】INSERT時のDBエラー → 409 Conflict(詳細コードを付ける)
            raise DbInsertConflictError(
                error_code="DB_INSERT_FAILED",
                message="failed to insert user",
                details={"reason": str(e.__class__.__name__)},
            )

4-9. Controller(HTTP入口:例外→HTTP変換 / エラーJSON統一): app/controllers/users_controller.py

# app/controllers/users_controller.py
import logging
from fastapi import HTTPException

from app.services.users_service import UsersService
from app.dto.users_dto import CreateUserRequest, CreateUserResponse
from app.responses.error_response import ErrorResponse
from app.errors.app_errors import DuplicateEmailError, DbInsertConflictError

logger = logging.getLogger("app")

class UsersController:
    """
    【Controller】HTTP入口の処理
    - Serviceを呼ぶ
    - 業務例外をHTTPステータスに変換
    - エラーボディは ErrorResponse 形式で返す
    """

    def create_user(self, req: CreateUserRequest, service: UsersService) -> CreateUserResponse:
        """
        【メソッド】POST /users の処理
        """
        try:
            user = service.create_user(name=req.name, tel=req.tel, email=req.email)

            # 【返却】レスポンスDTO(updated_atはISO文字列にして返す)
            return CreateUserResponse(
                id=user.id,
                name=user.name,
                tel=user.tel,
                email=user.email,
                isactive=bool(user.isactive),
                updated_at=user.updated_at.isoformat(),
            )

        except DuplicateEmailError as e:
            # 【業務】email重複(要件:400 + JSONでエラー情報)
            body = ErrorResponse(error_code=e.error_code, message=e.message, details=e.details).model_dump()
            raise HTTPException(status_code=400, detail=body)

        except DbInsertConflictError as e:
            # 【業務】INSERT失敗(要件:409 + 詳細コード)
            body = ErrorResponse(error_code=e.error_code, message=e.message, details=e.details).model_dump()
            raise HTTPException(status_code=409, detail=body)

        except Exception as e:
            # 【想定外】業務レベルではログを残して500
            logger.exception("unexpected error in create_user")
            body = ErrorResponse(
                error_code="INTERNAL_ERROR",
                message="internal server error",
                details={"reason": str(e.__class__.__name__)},
            ).model_dump()
            raise HTTPException(status_code=500, detail=body)

4-10. Router(URL紐付け+DI注入): app/routers/users_routes.py

# app/routers/users_routes.py
from fastapi import APIRouter, Depends
from app.dto.users_dto import CreateUserRequest, CreateUserResponse
from app.services.users_service import UsersService
from app.dependencies import get_users_service
from app.controllers.users_controller import UsersController

router = APIRouter(prefix="/users", tags=["users"])
controller = UsersController()

@router.post("", response_model=CreateUserResponse)
def create_user(
    body: CreateUserRequest,                                   # 【引数】JSON Body(DTOで形式チェック)
    service: UsersService = Depends(get_users_service),         # 【DI】Service注入
) -> CreateUserResponse:
    """
    【ルート】POST /users
    - RouterはURL紐付けのみ
    - 実処理はControllerへ委譲
    """
    return controller.create_user(req=body, service=service)

4-11. main.py(起動点+テーブル作成例): app/main.py

# app/main.py
import logging
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError as FastApiRequestValidationError

from app.routers.users_routes import router as users_router
from app.db.session import engine
from app.db.models import Base
from app.responses.error_response import ErrorResponse

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s - %(message)s")
logger = logging.getLogger("app")

app = FastAPI()
app.include_router(users_router)

@app.on_event("startup")
def on_startup():
    """
    【イベント】起動時処理
    - 例:テーブル作成(学習用)
    - 実務では migration ツールを使うことが多い
    """
    Base.metadata.create_all(bind=engine)
    logger.info("app started")

@app.exception_handler(FastApiRequestValidationError)
async def validation_exception_handler(request, exc):
    """
    【例外ハンドラ】Pydantic/入力形式エラーを 400 に統一する
    - FastAPIのデフォルトは422になりがちなので、要件に合わせて400へ寄せる
    """
    body = ErrorResponse(
        error_code="BAD_REQUEST",
        message="invalid request",
        details={"errors": exc.errors()},
    ).model_dump()
    return JSONResponse(status_code=400, content={"detail": body})

5. 返却されるエラーJSONの例

5-1. email重複(400)

{
  "detail": {
    "error_code": "DUPLICATE_EMAIL",
    "message": "email is already registered",
    "details": {
      "field": "email",
      "email": "alice@example.com"
    }
  }
}

5-2. INSERT失敗(409)

{
  "detail": {
    "error_code": "DB_INSERT_FAILED",
    "message": "failed to insert user",
    "details": {
      "reason": "OperationalError"
    }
  }
}
補足:HTTPExceptionの標準仕様により、ここでは detail 配下にJSONを入れています。
「エラーJSONをトップレベルで返したい」場合は、Controller側で JSONResponse を直接返す運用にします。

注文・請求ルールエンジン(FastAPI版:業務ロジック中心)

1. 仕様(やりたいこと)

入力(注文JSON)を受け取り、税・割引・送料を計算して、請求結果を返します。
「HTTP通信」よりも、業務ロジックを安全に実装することを主目的とします。

1-1. 入力(注文JSON)

{
  "customerRank": "GOLD",
  "region": "TOKYO",
  "items": [
    {"sku": "A001", "unitPrice": 980,  "qty": 2, "tax": "STANDARD"},
    {"sku": "B777", "unitPrice": 1500, "qty": 1, "tax": "REDUCED"}
  ],
  "coupon": "WELCOME10"
}

1-2. 出力(計算結果)

{
  "subtotal": 3460,
  "taxTotal": 292,
  "discount": 346,
  "shipping": 500,
  "grandTotal": 3906
}

1-3. 業務ルール


2. 注意点(業務として安全に書くコツ)

  • 区分値はEnumで固定:customerRank / region / tax を文字列のまま扱わない
  • 入力検証はDTO(Pydantic)に寄せる:必須/型/不正値を入口で落とす
  • 計算はService(RuleEngine)に集約:Controllerに計算ロジックを書かない
  • couponはoptional:無いケースを必ず想定し、None分岐を入れる
  • 金額はint(円)で扱う:float(小数)で計算すると誤差が出る
  • 行処理は for(foreach相当)で素直に回す(可読性優先)

3. コード(Router / Controller / Service / DTO / Enum / Response)

3-1. フォルダ構成(例)

app/
  main.py
  routers/
    billing_routes.py
  controllers/
    billing_controller.py
  services/
    billing_rule_engine.py
  dto/
    billing_dto.py
  enums/
    billing_enums.py
  responses/
    error_response.py

3-2. Enum定義: app/enums/billing_enums.py

# app/enums/billing_enums.py
from enum import Enum

class CustomerRank(str, Enum):
    """
    【Enum】顧客ランク
    - 入力値を固定し、タイポや想定外値を弾く
    """
    BRONZE = "BRONZE"
    SILVER = "SILVER"
    GOLD = "GOLD"

class Region(str, Enum):
    """
    【Enum】配送地域
    """
    TOKYO = "TOKYO"
    OTHER = "OTHER"

class TaxCategory(str, Enum):
    """
    【Enum】税区分
    """
    STANDARD = "STANDARD"
    REDUCED = "REDUCED"
    EXEMPT = "EXEMPT"

class CouponCode(str, Enum):
    """
    【Enum】クーポン(任意)
    - 必要最小限の例
    """
    WELCOME10 = "WELCOME10"

3-3. DTO(入力/出力): app/dto/billing_dto.py

# app/dto/billing_dto.py
from pydantic import BaseModel, Field
from typing import Optional, List

from app.enums.billing_enums import CustomerRank, Region, TaxCategory, CouponCode

class OrderItemDto(BaseModel):
    """
    【DTO】注文行
    """
    sku: str = Field(..., min_length=1, max_length=50, description="商品コード")
    unitPrice: int = Field(..., ge=0, description="単価(円)")
    qty: int = Field(..., ge=1, description="数量")
    tax: TaxCategory = Field(..., description="税区分(Enum)")

class OrderInputDto(BaseModel):
    """
    【DTO】注文入力(Request Body)
    - customerRank / region / tax はEnumとして受け取る
    - coupon は optional(無い場合は None)
    """
    customerRank: CustomerRank = Field(..., description="顧客ランク")
    region: Region = Field(..., description="配送地域")
    items: List[OrderItemDto] = Field(..., min_length=1, description="注文行配列")
    coupon: Optional[CouponCode] = Field(None, description="クーポン(任意)")

class BillingResultDto(BaseModel):
    """
    【DTO】計算結果(Response)
    """
    subtotal: int
    taxTotal: int
    discount: int
    shipping: int
    grandTotal: int

3-4. エラーレスポンス(業務向けの形): app/responses/error_response.py

# app/responses/error_response.py
from pydantic import BaseModel

class ErrorResponse(BaseModel):
    """
    【DTO】エラーレスポンスの統一形式
    """
    error_code: str
    message: str
    details: dict

3-5. Service(ルールエンジン本体): app/services/billing_rule_engine.py

# app/services/billing_rule_engine.py
from dataclasses import dataclass
from typing import Tuple

from app.dto.billing_dto import OrderInputDto, BillingResultDto
from app.enums.billing_enums import TaxCategory, CustomerRank, Region, CouponCode

@dataclass(frozen=True)
class BillingRules:
    """
    【定数】業務ルール値(マジックナンバー排除)
    """
    TAX_STANDARD: float = 0.10
    TAX_REDUCED: float = 0.08
    TAX_EXEMPT: float = 0.00

    GOLD_DISCOUNT: float = 0.05
    WELCOME10_DISCOUNT: float = 0.10

    SHIPPING_TOKYO: int = 500
    SHIPPING_OTHER: int = 800
    FREE_SHIPPING_THRESHOLD: int = 10000

class BillingRuleEngine:
    """
    【Service】請求計算ルールエンジン
    - 税/割引/送料/合計の計算本体をここに集約
    """

    def __init__(self, rules: BillingRules = BillingRules()):
        """
        【コンストラクタ】ルール値を注入(テスト差し替え可能)
        """
        self.rules = rules

    def calc_tax_rate(self, tax: TaxCategory) -> float:
        """
        【メソッド】税区分(Enum)から税率を返す
        """
        # matchはPython 3.10+(Windowsでも普通に使える)
        match tax:
            case TaxCategory.STANDARD:
                return self.rules.TAX_STANDARD
            case TaxCategory.REDUCED:
                return self.rules.TAX_REDUCED
            case TaxCategory.EXEMPT:
                return self.rules.TAX_EXEMPT
        # Enumなので原則ここには来ない(保険)
        return self.rules.TAX_EXEMPT

    def calc_member_discount_rate(self, rank: CustomerRank) -> float:
        """
        【メソッド】会員ランクから割引率を返す
        """
        if rank == CustomerRank.GOLD:
            return self.rules.GOLD_DISCOUNT
        return 0.0

    def calc_coupon_discount_rate(self, coupon: CouponCode | None) -> float:
        """
        【メソッド】クーポンから割引率を返す(任意)
        """
        if coupon == CouponCode.WELCOME10:
            return self.rules.WELCOME10_DISCOUNT
        return 0.0

    def calc_shipping(self, region: Region, subtotal: int) -> int:
        """
        【メソッド】送料を計算する(送料無料条件あり)
        """
        if subtotal >= self.rules.FREE_SHIPPING_THRESHOLD:
            return 0

        match region:
            case Region.TOKYO:
                return self.rules.SHIPPING_TOKYO
            case Region.OTHER:
                return self.rules.SHIPPING_OTHER
        return self.rules.SHIPPING_OTHER

    def calc(self, order: OrderInputDto) -> BillingResultDto:
        """
        【メソッド】注文入力から請求結果を計算して返す(業務の中心)
        - 行処理:forで安全に回す
        - 金額:int(円)
        """
        # 1) 行小計・税額を集計
        subtotal = 0
        tax_total = 0

        for item in order.items:
            line_subtotal = item.unitPrice * item.qty
            subtotal += line_subtotal

            rate = self.calc_tax_rate(item.tax)
            # 金額は int(円)なので丸めが必要。ここでは四捨五入(業務要件次第)
            line_tax = int(round(line_subtotal * rate))
            tax_total += line_tax

        # 2) 割引(会員 + クーポン)を合算
        member_rate = self.calc_member_discount_rate(order.customerRank)
        coupon_rate = self.calc_coupon_discount_rate(order.coupon)
        discount_rate = member_rate + coupon_rate

        discount = int(round(subtotal * discount_rate))

        # 3) 送料
        shipping = self.calc_shipping(order.region, subtotal)

        # 4) 合計
        grand_total = subtotal + tax_total - discount + shipping

        return BillingResultDto(
            subtotal=subtotal,
            taxTotal=tax_total,
            discount=discount,
            shipping=shipping,
            grandTotal=grand_total,
        )

3-6. Controller(HTTP入口は薄く): app/controllers/billing_controller.py

# app/controllers/billing_controller.py
import logging
from fastapi import HTTPException

from app.dto.billing_dto import OrderInputDto, BillingResultDto
from app.responses.error_response import ErrorResponse
from app.services.billing_rule_engine import BillingRuleEngine

logger = logging.getLogger("app")

class BillingController:
    """
    【Controller】HTTP受け口(薄く)
    - 入力DTOはFastAPI/Pydanticが検証済み
    - ここではServiceを呼ぶだけ
    - 想定外はログを残して500
    """

    def __init__(self, engine: BillingRuleEngine):
        """
        【コンストラクタ】RuleEngineを受け取る
        """
        self.engine = engine

    def calculate(self, order: OrderInputDto) -> BillingResultDto:
        """
        【メソッド】請求計算を実行する
        """
        try:
            return self.engine.calc(order)
        except Exception as e:
            logger.exception("billing calculation failed")
            body = ErrorResponse(
                error_code="BILLING_CALC_FAILED",
                message="billing calculation failed",
                details={"reason": e.__class__.__name__},
            ).model_dump()
            raise HTTPException(status_code=500, detail=body)

3-7. Router(DI組み立て): app/routers/billing_routes.py

# app/routers/billing_routes.py
from fastapi import APIRouter, Depends
from app.dto.billing_dto import OrderInputDto, BillingResultDto
from app.services.billing_rule_engine import BillingRuleEngine
from app.controllers.billing_controller import BillingController

router = APIRouter(prefix="/billing", tags=["billing"])

def get_billing_engine() -> BillingRuleEngine:
    """
    【DI関数】RuleEngineを生成して返す
    - ルールが固定ならシングルトンでもよい
    """
    return BillingRuleEngine()

def get_billing_controller(engine: BillingRuleEngine = Depends(get_billing_engine)) -> BillingController:
    """
    【DI関数】Controllerを生成して返す
    """
    return BillingController(engine)

@router.post("/calculate", response_model=BillingResultDto)
def calculate(
    order: OrderInputDto,
    controller: BillingController = Depends(get_billing_controller),
) -> BillingResultDto:
    """
    【ルート】POST /billing/calculate
    - RouterはHTTP入口(薄く)
    - DTO検証後にControllerへ委譲する
    """
    return controller.calculate(order)

3-8. main.py(起動点): app/main.py

# app/main.py
import logging
from fastapi import FastAPI
from app.routers.billing_routes import router as billing_router

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s - %(message)s")

app = FastAPI()
app.include_router(billing_router)

4. 動作確認用サンプル(curl)

curl -X POST http://127.0.0.1:8000/billing/calculate ^
  -H "Content-Type: application/json" ^
  -d "{\"customerRank\":\"GOLD\",\"region\":\"TOKYO\",\"items\":[{\"sku\":\"A001\",\"unitPrice\":980,\"qty\":2,\"tax\":\"STANDARD\"},{\"sku\":\"B777\",\"unitPrice\":1500,\"qty\":1,\"tax\":\"REDUCED\"}],\"coupon\":\"WELCOME10\"}"
WindowsのPowerShellだとクォートが面倒なので、Postman/Insomniaを使うのが楽です。

FastAPI + SQLite3:DBファイル生成 → テーブル作成 → 初期データ投入(起動時/セットアップ時)

1. 結論:FastAPIでも可能。ただし「HTTPのたびに作る」はやらない

重要(業務の常識)
  • FastAPIでも テーブル作成・初期データ投入(Seeder) は可能
  • ただし HTTPリクエスト毎に「テーブル無ければ作る」は NG
  • 理由:同時アクセス競合、性能劣化、意図しないスキーマ変更、本番事故の原因
  • 定石:起動時 または セットアップ用コマンドで実行する
  • 本番運用:Migrationツール(例:Alembic)を使うのが一般的

2. 推奨フロー(Mermaid)

flowchart TD
    A[アプリ起動 / テスト開始] --> B[DBファイルの存在チェック]
    B -->|無い| C[SQLiteファイル生成(接続すれば自動作成)]
    B -->|ある| D[次へ]
    C --> E[テーブル作成(Base.metadata.create_all)]
    D --> E[テーブル作成(必要に応じて)]
    E --> F[初期データ投入(seed)]
    F --> G[API起動完了]
SQLiteは「接続した瞬間にDBファイルが作られる」ため、
ファイル生成=接続とほぼ同義です。

3. フォルダ構成(例:起動時initルーチンを分離)

app/
  main.py
  db/
    session.py
    models.py
    init_db.py          # ★ここに「作成&seed」を集約
  routers/
    ...

4. コード例(SQLAlchemy + SQLite)

4-1. DB接続(SQLite): app/db/session.py

# app/db/session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "sqlite:///./app.db"

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False},
    pool_pre_ping=True,
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

4-2. ORMモデル(例:users): app/db/models.py

# app/db/models.py
from datetime import datetime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String, Boolean, DateTime, UniqueConstraint

class Base(DeclarativeBase):
    pass

class UserModel(Base):
    __tablename__ = "users"
    __table_args__ = (UniqueConstraint("email", name="uq_users_email"),)

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(100), nullable=False)
    tel: Mapped[str] = mapped_column(String(20), nullable=False)
    email: Mapped[str] = mapped_column(String(255), nullable=False)
    isactive: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
    updated_at: Mapped[datetime] = mapped_column(
        DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow
    )

4-3. DB初期化(テーブル作成+seed): app/db/init_db.py

# app/db/init_db.py
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.db.session import engine, SessionLocal
from app.db.models import Base, UserModel

def create_tables() -> None:
    """
    【関数】テーブル作成(ローカル/テスト用)
    - SQLiteなら、engine接続でDBファイルが自動生成される
    - create_all は「無ければ作る」だが、HTTPのたびに呼ばない
    """
    Base.metadata.create_all(bind=engine)

def seed_initial_data(db: Session) -> None:
    """
    【関数】初期データ投入(Seeder相当)
    - 例:管理者ユーザー1件を入れる
    - 既に存在する場合は二重投入しない
    """
    admin_email = "admin@example.com"

    # 既存チェック(INSERT重複防止)
    exists = db.execute(select(UserModel.id).where(UserModel.email == admin_email)).first()
    if exists:
        return

    admin = UserModel(
        name="Admin",
        tel="000-0000-0000",
        email=admin_email,
        isactive=True,
    )
    db.add(admin)
    db.commit()

def init_db_for_local_or_test() -> None:
    """
    【関数】初期化ルーチン(まとめ)
    - テーブル作成
    - seed投入
    """
    create_tables()
    db = SessionLocal()
    try:
        seed_initial_data(db)
    finally:
        db.close()

4-4. 起動時に一度だけ呼ぶ(FastAPI startup): app/main.py

# app/main.py
import os
import logging
from fastapi import FastAPI

from app.db.init_db import init_db_for_local_or_test

logger = logging.getLogger("app")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s - %(message)s")

app = FastAPI()

@app.on_event("startup")
def on_startup():
    """
    【イベント】起動時に1回だけ呼ばれる
    - ローカル/テスト環境だけ DB初期化する(本番で勝手に作らない)
    """
    env = os.getenv("APP_ENV", "dev")
    if env in ("dev", "test"):
        init_db_for_local_or_test()
        logger.info("db initialized for %s", env)
    else:
        logger.info("skip db init (env=%s)", env)

5. 本番ではどうする?(業務の定石)

  • 本番で create_all() は避ける(スキーマ変更管理ができない)
  • 本番は Alembic 等でMigrationを管理する
  • Seedは「初回だけ」「管理コマンドで実行」など運用設計をする
  • FastAPIのstartupは便利だが、環境変数で実行範囲を制御するのが必須

6. 使い方(例)

# Windows PowerShell(開発)
$env:APP_ENV="dev"
uvicorn app.main:app --reload

# Linux(開発)
export APP_ENV=dev
uvicorn app.main:app --reload

Alembic migration(Laravelのmigration相当)を最小構成で導入する手順(FastAPI + SQLAlchemy + SQLite例)

1. 仕様(この手順で実現すること)


2. 注意点(業務の常識)

  • 本番で create_all() は避ける:履歴管理ができず事故る
  • migrationは「起動時に自動で流さない」のが定石(多重起動・競合の原因)
  • 本番は「デプロイ手順の一部」として alembic upgrade head を実行する
  • autogenerateは万能ではない:rename/複雑な変更は手で編集が必要
  • Seeder(初期データ投入)は migration と分けて管理するのが普通

3. フォルダ構成(最小例)

app/
  main.py
  db/
    session.py
    models.py          # SQLAlchemy ORMモデル(Base含む)
alembic.ini
alembic/
  env.py
  script.py.mako
  versions/
    xxxxx_initial_users.py

4. 導入手順(コマンド)

4-1. インストール

# 例:同期SQLAlchemy + Alembic + SQLite
pip install sqlalchemy alembic

4-2. Alembic初期化(プロジェクト直下で)

alembic init alembic

これで alembic/ フォルダと alembic.ini が生成されます。


5. コード(最小構成)

5-1. DB URLを定義(SQLAlchemy engine): app/db/session.py

# app/db/session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "sqlite:///./app.db"  # 本番は環境変数に寄せるのが定石

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False},  # SQLite + FastAPIの定番設定
    pool_pre_ping=True,
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

5-2. ORMモデル定義(Base含む): app/db/models.py

# app/db/models.py
from datetime import datetime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String, Boolean, DateTime, UniqueConstraint

class Base(DeclarativeBase):
    pass

class UserModel(Base):
    __tablename__ = "users"
    __table_args__ = (UniqueConstraint("email", name="uq_users_email"),)

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(100), nullable=False)
    tel: Mapped[str] = mapped_column(String(20), nullable=False)
    email: Mapped[str] = mapped_column(String(255), nullable=False)
    isactive: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
    updated_at: Mapped[datetime] = mapped_column(
        DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow
    )

6. Alembic設定(ここが肝)

6-1. alembic.ini のDB URLを設定

生成された alembic.ini を開き、以下を設定します。

; alembic.ini
sqlalchemy.url = sqlite:///./app.db
実務では 環境変数から読むことが多いです(後述の env.py で上書き可能)。

6-2. alembic/env.py に「モデルのBase」を教える(autogenerate用)

生成された alembic/env.py を編集して、 target_metadata にSQLAlchemyの Base.metadata を設定します。

# alembic/env.py(重要部分だけ抜粋・最小例)
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context

import os
import sys

# app を import できるようにパス追加(最小構成の定番)
sys.path.append(os.path.abspath("."))

from app.db.models import Base  # ← ここが重要(モデルのBase)
# もし engine を使いたいなら:from app.db.session import DATABASE_URL

config = context.config
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# ★ autogenerateが参照するメタデータ
target_metadata = Base.metadata


def run_migrations_online() -> None:
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            compare_type=True,  # 型変更検出をある程度助ける
        )

        with context.begin_transaction():
            context.run_migrations()


run_migrations_online()
ここがLaravelとの対応
  • Laravel:migrationファイルがスキーマ変更の真実
  • FastAPI/SQLAlchemy:モデルがスキーマの真実になりがち
  • Alembic:モデル差分からmigrationを生成し、履歴として保存する

7. 初回 migration を作る(initial)

7-1. autogenerateで作成

alembic revision --autogenerate -m "initial users"

alembic/versions/xxxx_initial_users.py が生成されます。

7-2. 生成されたmigration(例)

# alembic/versions/xxxx_initial_users.py(例:概略)
from alembic import op
import sqlalchemy as sa

revision = "xxxx"
down_revision = None

def upgrade() -> None:
    op.create_table(
        "users",
        sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
        sa.Column("name", sa.String(length=100), nullable=False),
        sa.Column("tel", sa.String(length=20), nullable=False),
        sa.Column("email", sa.String(length=255), nullable=False),
        sa.Column("isactive", sa.Boolean(), nullable=False),
        sa.Column("updated_at", sa.DateTime(), nullable=False),
    )
    op.create_unique_constraint("uq_users_email", "users", ["email"])

def downgrade() -> None:
    op.drop_table("users")
autogenerateは便利ですが、生成物を 必ず目視確認してください。

8. migration を適用(テーブル作成)

# 最新まで適用(Laravelの migrate 相当)
alembic upgrade head

これで app.db が無ければ作られ、テーブルが作成されます。

8-1. 状態確認コマンド

# 現在適用されている revision を表示
alembic current

# 履歴表示
alembic history --verbose

9. どう運用するのが定石か(開発/本番)

9-1. 開発(ローカル)

# モデル変更
# ↓
alembic revision --autogenerate -m "add xxx"
# ↓
alembic upgrade head
# ↓
アプリ起動(uvicorn)

9-2. 本番

  • デプロイ手順の中で alembic upgrade head を実行
  • アプリ起動時に自動migrationはしない(競合するから)
  • 複数台/複数プロセスの場合、migrationは 代表1箇所だけで実行する

10. (任意)DB URLを環境変数にする(本番向け)

alembic.ini を固定にせず、環境変数で切り替える場合は、 alembic/env.pysqlalchemy.url を上書きします。

# alembic/env.py(冒頭あたりで追加)
db_url = os.getenv("DATABASE_URL")
if db_url:
    config.set_main_option("sqlalchemy.url", db_url)
# Windows PowerShell
$env:DATABASE_URL="sqlite:///./app.db"
alembic upgrade head

# Linux
export DATABASE_URL="sqlite:///./app.db"
alembic upgrade head

UserSetting(JSON)をログイン時に読み込み、全処理から参照できる設定クラスに保持する(FastAPI版)

1. 仕様(FastAPIに置き換え)


2. 注意点(FastAPI/Starletteの前提)

  • FastAPIも通常は「リクエスト単位」で処理が完結するため、メモリに保持した値は次リクエストに自動で残りません
  • そのため Laravel同様に 2段構えにします:
    • ① ログイン直後:DB → Session に保存(次リクエスト以降も維持)
    • ② 各リクエスト開始時:Session → UserSettingsStore に復元(全処理から参照)
  • Sessionの保存先
    • 最小構成:Cookie署名型セッション(Starlette SessionsMiddleware)
    • 注意:Cookieにはサイズ制限があるため、settingsが大きいなら Redis 等のサーバーセッションを推奨
  • 「全処理から参照」は、FastAPIでは通常 request.state に置いて依存性(Depends)で取り出すのが定石

3. フォルダ構成(例)

app/
  main.py
  middleware/
    user_settings_middleware.py
  support/
    user_settings_store.py
  db/
    session.py
    models.py
  repositories/
    user_settings_repository.py
  services/
    auth_service.py
  controllers/
    auth_controller.py
  dto/
    auth_dto.py
    error_response.py
  routers/
    auth_routes.py
    sample_routes.py

4. コード一式(Router / Controller / Service / Repository / DB / DTO / Middleware / Store)

4-1. UserSettingsStore(全層から参照するためのクラス): app/support/user_settings_store.py

# app/support/user_settings_store.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Any

def _get_by_path(obj: dict, path: str) -> Any:
    """
    【関数】"paging.per_page" のようなドット区切りパスで dict を辿る
    - 無ければ KeyError を投げる
    """
    cur: Any = obj
    for part in path.split("."):
        if not isinstance(cur, dict) or part not in cur:
            raise KeyError(path)
        cur = cur[part]
    return cur

@dataclass
class UserSettingsStore:
    """
    【クラス】ユーザー設定の参照ストア(リクエスト内で有効)
    - settings: dict(JSONを読み込んだもの)
    """
    settings: dict

    def get(self, path: str, default: Any = None) -> Any:
        """
        【メソッド】任意型で取得
        """
        try:
            return _get_by_path(self.settings, path)
        except KeyError:
            return default

    def get_int(self, path: str, default: int) -> int:
        """
        【メソッド】intで取得(型が違う/無いならdefault)
        """
        value = self.get(path, default)
        try:
            return int(value)
        except Exception:
            return default

4-2. DB(SQLAlchemy + SQLite): app/db/session.py

# app/db/session.py
from typing import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session

DATABASE_URL = "sqlite:///./app.db"

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False},
    pool_pre_ping=True,
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db() -> Generator[Session, None, None]:
    """
    【DI関数】DBセッション(リクエスト単位)
    """
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

4-3. ORMモデル(user_settings): app/db/models.py

# app/db/models.py
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, Text

class Base(DeclarativeBase):
    pass

class UserSettingsModel(Base):
    """
    【ORM】user_settings
    - SQLiteでは JSON 型が環境で揺れるため、最小構成では Text に JSON文字列を保存する例にする
    """
    __tablename__ = "user_settings"

    user_id: Mapped[int] = mapped_column(Integer, primary_key=True)  # user_id PK
    settings: Mapped[str] = mapped_column(Text, nullable=False)      # JSON文字列(必須)

4-4. Repository(settings取得): app/repositories/user_settings_repository.py

# app/repositories/user_settings_repository.py
import json
import logging
from sqlalchemy.orm import Session
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError

from app.db.models import UserSettingsModel

logger = logging.getLogger("app")

class UserSettingsRepository:
    """
    【Repository】user_settings 取得
    """

    def __init__(self, db: Session):
        """
        【コンストラクタ】DBセッションを受け取る
        """
        self.db = db

    def find_settings_dict_by_user_id(self, user_id: int) -> dict | None:
        """
        【メソッド】user_id の settings(JSON) を dict にして返す
        - 無ければ None
        - DB例外は上位で業務エラー化するため raise する
        """
        try:
            stmt = select(UserSettingsModel.settings).where(UserSettingsModel.user_id == user_id)
            row = self.db.execute(stmt).first()
            if not row:
                return None

            settings_json = row[0]
            return json.loads(settings_json)

        except SQLAlchemyError:
            logger.exception("DB error while loading user_settings (user_id=%s)", user_id)
            raise

        except json.JSONDecodeError:
            # 業務としては「壊れた設定」なので、安全にデフォルトへ落とす等の方針もある
            logger.exception("Invalid JSON in user_settings (user_id=%s)", user_id)
            raise

4-5. DTO(ログイン入力/出力、エラー): app/dto/auth_dto.py / app/dto/error_response.py

# app/dto/auth_dto.py
from pydantic import BaseModel, Field

class LoginRequest(BaseModel):
    """
    【DTO】ログイン入力
    """
    username: str = Field(..., min_length=1)
    password: str = Field(..., min_length=1)

class LoginResponse(BaseModel):
    """
    【DTO】ログイン成功応答(例)
    """
    ok: bool
    user_id: int
# app/dto/error_response.py
from pydantic import BaseModel

class ErrorResponse(BaseModel):
    """
    【DTO】業務エラー応答
    """
    error_code: str
    message: str
    details: dict

4-6. Service(ログイン成功時に settings を読み込んで session に保存): app/services/auth_service.py

# app/services/auth_service.py
import logging
from sqlalchemy.exc import SQLAlchemyError

from app.repositories.user_settings_repository import UserSettingsRepository

logger = logging.getLogger("app")

DEFAULT_SETTINGS: dict = {
    "paging": {"per_page": 20},
    "theme": "light",
}

class AuthService:
    """
    【Service】認証+ユーザー設定ロード
    - 例では認証は簡略化(実務ではDB/IdP等)
    """

    def __init__(self, settings_repo: UserSettingsRepository):
        """
        【コンストラクタ】Repository注入
        """
        self.settings_repo = settings_repo

    def authenticate(self, username: str, password: str) -> int:
        """
        【メソッド】認証(例)
        - 実務ではユーザーテーブル照会、ハッシュ照合など
        - ここでは説明のため固定で user_id を返す
        """
        if username == "demo" and password == "demo":
            return 1
        return 0  # 0は認証失敗扱い

    def load_user_settings_or_default(self, user_id: int) -> dict:
        """
        【メソッド】user_settings を読み込む。無ければデフォルト
        - DB例外は上に投げて、Controllerで安全なエラーへ変換する
        """
        try:
            settings = self.settings_repo.find_settings_dict_by_user_id(user_id)
            if settings is None:
                return DEFAULT_SETTINGS
            return settings
        except SQLAlchemyError:
            logger.exception("Failed to load user_settings (user_id=%s)", user_id)
            raise

4-7. Controller(業務レベル例外処理+Session保存): app/controllers/auth_controller.py

# app/controllers/auth_controller.py
import logging
from fastapi import HTTPException, Request
from sqlalchemy.exc import SQLAlchemyError

from app.dto.auth_dto import LoginRequest, LoginResponse
from app.dto.error_response import ErrorResponse
from app.services.auth_service import AuthService

logger = logging.getLogger("app")

class AuthController:
    """
    【Controller】ログイン処理
    - 成功時:settingsをDBから読み→session保存
    - 失敗時:安全なエラーを返す
    """

    def __init__(self, auth_service: AuthService):
        """
        【コンストラクタ】Service注入
        """
        self.auth_service = auth_service

    def login(self, request: Request, body: LoginRequest) -> LoginResponse:
        """
        【メソッド】POST /auth/login
        """
        user_id = self.auth_service.authenticate(body.username, body.password)
        if user_id == 0:
            err = ErrorResponse(
                error_code="AUTH_FAILED",
                message="invalid username or password",
                details={},
            ).model_dump()
            raise HTTPException(status_code=401, detail=err)

        # ログイン成功:settingsを読み込む(無ければデフォルト)
        try:
            settings = self.auth_service.load_user_settings_or_default(user_id)
        except SQLAlchemyError:
            # DB詳細はログに、APIは安全に
            logger.exception("Login succeeded but settings load failed (user_id=%s)", user_id)
            err = ErrorResponse(
                error_code="SETTINGS_LOAD_FAILED",
                message="failed to load user settings",
                details={},
            ).model_dump()
            raise HTTPException(status_code=500, detail=err)

        # Sessionへ保存(次リクエスト以降も維持)
        # ※ Starlette SessionsMiddleware により request.session が使える
        request.session["user_id"] = user_id
        request.session["user_settings"] = settings

        return LoginResponse(ok=True, user_id=user_id)

4-8. Middleware(各リクエスト開始時:session → store へ復元): app/middleware/user_settings_middleware.py

# app/middleware/user_settings_middleware.py
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response

from app.support.user_settings_store import UserSettingsStore
from app.services.auth_service import DEFAULT_SETTINGS

class UserSettingsRestoreMiddleware(BaseHTTPMiddleware):
    """
    【Middleware】各リクエスト開始時に session から UserSettingsStore を復元する
    - request.state に載せることで、Controller/Serviceから参照できる
    """

    async def dispatch(self, request: Request, call_next) -> Response:
        settings = request.session.get("user_settings") or DEFAULT_SETTINGS
        request.state.user_settings_store = UserSettingsStore(settings=settings)
        return await call_next(request)

4-9. 依存性(どこからでも参照できるようにする): app/main.py 内 or app/dependencies.py

# app/main.py(dependenciesもここに最小実装する例)
from fastapi import Depends, Request
from app.support.user_settings_store import UserSettingsStore

def get_user_settings_store(request: Request) -> UserSettingsStore:
    """
    【DI関数】UserSettingsStoreを取得する
    - request.state から取り出す(Middlewareで復元済み)
    """
    return request.state.user_settings_store

4-10. Router(ログイン): app/routers/auth_routes.py

# app/routers/auth_routes.py
from fastapi import APIRouter, Depends, Request
from sqlalchemy.orm import Session

from app.db.session import get_db
from app.repositories.user_settings_repository import UserSettingsRepository
from app.services.auth_service import AuthService
from app.controllers.auth_controller import AuthController
from app.dto.auth_dto import LoginRequest, LoginResponse

router = APIRouter(prefix="/auth", tags=["auth"])

def get_auth_controller(db: Session = Depends(get_db)) -> AuthController:
    """
    【DI関数】Controller生成(Repo→Service→Controller)
    """
    repo = UserSettingsRepository(db)
    service = AuthService(repo)
    return AuthController(service)

@router.post("/login", response_model=LoginResponse)
def login(
    request: Request,
    body: LoginRequest,
    controller: AuthController = Depends(get_auth_controller),
) -> LoginResponse:
    """
    【ルート】POST /auth/login
    """
    return controller.login(request=request, body=body)

4-11. 「全処理から参照」例(Controller/ServiceどこでもOK): app/routers/sample_routes.py

# app/routers/sample_routes.py
from fastapi import APIRouter, Depends
from app.support.user_settings_store import UserSettingsStore
from app.main import get_user_settings_store

router = APIRouter(prefix="/sample", tags=["sample"])

@router.get("/paging")
def get_paging_settings(store: UserSettingsStore = Depends(get_user_settings_store)):
    """
    【ルート】どの層でも参照できる形のデモ
    """
    per_page = store.get_int("paging.per_page", 20)
    theme = store.get("theme", "light")
    return {"per_page": per_page, "theme": theme}

4-12. main.py(SessionMiddleware + RestoreMiddleware を組み込み): app/main.py

# app/main.py
import logging
from fastapi import FastAPI
from starlette.middleware.sessions import SessionsMiddleware

from app.db.session import engine
from app.db.models import Base
from app.middleware.user_settings_middleware import UserSettingsRestoreMiddleware
from app.routers.auth_routes import router as auth_router
from app.routers.sample_routes import router as sample_router

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s - %(message)s")

app = FastAPI()

# 【Session】署名付きCookieセッション(最小構成)
# ※ 実務では SECRET_KEY は環境変数から読み込む
app.add_middleware(SessionsMiddleware, secret_key="CHANGE_ME_TO_SECURE_RANDOM")

# 【Restore】各リクエスト開始時に session → store 復元
app.add_middleware(UserSettingsRestoreMiddleware)

# ルータ登録
app.include_router(auth_router)
app.include_router(sample_router)

@app.on_event("startup")
def on_startup():
    """
    【起動時】テーブル作成(学習用)
    - 本番ではAlembic migration推奨
    """
    Base.metadata.create_all(bind=engine)

5. 動作イメージ(実行順)

  1. POST /auth/login:認証成功 → DBから user_settings を取得(無ければデフォルト)
  2. 取得した settings を request.session["user_settings"] に保存
  3. 次のリクエスト(例:GET /sample/paging)開始時に Middleware が動き、session → UserSettingsStore に復元
  4. 任意の層で store.get_int("paging.per_page", 20) のように参照できる

6. Laravelの完成形に相当する「参照方法」(FastAPI版)

# 例:どこでも(Router/Controller/Service)で
per_page = store.get_int("paging.per_page", 20)
theme    = store.get("theme", "light")

FastAPI + Python:/hello で Hello, World を返す(VS Code / venv / 1ファイル)

1) venv環境の作り方(Windows / VS Code ターミナル)

  1. 作業フォルダを作って VS Code で開く(例:C:\work\fastapi-hello
  2. VS Code のターミナルを開く(Terminal → New Terminal)
  3. venv を作成:
cd C:\work
mkdir fastapi-hello
cd fastapi-hello

python -m venv .venv

venv を有効化

# PowerShell の場合
.\.venv\Scripts\Activate.ps1

# cmd の場合
.\.venv\Scripts\activate.bat

成功するとプロンプト先頭に (.venv) が付きます。:contentReference[oaicite:0]{index=0}

2) FastAPI のインストール方法

python -m pip install --upgrade pip
pip install fastapi "uvicorn[standard]"

uvicorn は FastAPI アプリを起動するサーバー役です。:contentReference[oaicite:1]{index=1}

3) 1 pythonファイルでの実装(main.py)

作業フォルダ直下に main.py を作成して、以下を貼り付け:

    
from __future__ import annotations

import asyncio
import json
from pathlib import Path
from typing import Any, Dict, List

from fastapi import FastAPI, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, PlainTextResponse

app = FastAPI(
    title="Fetch Debug Mock API",
    version="2.0.0",
    description="Frontend fetch() debugging mock server (advanced)",
)

# =============================================================================
# CORS設定(開発用・完全許可モード)
# -----------------------------------------------------------------------------
# 目的:
#   - フロント側がどのホスト/どのポート/どのフレームワークで動いていても、
#     ほぼ追加設定なしで fetch() デバッグができるようにする。
#   - 例: React(3000), Vite(5173), Vue, Angular, 素HTML(80), 別PC(LAN), Docker など。
#
# 何が解決できるか:
#   - ブラウザのCORSブロックを回避し、"呼べているか/レスポンスが何か" を即確認できる。
#
# 注意(重要):
#   - allow_origins=["*"] は「全オリジン許可」。
#   - 仕様上 allow_credentials=True(Cookie送信許可)とは併用できない。
#   - Cookie/セッション認証の検証が必要なら、allow_origins を具体的なOriginに絞ること。
#   - 本設定は「ローカル開発・モック用途専用」。本番では使用しない。
# =============================================================================
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # どこからでもOK(開発用)
    allow_methods=["*"],  # GET/POST/PUT/DELETE/OPTIONS... 全部OK
    allow_headers=["*"],  # Content-Type, Authorization など任意ヘッダOK
)

# =============================================================================
# ローカルJSONファイルパス(main.py と同じフォルダに mock.json を置く)
# -----------------------------------------------------------------------------
# 目的:
#   - UI開発のための固定モックデータを、DBや外部API無しで返す。
#   - バックエンド未完成でもフロントを先に作れるようにする。
#
# 使い方:
#   - main.py と同じフォルダに mock.json を置く。
# =============================================================================
MOCK_JSON_FILE = Path(__file__).with_name("mock.json")


# =============================================================================
# GET /hello
# -----------------------------------------------------------------------------
# 目的:
#   - fetch() の最小疎通確認(textレスポンス確認)。
#   - まず「リクエストが届くか」「CORSが通るか」「text() で読めるか」を確認する。
#
# 入力:
#   - なし
#
# 出力:
#   - 200 OK
#   - Content-Type: text/plain
#   - Body: "Hello, World"
#
# フロント側の確認例:
#   fetch("http://127.0.0.1:8000/hello").then(r => r.text()).then(console.log);
# =============================================================================
@app.get("/hello", response_class=PlainTextResponse)
def hello() -> str:
    return "Hello, World"


# =============================================================================
# GET /hellojson
# -----------------------------------------------------------------------------
# 目的:
#   - fetch().then(r => r.json()) の動作確認。
#   - JSONレスポンスの最小形で、JSONパースとUI表示の流れを確認する。
#
# 入力:
#   - なし
#
# 出力:
#   - 200 OK
#   - Content-Type: application/json
#   - Body: {"message":"hello, world"}
#
# フロント側の確認例:
#   fetch("http://127.0.0.1:8000/hellojson").then(r => r.json()).then(console.log);
# =============================================================================
@app.get("/hellojson", response_class=JSONResponse)
def hello_json() -> Dict[str, str]:
    return {"message": "hello, world"}


# =============================================================================
# GET /helloerror
# -----------------------------------------------------------------------------
# 目的:
#   - フロント側のエラー処理をテストする。
#   - response.ok === false, status=500 の分岐、UI通知(トースト/ダイアログ)等を確認する。
#
# 入力:
#   - なし
#
# 出力:
#   - 500 Internal Server Error
#   - Content-Type: application/json
#   - Body: {"error":"..."}
#
# フロント側の確認例:
#   fetch("/helloerror").then(r => console.log(r.ok, r.status));
# =============================================================================
@app.get("/helloerror")
def hello_error() -> JSONResponse:
    return JSONResponse(
        status_code=500,
        content={"error": "Intentional server error for fetch debugging"},
    )


# =============================================================================
# GET /hellojsonfile
# -----------------------------------------------------------------------------
# 目的:
#   - 固定モックデータ(JSONファイル)を返すことで、UI開発をバックエンドから分離する。
#   - 「実データっぽい形」をファイルで管理でき、UI開発・デバッグが容易になる。
#
# 使用方法:
#   - main.py と同じフォルダに mock.json を置く。
#
# 正常時:
#   - 200 OK
#   - Content-Type: application/json
#   - Body: mock.json の内容をそのまま返却
#
# 異常時:
#   - mock.json が無い → 500 + ヒント
#   - mock.json が壊れている(JSONとして不正)→ 500 + どこが壊れているか
#
# 補足:
#   - ここでいう「解析しない」は「業務的意味づけの検証をしない」ことで、
#     JSONとしてロード(パース)し、その結果を返すこと自体は行う。
# =============================================================================
@app.get("/hellojsonfile")
def hello_json_file() -> JSONResponse:
    try:
        if not MOCK_JSON_FILE.exists():
            return JSONResponse(
                status_code=500,
                content={
                    "error": "mock.json not found",
                    "hint": f"Create {MOCK_JSON_FILE.name} next to main.py",
                },
            )

        data: Any = json.loads(MOCK_JSON_FILE.read_text(encoding="utf-8"))
        return JSONResponse(status_code=200, content=data)

    except json.JSONDecodeError as e:
        return JSONResponse(
            status_code=500,
            content={
                "error": "mock.json is not valid JSON",
                "detail": f"{e.msg} (line {e.lineno}, col {e.colno})",
            },
        )
    except Exception as e:
        return JSONResponse(
            status_code=500,
            content={"error": "unexpected error reading json file", "detail": str(e)},
        )


# =============================================================================
# POST /postjson
# -----------------------------------------------------------------------------
# 目的:
#   - fetch() POST のデバッグ用。
#   - 「JSONとして正しいか」だけを確認し、内容(フィールド/型/必須)などの検証はしない。
#   - フォーム送信やAPI呼び出しの基本動作確認に使う。
#
# 入力:
#   - Body: JSON(推奨)
#   - Header: Content-Type: application/json(推奨だが、厳密に強制はしない)
#
# 正常:
#   - JSONとしてパース可能 → 200 OK + {"ok": true}
#
# 異常:
#   - 空Body → 400 Bad Request
#   - JSONとして不正(壊れている / JSONではない)→ 400 Bad Request
#
# フロント側の確認例:
#   fetch("/postjson", {method:"POST", headers:{ "Content-Type":"application/json"}, body: JSON.stringify({a:1})})
# =============================================================================
@app.post("/postjson")
async def post_json(request: Request) -> JSONResponse:
    try:
        raw = await request.body()
        if not raw:
            return JSONResponse(status_code=400, content={"error": "empty body (expected JSON)"})

        # JSONとして読めるかのみ確認(構造の意味解釈はしない)
        _parsed: Any = await request.json()

        return JSONResponse(status_code=200, content={"ok": True})

    except Exception:
        return JSONResponse(status_code=400, content={"ok": False, "error": "invalid JSON"})


# =============================================================================
# GET /delay
# -----------------------------------------------------------------------------
# 目的:
#   - 通信遅延をシミュレートし、ローディング表示やタイムアウト設計を確認する。
#   - 「待機中のUI」「キャンセル」「二重送信防止」「スピナー」等のデバッグに使う。
#
# 入力:
#   - なし(固定で2秒遅延)
#
# 出力:
#   - 2秒待機後に 200 OK
#   - {"message":"delayed response","delaySeconds":2}
#
# フロント側の確認例:
#   setLoading(true); await fetch("/delay"); setLoading(false);
# =============================================================================
@app.get("/delay")
async def delay_response() -> JSONResponse:
    await asyncio.sleep(2)
    return JSONResponse(status_code=200, content={"message": "delayed response", "delaySeconds": 2})


# =============================================================================
# GET /items
# -----------------------------------------------------------------------------
# 目的:
#   - ページネーションUIの動作確認(一覧 + ページ送り)。
#   - page/size の扱い、総件数(total)、次ページ有無判定などをフロント側で確認する。
#
# クエリパラメータ:
#   - page: 1以上(例: /items?page=1)
#   - size: 1〜50(例: /items?size=10)
#
# データ仕様:
#   - total = 100 固定の疑似データを返す
#   - items は {"id": number, "name": string} の配列
#
# 出力例:
#   {
#     "page": 2,
#     "size": 10,
#     "total": 100,
#     "items": [ {id:11,name:"Item 11"}, ... {id:20,name:"Item 20"} ]
#   }
#
# 注意:
#   - pageが範囲外でも空配列を返す(UI側の境界処理テストに便利)
# =============================================================================
@app.get("/items")
def get_items(
    page: int = Query(1, ge=1, description="1-based page number"),
    size: int = Query(10, ge=1, le=50, description="page size (1..50)"),
) -> JSONResponse:
    total_items = 100
    all_items: List[Dict[str, Any]] = [{"id": i, "name": f"Item {i}"} for i in range(1, total_items + 1)]

    start = (page - 1) * size
    end = start + size
    paged_items = all_items[start:end]

    return JSONResponse(
        status_code=200,
        content={"page": page, "size": size, "total": total_items, "items": paged_items},
    )


# =============================================================================
# GET /unauthorized
# -----------------------------------------------------------------------------
# 目的:
#   - 401 Unauthorized のUI確認用。
#   - 「未ログイン」扱い時の挙動(ログイン画面へ誘導、トークン再取得、メッセージ表示等)を確認する。
#
# 典型的なフロント側処理:
#   - status === 401 → login画面へリダイレクト / refresh token 実行 / 再ログイン要求
#
# 出力:
#   - 401 Unauthorized
#   - {"error":"Unauthorized"}
# =============================================================================
@app.get("/unauthorized")
def unauthorized() -> JSONResponse:
    return JSONResponse(status_code=401, content={"error": "Unauthorized"})


# =============================================================================
# GET /forbidden
# -----------------------------------------------------------------------------
# 目的:
#   - 403 Forbidden のUI確認用。
#   - 「ログイン済みだが権限がない」ケースの挙動(権限不足表示、申請導線等)を確認する。
#
# 典型的なフロント側処理:
#   - status === 403 → 権限不足ページ/ダイアログ表示
#
# 出力:
#   - 403 Forbidden
#   - {"error":"Forbidden"}
# =============================================================================
@app.get("/forbidden")
def forbidden() -> JSONResponse:
    return JSONResponse(status_code=403, content={"error": "Forbidden"})


# =============================================================================
# GET /notfound
# -----------------------------------------------------------------------------
# 目的:
#   - 404 Not Found のUI確認用。
#   - API側で「存在しないリソース」を返した時の扱い(404ページ、空状態、再試行UI)を確認する。
#
# 注意:
#   - FastAPIの"存在しないパス"にアクセスした場合も 404 になるが、
#     それは「ルーティング不一致の404」。
#   - この /notfound は「APIは存在するが、意図的に404を返す」ため、
#     フロントのハンドリングを確実に再現できる。
#
# 出力:
#   - 404 Not Found
#   - {"error":"Resource not found"}
# =============================================================================
@app.get("/notfound")
def not_found() -> JSONResponse:
    return JSONResponse(status_code=404, content={"error": "Resource not found"})

# =============================================================================
# POST /echo
# -----------------------------------------------------------------------------
# 目的:
#   - fetch() の POST デバッグをさらに楽にする「エコーAPI」。
#   - フロントから送ったJSONが、
#       1) そのままサーバに届いているか
#       2) Content-Type や body の作り方が正しいか
#       3) 文字コードや改行、null/boolean/number の扱いが崩れていないか
#     を最短で確認できる。
#
# 典型的な使い方(フロント側):
#   - フォーム入力値を JSON.stringify() して送る
#   - 返ってきたJSONを画面に表示し、送信内容と一致しているか確認する
#
# 入力:
#   - Method: POST
#   - Body: JSON(必須)
#   - Header: Content-Type: application/json 推奨
#     ※ただし現場で Content-Type が揺れることもあるため、
#       本APIは「JSONとして解釈できるか」を優先して判定する。
#
# 正常時:
#   - JSONとしてパース可能な本文が来た場合
#   - 200 OK
#   - Body:
#       {
#         "ok": true,
#         "received": (送ったJSONをそのまま)
#       }
#
# 異常時:
#   - 空Body
#       400 Bad Request
#       {"ok": false, "error": "empty body (expected JSON)"}
#   - JSONとして不正(壊れている / JSONではない)
#       400 Bad Request
#       {"ok": false, "error": "invalid JSON"}
#
# 重要:
#   - このAPIは「JSONの中身の検証(必須項目/型/範囲など)」はしない。
#     あくまで fetch の疎通と、送信したJSONが正しく届いているかの確認用途。
# =============================================================================
@app.post("/echo")
async def echo_json(request: Request) -> JSONResponse:
    try:
        raw = await request.body()
        if not raw:
            return JSONResponse(
                status_code=400,
                content={"ok": False, "error": "empty body (expected JSON)"},
            )

        # JSONとしてパースできるかのみ確認(内容の意味解釈はしない)
        data: Any = await request.json()

        # 送られてきたJSONをそのまま返す(デバッグ用途)
        return JSONResponse(
            status_code=200,
            content={"ok": True, "received": data},
        )

    except Exception:
        return JSONResponse(
            status_code=400,
            content={"ok": False, "error": "invalid JSON"},
        )

mock.jsonの例

[
  { "id": 1,  "name": "Item 1",  "category": "A", "price": 1000, "inStock": true  },
  { "id": 2,  "name": "Item 2",  "category": "B", "price": 1100, "inStock": false },
  { "id": 3,  "name": "Item 3",  "category": "C", "price": 1200, "inStock": true  },
  { "id": 4,  "name": "Item 4",  "category": "A", "price": 1300, "inStock": true  },
  { "id": 5,  "name": "Item 5",  "category": "B", "price": 1400, "inStock": false },
  { "id": 6,  "name": "Item 6",  "category": "C", "price": 1500, "inStock": true  },
  { "id": 7,  "name": "Item 7",  "category": "A", "price": 1600, "inStock": true  },
  { "id": 8,  "name": "Item 8",  "category": "B", "price": 1700, "inStock": false },
  { "id": 9,  "name": "Item 9",  "category": "C", "price": 1800, "inStock": true  },
  { "id": 10, "name": "Item 10", "category": "A", "price": 1900, "inStock": true  },
  { "id": 11, "name": "Item 11", "category": "B", "price": 2000, "inStock": false },
  { "id": 12, "name": "Item 12", "category": "C", "price": 2100, "inStock": true  },
  { "id": 13, "name": "Item 13", "category": "A", "price": 2200, "inStock": true  },
  { "id": 14, "name": "Item 14", "category": "B", "price": 2300, "inStock": false },
  { "id": 15, "name": "Item 15", "category": "C", "price": 2400, "inStock": true  },
  { "id": 16, "name": "Item 16", "category": "A", "price": 2500, "inStock": true  },
  { "id": 17, "name": "Item 17", "category": "B", "price": 2600, "inStock": false },
  { "id": 18, "name": "Item 18", "category": "C", "price": 2700, "inStock": true  },
  { "id": 19, "name": "Item 19", "category": "A", "price": 2800, "inStock": true  },
  { "id": 20, "name": "Item 20", "category": "B", "price": 2900, "inStock": false }
]

4) テストサーバー起動(開発用)

uvicorn main:app --reload --host 127.0.0.1 --port 8000

(起動とブラウザ確認の流れはこの手順と同じです):contentReference[oaicite:2]{index=2}

5) ブラウザを用いた確認

(VS Code 補足)Interpreter が .venv になっているか

VS Code 右下(または Ctrl+Shift+P → “Python: Select Interpreter”)で .venv を選んでおくと、実行・補完・デバッグが安定します。:contentReference[oaicite:3]{index=3}