FastAPI で Task 管理アプリを作る
テクノロジ系 / L18 app-task-sqlite — Task 管理アプリ (SQLite)
FastAPI で Task 管理アプリを作る
データの置き場を「ファイル」から「データベース」に変える
作るもの: Task 管理アプリ
「やることを登録して、終わったらチェックを付ける」自分専用の ToDo サーバー。
データは SQLite データベース に保存する。
ユーザー (=自分) Task アプリ (これから作るもの)
┌──────────────────────────┐
📋 一覧を見る ──→ │ GET /tasks │
✏️ 登録する ──→ │ POST /tasks │
📂 1件開く ──→ │ GET /tasks/{id} │
✓ 完了にする ──→ │ PATCH /tasks/{id} │
🗑 消す ──→ │ DELETE /tasks/{id} │
🔍 完了/未完で絞る ──→ │ GET /tasks?done=… │
└──────────────────────────┘
データは tasks.db (SQLite) に保存この教材のテーマ: なぜ「ファイル」をやめるのか
前回は、データを JSON ファイル に保存した。それで動く。
でも、人数や件数が増えると、ファイル保存には限界が出てくる。
| ファイル保存 (JSON) の困りごと | 何が起きるか |
|---|---|
| 🔁 毎回ファイル全体を読み書き | 件数が増えると重い |
| 👥 2人が同時に書き込む | 片方の変更が消える |
| 🔍 「完了したものだけ」を探す | 全件ループして自分で絞り込む必要がある |
| 🔢 id の採番を自分で管理 | max + 1 のような処理を毎回書く |
→ これらを データベース (DB) が肩代わりしてくれる。
ここでは SQLite という最小の DB を使う。
SQLite を選ぶ理由
| 説明 | |
|---|---|
| 📦 インストール不要 | Python に最初から入っている (import sqlite3)。環境構築なし |
| 📄 ファイル 1 個 | tasks.db というファイル 1 つが DB 本体。持ち運べる |
| 🗄 本物の DB | SELECT / INSERT / UPDATE / DELETE という SQL で操作する |
| 🚀 学びが移植できる | ここで覚えた SQL は PostgreSQL / MySQL でもほぼ同じ |
> この教材は SQL の文法を体系的に学ぶものではない。
> 「FastAPI から DB を使うと、こう書ける」を体験する。SQL 文は資料に全部載っている。
Task アプリの 5 つの機能
これから 5 つの STEP で、順に機能を足していく。
| 機能 | ユーザーから見ると | STEP |
|---|---|---|
| 📋 一覧を見る | やることリストを開く | 1 |
| ✏️ 登録する | 新しいタスクを足す | 2 |
| 📂 1 件開く・消す | 特定のタスクを見る / 消す | 3 |
| ✓ 完了にする・直す | チェックを付ける / 名前を直す | 4 |
| 🔍 完了/未完で絞る | 終わったものだけ表示する | 5 |
進め方
1 つ前の STEP で書いたコードに、次の STEP で機能を足していく。最後には 1 つの動く Task アプリが手元に残る。
| STEP | 作る機能 | 裏で学ぶ FastAPI + SQL の道具 |
|---|---|---|
| 1 | サーバー起動 + DB とテーブルを用意 | sqlite3 接続 / CREATE TABLE / SELECT |
| 2 | タスクを登録する | POST / Pydantic / INSERT / DB 採番 |
| 3 | 1 件開く・消す | パスパラ / SELECT WHERE / 404 / DELETE |
| 4 | 完了にする・直す | PATCH / UPDATE / 部分更新 |
| 5 | 完了/未完で絞る | クエリパラメータ / WHERE done=? |
詰まったら starter / answer を使う
各 STEP には 2 種類の参考コードが用意されている:
| 種類 | いつ使う |
|---|---|
stepN/starter/main.py | STEP N に入る直前の状態。途中で追いつけなくなったらこれをコピーして再スタート |
stepN/answer/main.py | STEP N を完了した状態。自分のコードがおかしくなったらここを見て答え合わせ |
📌 step2/starter = step1/answer と同じ内容。
STEP を 1 つ進めるごとに「前のSTEPの完成形」=「次のSTEPのスタート地点」になる。
→ 完走できれば理想だが、詰まった時間で全体を諦めないこと。
starter を使って次のSTEPに進めば、機能の積み上げは体験できる。
最終ゴール: Swagger UI に並ぶ 5 つのエンドポイント
最終的にブラウザの /docs で以下の 5 つが並ぶ:
GET 一覧 / POST 作成 / GET 1件 / PATCH 更新 / DELETE 削除 = CRUD すべて。
最終ゴール: 完了タスクだけ取り出してみる
GET /tasks?done=true を Try it out → Execute すると、完了済みのタスクだけが JSON で返る:
「完了したものだけ」の絞り込みを、WHERE done = 1 という SQL 1 行で DB に任せられる。
STEP 1
📋 タスク一覧サーバーを起動する
サーバーを立てて、データの置き場 (DB とテーブル) を用意する
この機能があると何が嬉しい?
| 🚀 | サーバーが立つ = アプリの土台ができる |
|---|---|
| 🗄 | DB とテーブルが用意される = データの「置き場所」が決まる |
| 🛠 | Swagger UI が自動で出る → 動作確認に curl すらいらない |
| 🧪 | 中身が空でも「サーバーと DB は生きてる」と確認できる |
土台ができたら、後は「機能を足していく」だけになる。
このSTEPで作るもの
Task アプリの「📋 一覧を開く」機能の最小形
- サーバーが起動する
- 起動時に SQLite に
tasksテーブルを用意する - 一覧を見にいくと「まだ何も無い (空っぽ)」と返す
ユーザー Task アプリ SQLite (tasks.db) 📋 一覧を見る ──→ GET /tasks ──→ SELECT * FROM tasks ──→ [] (空)
「DB に問い合わせて、結果を JSON で返す」を一番小さい形で体験する。
DB に接続する関数を作る
import sqlite3
DB_FILE = "tasks.db"
def get_conn() -> sqlite3.Connection:
conn = sqlite3.connect(DB_FILE)
conn.row_factory = sqlite3.Row # 行を dict のように扱えるようにする
return conn| 部分 | 意味 |
|---|---|
sqlite3.connect("tasks.db") | tasks.db ファイルに繋ぐ (無ければ自動で作られる) |
conn.row_factory = sqlite3.Row | 取り出した行を row["title"] のように名前で読める形にする |
sqlite3 は Python に最初から入っている。pip install は不要。
テーブルを用意する
DB の中に「タスクを入れる箱 = テーブル」を作る。これは起動時に 1 回だけ。
def init_db() -> None:
conn = get_conn()
conn.execute(
"""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
)
"""
)
conn.commit()
conn.close()
init_db() # ← サーバ起動時に呼ぶCREATE TABLE の中身を読む
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
)| カラム | 意味 |
|---|---|
IF NOT EXISTS | すでにあれば作り直さない (2回目以降の起動でも安全) |
id ... AUTOINCREMENT | DB が自動で 1, 2, 3... と採番する (自分で max+1 しなくていい) |
title TEXT NOT NULL | 文字列。空 (NULL) は許さない |
done INTEGER ... DEFAULT 0 | 完了フラグ。SQLite に真偽値型は無いので 0/1 の数値 で表す。初期値 0 (未完了) |
created_at TEXT NOT NULL | 作成日時を文字列で持つ |
一覧を返す: SELECT
from fastapi import FastAPI
app = FastAPI(title="Task API")
@app.get("/tasks")
def list_tasks():
conn = get_conn()
rows = conn.execute("SELECT * FROM tasks").fetchall()
conn.close()
return [dict(row) for row in rows]| 部分 | 意味 |
|---|---|
SELECT * FROM tasks | tasks テーブルの全行を取り出す SQL |
.fetchall() | 結果を全部リストで受け取る |
dict(row) | 1 行を {"id":1, "title":"...", ...} の辞書に変換 |
辞書のリストを return すると、そのまま JSON 配列になる。
サーバーを起動して確認
uvicorn main:app --reload
| URL | 何が見える |
|---|---|
| http://localhost:8000/tasks | [] (まだ空。でも DB とテーブルはできている) |
| http://localhost:8000/docs | Swagger UI (操作画面) |
ターミナルで確認:
ls # tasks.db ができている
tasks.db が出現していれば、DB の用意は成功。
演習 1-1
main.py を作って、以下の状態まで持っていく:
- [ ]
get_conn()とinit_db()を書いた - [ ]
uvicorn main:app --reloadで起動する - [ ]
tasks.dbがフォルダにできている - [ ] http://localhost:8000/tasks が
[]を返す - [ ] Swagger UI (
/docs) でGET /tasksを Try it out できる
詰まったら:
ModuleNotFoundError: fastapi→pip install fastapi uvicorn(sqlite3 は不要、標準装備)[Errno 48] Address already in use→ 古いサーバーが残っている。Ctrl+C
STEP1 まとめ
Task アプリの「📋 一覧を開く」機能と、DB の土台ができた。
| 学んだ道具 | コード |
|---|---|
| DB に接続 | sqlite3.connect("tasks.db") |
| 名前で読める行 | conn.row_factory = sqlite3.Row |
| テーブル作成 | CREATE TABLE IF NOT EXISTS tasks (...) |
| 全件取得 | SELECT * FROM tasks |
| 行→辞書 | dict(row) |
STEP1 完成時の Swagger UI
http://localhost:8000/docs を開くとこの画面になっていれば OK。
→ 次は ✏️ タスクを登録する 機能を作る
STEP 2
✏️ タスクを登録する
「やることを送ったら、DB が 1 行として覚えてくれる」
この機能があると何が嬉しい?
| ✏️ | やることをその場で登録できる |
|---|---|
| 🔢 | id は DB が自動で振る = max+1 を自分で書かなくていい |
| 🤖 | done (未完了) と作成日時はサーバー側が勝手に決める |
| 🛡 | title を忘れたら「title が無い」と即 422 で教えてくれる |
| 📋 | Swagger UI に入力欄が自動で出る = 試し書きが超ラク |
ユーザーは やること だけ書けば良い。id 採番という面倒を DB が引き受ける。
このSTEPで作るもの
Task アプリの「✏️ 登録する」機能
ユーザー Task アプリ SQLite ✏️ "牛乳を買う" ──→ POST /tasks ──→ INSERT INTO tasks ... ──→ 1行追加 📋 一覧を見る ──→ GET /tasks ──→ SELECT * FROM tasks ──→ ["牛乳を買う"]
ユーザーが送るのは title だけ。
id・done・created_at はサーバーと DB が決める。
POST /tasks {"title": "牛乳を買う"}
→ {"id": 1, "title": "牛乳を買う", "done": 0,
"created_at": "2026-06-08T09:00:00"}ボディを受け取る Pydantic モデル
from pydantic import BaseModel
class TaskCreate(BaseModel):
title: str # 必須。これだけ受け取る前回と同じ。ユーザーから受け取るのは title だけ。
id・done・created_at は 受け取らず、サーバ側で決める (API 設計の定石)。
INSERT 文で 1 行追加する
from datetime import datetime
@app.post("/tasks", status_code=201)
def create_task(payload: TaskCreate):
created_at = datetime.now().isoformat(timespec="seconds")
conn = get_conn()
cur = conn.execute(
"INSERT INTO tasks (title, created_at) VALUES (?, ?)",
(payload.title, created_at),
)
conn.commit()
new_id = cur.lastrowid # ← DB が振った id
row = conn.execute(
"SELECT * FROM tasks WHERE id = ?", (new_id,)
).fetchone()
conn.close()
return dict(row)INSERT 文の 4 つのポイント
conn.execute(
"INSERT INTO tasks (title, created_at) VALUES (?, ?)",
(payload.title, created_at),
)| ポイント | 説明 |
|---|---|
(title, created_at) だけ指定 | id は AUTOINCREMENT、done は DEFAULT 0 なので書かなくていい |
VALUES (?, ?) の ? | プレースホルダ。値を直接埋め込まず ? で受ける |
(payload.title, created_at) | ? に入る値をタプルで渡す |
conn.commit() | これを忘れると保存されない。変更を確定する合図 |
> なぜ ? を使う? → 文字列を直接 SQL に埋めると危険 (SQL インジェクション)。
> ? で渡すと SQLite が安全に処理してくれる。必ず ? を使う。
登録直後のデータを返す
cur = conn.execute("INSERT ...", (...))
conn.commit()
new_id = cur.lastrowid # 今 INSERT した行の id
row = conn.execute("SELECT * FROM tasks WHERE id = ?", (new_id,)).fetchone()
return dict(row)| 部分 | 意味 |
|---|---|
cur.lastrowid | 直前に INSERT した行の id を DB が教えてくれる |
.fetchone() | 1 行だけ取り出す (一覧の fetchall と対) |
登録したタスクを「id・done・created_at 付き」で返すと、ユーザーが結果を確認できる。
型違反 (422 を観察する)
POST /tasks のボディを わざと壊して 送ってみる:
{} // ← title が無い→ 422 Unprocessable Entity:
{
"detail": [{
"loc": ["body", "title"],
"msg": "Field required",
"type": "missing"
}]
}Pydantic が「title が必須なのに無い」と自動で弾く。自分でチェックを書かなくていい。
演習 2-1
main.py を以下まで持っていく:
- [ ]
TaskCreateモデルを定義した (title: str) - [ ]
POST /tasksでタスクを登録できる (id=1, 2, 3... と DB が採番) - [ ]
done=0とcreated_atがサーバ側で自動付与される - [ ]
GET /tasksで登録したタスクが返る - [ ] Swagger UI で
titleを抜くと 422 が返ることを確認した
時間が余ったら: 2件登録して、id が自動で 1→2 になることを確認する
STEP2 まとめ
Task アプリの「✏️ 登録」と「📋 一覧」が DB 経由で繋がった。
| 学んだ道具 | コード |
|---|---|
| Pydantic モデル | class TaskCreate(BaseModel): |
| 行を追加 | INSERT INTO tasks (...) VALUES (?, ?) |
| 安全な値渡し | プレースホルダ ? |
| 変更を確定 | conn.commit() |
| DB が振った id | cur.lastrowid |
STEP2 完成時の Swagger UI
POST /tasks が追加された。クリックして展開すると入力欄が自動で出る。
下に TaskCreate という Schemas が出ているのも自動生成 = Pydantic で型定義した恩恵。
→ 次は 📂 1 件開く・消す 機能を作る
STEP 3
📂 1 件開く・消す
「特定のタスクだけ開いて、要らなくなったら消す」
この機能があると何が嬉しい?
| 📂 | 一覧から「1件だけ」を取り出せる |
|---|---|
| 🗑 | 終わって不要になったタスクを消せる |
| ❓ | 存在しないタスクを取りに行くと「無い」と教えてくれる (404) |
| 🎯 | WHERE id = ? で DB が目的の 1 行を探してくれる (自分でループしない) |
このSTEPで作るもの
Task アプリの「📂 1件ずつ操作する」2 機能
ユーザー Task アプリ SQLite 📂 1番を開く ──→ GET /tasks/1 ──→ SELECT ... WHERE id=1 ❓ 999番を開く ──→ GET /tasks/999 ──→ 該当なし → 404 🗑 1番を消す ──→ DELETE /tasks/1 ──→ DELETE ... WHERE id=1
パスパラメータ {task_id} で「何番のタスク?」を受け取り、SQL の WHERE id = ? に渡す。
パスパラメータ + SELECT WHERE
@app.get("/tasks/{task_id}") # ← {task_id} が変数
def get_task(task_id: int): # ← 型ヒントで自動変換
conn = get_conn()
row = conn.execute(
"SELECT * FROM tasks WHERE id = ?", (task_id,)
).fetchone()
conn.close()
return dict(row) # ← まだ仮実装 (404 はこの後)| URL | 何が起こる |
|---|---|
GET /tasks/1 | task_id = 1 → WHERE id = 1 の行を取得 |
GET /tasks/abc | 422 (int に変換できない) |
{task_id} の 波括弧の中の名前 と 関数引数の名前 を一致させる。
404 を正しく返す: 探すヘルパー
「無いタスクを取りに行ったら 404 で教える」。GET も DELETE も共通なので ヘルパー関数にする。
from fastapi import FastAPI, HTTPException
def _find(conn: sqlite3.Connection, task_id: int) -> sqlite3.Row:
row = conn.execute(
"SELECT * FROM tasks WHERE id = ?", (task_id,)
).fetchone()
if row is None: # ← .fetchone() は無ければ None を返す
conn.close()
raise HTTPException(status_code=404, detail="task not found")
return rowfetchone() が None を返したら「その id は無い」。そこで 404 を投げる。
GET 1件を仕上げる
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
conn = get_conn()
row = _find(conn, task_id) # 無ければここで 404
conn.close()
return dict(row)3行で書けるようになった。重複処理を関数化 するのは前回 Memo でもやった作法。
🗑 タスクを消す: DELETE 文
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
conn = get_conn()
_find(conn, task_id) # 無ければ 404
conn.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
conn.commit() # ← 削除も commit が必要
conn.close()
return None # 204 は body 無しの慣習| 部分 | 意味 |
|---|---|
DELETE FROM tasks WHERE id = ? | 指定 id の行を消す SQL |
conn.commit() | INSERT と同じく、変更を確定する |
status_code=204 | No Content (削除成功の慣習) |
JSON ファイル時代の「リスト内包表記で作り直し」は不要。SQL 1 行で消える。
演習 3-1
- [ ]
_findヘルパー関数を作った (無ければ 404) - [ ]
GET /tasks/{task_id}で 1件取れる / 存在しないと 404 - [ ]
DELETE /tasks/{task_id}で消える / 存在しないと 404 - [ ] Swagger UI で全部のメソッドを試した
時間が余ったら:
/tasks/abc(文字列) を GET → 422 になることを確認- 消した後にもう一度
GET /tasks/1→ 404 になることを確認
STEP3 まとめ
Task アプリの「📂 開く」と「🗑 消す」が揃った。
| 学んだ道具 | コード |
|---|---|
| パスパラメータ | @app.get("/tasks/{task_id}") + task_id: int |
| 1行を探す | SELECT * FROM tasks WHERE id = ? + .fetchone() |
| 無い判定 | if row is None: |
| 404 を返す | raise HTTPException(status_code=404, ...) |
| 1行を消す | DELETE FROM tasks WHERE id = ? |
STEP3 完成時の Swagger UI
GET一覧 / POST / GET 1件 / DELETE が並ぶ。
→ 次は ✓ 完了にする・直す 機能を作る
STEP 4
✓ 完了にする・直す
「終わったタスクにチェックを付ける / タイトルを直す」
この機能があると何が嬉しい?
| ✓ | 終わったタスクに「完了」を付けられる = ToDo として機能する |
|---|---|
| ✏️ | タイトルのタイポを後から直せる |
| 🎯 | 「送ったキーだけ更新」= 触っていない項目を誤って消さない |
| 🗄 | UPDATE ... WHERE id=? で DB が該当行だけ書き換える |
「完了にできる」ことで、ただの一覧が やることリスト になる。
このSTEPで作るもの
Task アプリの「✓ 完了にする・直す」機能
ユーザー Task アプリ SQLite ✓ 1番を完了に ──→ PATCH /tasks/1 ──→ UPDATE tasks SET done=1 WHERE id=1 ✏️ 1番の名前を直す ─→ PATCH /tasks/1 ──→ UPDATE tasks SET title=? WHERE id=1
PATCH は 部分更新。送ったキーだけ書き換える。
title を送らなければ title はそのまま。done を送らなければ done はそのまま。
部分更新用の Pydantic モデル
class TaskUpdate(BaseModel):
title: str | None = None # 全部 Optional
done: bool | None = None前回 Memo の MemoUpdate と同じパターン。全フィールドを省略可能にする。
done は受け取りは bool (true/false)、DB へは 0/1 で保存する。
UPDATE 文で「送られたキーだけ」書き換える
@app.patch("/tasks/{task_id}")
def update_task(task_id: int, payload: TaskUpdate):
conn = get_conn()
_find(conn, task_id) # 無ければ 404
fields = payload.model_dump(exclude_unset=True) # 送られたキーだけ
if "title" in fields:
conn.execute("UPDATE tasks SET title = ? WHERE id = ?",
(fields["title"], task_id))
if "done" in fields:
conn.execute("UPDATE tasks SET done = ? WHERE id = ?",
(int(fields["done"]), task_id)) # True→1 / False→0
conn.commit()
row = conn.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)).fetchone()
conn.close()
return dict(row)このコードの 3 つのポイント
| ポイント | 説明 |
|---|---|
exclude_unset=True | 送られたキーだけ 取り出す (前回 Memo と同じ)。送っていない項目は触らない |
if "title" in fields: | 送られたものだけ UPDATE する。if で分けるとカラム名を安全に固定できる |
int(fields["done"]) | True/False を 1/0 に変換。SQLite の done は数値カラム |
> なぜ if で 1 つずつ分ける? → SQL のカラム名を変数で組み立てると危険。
> 「title なら title の UPDATE」と固定で書くのが安全。
PATCH の動作例
{"id":1, "title":"牛乳を買う", "done":0} がある状態で:
PATCH /tasks/1 {"done": true}
→ {"id":1, "title":"牛乳を買う", "done":1, ...}title は送っていないので元のまま。done だけ 0→1 になる。
PATCH /tasks/1 {"title": "低脂肪牛乳を買う"}
→ {"id":1, "title":"低脂肪牛乳を買う", "done":1, ...}done は送っていないので元のまま (1 を維持)。
演習 4-1
- [ ]
TaskUpdateモデルを定義した (title,doneどちらも Optional) - [ ]
PATCH /tasks/{task_id}でdoneだけ送って、titleが変わらないことを確認 - [ ]
done: trueを送ると DB に1が入ることを確認 - [ ]
PATCH /tasks/999→ 404 が返ることを確認 - [ ] Swagger UI で動作確認した
時間が余ったら: title と done を同時に送って、両方変わることを確認
STEP4 まとめ
Task アプリに「✓ 完了にする・直す」が加わり、CRUD が揃った。
| 学んだ道具 | コード |
|---|---|
| 部分更新モデル | class TaskUpdate(BaseModel): (全部 Optional) |
| 送られたキーだけ | payload.model_dump(exclude_unset=True) |
| 行を書き換え | UPDATE tasks SET title = ? WHERE id = ? |
| bool→数値 | int(fields["done"]) |
STEP4 完成時の Swagger UI
CRUD 5 メソッド (GET一覧 / POST / GET 1件 / PATCH / DELETE) が並ぶ。
→ 次は 🔍 完了/未完で絞る 機能を作る
STEP 5
🔍 完了/未完で絞る
「終わったタスクだけ」「まだのタスクだけ」を切り替えて見る
この機能があると何が嬉しい?
| 🔍 | ?done=true で完了したものだけ表示できる |
|---|---|
| 📋 | ?done=false でまだ終わっていないものだけ表示できる |
| ⚡ | 絞り込みを DB に任せられる (WHERE done = ? の 1 行) |
| 🆚 | JSON ファイルなら全件ループして自分で絞った。SQL なら DB がやる |
これが「ファイルをやめて DB にした」一番分かりやすい恩恵。
このSTEPで作るもの
Task アプリの「🔍 絞り込む」機能
ユーザー Task アプリ SQLite 🔍 完了だけ見たい ──→ GET /tasks?done=true ──→ WHERE done = 1 🔍 未完だけ見たい ──→ GET /tasks?done=false ──→ WHERE done = 0 📋 全部見たい ──→ GET /tasks ──→ WHERE なし (全件)
クエリパラメータで SQL を切り替える
@app.get("/tasks")
def list_tasks(done: bool | None = None):
conn = get_conn()
if done is None: # 指定なし → 全件
rows = conn.execute("SELECT * FROM tasks").fetchall()
else: # 指定あり → 絞り込み
rows = conn.execute(
"SELECT * FROM tasks WHERE done = ?", (int(done),)
).fetchall()
conn.close()
return [dict(row) for row in rows]| URL | done の値 | 実行される SQL |
|---|---|---|
/tasks | None | SELECT * FROM tasks |
/tasks?done=true | True → 1 | ... WHERE done = 1 |
/tasks?done=false | False → 0 | ... WHERE done = 0 |
クエリパラメータの基本 (復習)
def list_tasks(done: bool | None = None):
- パスに
{}で出てこない引数は自動でクエリパラメータ扱い (前回 Memo と同じ) = Noneを付けると省略可能になる- 型を
boolにすると?done=true/?done=falseを自動で True/False に変換
bool → DB の数値カラムに渡すときは int(done) で 1/0 に直す (PATCH と同じ考え方)。
JSON ファイル時代との比較
| JSON ファイル (前回) | SQLite (今回) | |
|---|---|---|
| 絞り込み | [t for t in tasks if t["done"]] を自分で書く | WHERE done = ? を DB に任せる |
| 件数が多い時 | 全件メモリに読んでループ | DB が効率よく探す |
| id 採番 | max + 1 を自分で | AUTOINCREMENT で DB が |
| データの安全 | 同時書き込みで壊れることも | DB がある程度守ってくれる |
> 小さいアプリなら JSON で十分。だが「増えても困らない」のは DB の強み。
演習 5-1
- [ ]
GET /tasksにdoneクエリパラメータを追加 (done: bool | None = None) - [ ]
?done=trueで完了タスクだけ返る - [ ]
?done=falseで未完了タスクだけ返る - [ ]
/tasks(指定なし) で全件返る - [ ] Swagger UI で
doneの入力欄が自動で出ることを確認
時間が余ったら: タスクを数件作り、いくつか完了にして、3 パターン (全件 / 完了 / 未完) を試す
完成形チェック: Task アプリで何ができるか
| 機能 | エンドポイント | 使う SQL |
|---|---|---|
| 📋 一覧を見る | GET /tasks | SELECT * |
| ✏️ 登録する | POST /tasks body: {title} | INSERT |
| 📂 1件開く | GET /tasks/{id} | SELECT ... WHERE id=? |
| ✓ 完了/直す | PATCH /tasks/{id} body: {title?, done?} | UPDATE |
| 🗑 消す | DELETE /tasks/{id} | DELETE |
| 🔍 絞り込む | GET /tasks?done=… | WHERE done=? |
データは tasks.db に保存され、CRUD すべてが SQL で動く Task アプリ。
STEP5 完成時の Swagger UI
GET /tasks を展開すると、done のクエリパラメータ入力欄が自動で出る。
Swagger UI から直接「done=true」を入れて Try it out できる。
チャレンジ (任意)
余裕があれば取り組む追加題
チャレンジ題 (より便利な Task アプリにする)
好きなだけ手を伸ばす。「自分が欲しい使い心地」を実装する場。
| アイデア | ユーザーが嬉しいこと | ヒント |
|---|---|---|
| 🔍 タイトルで検索 | 「牛乳ってタスクどこ?」 | WHERE title LIKE ? (%牛乳%) |
| 📅 新しい順に並べる | 直近作ったものを上に | ORDER BY created_at DESC |
| 🧹 完了済みを一括削除 | 終わったものをまとめて消す | DELETE FROM tasks WHERE done = 1 |
| 📊 件数を数える | 残りいくつ? | SELECT COUNT(*) FROM tasks WHERE done = 0 |
| 📝 期限を足す | いつまでにやる? | テーブルに due_date カラムを追加 |
SQL 文・パラメータ名は自由に決めてOK。動けば正解。
FAQ
| 質問 | 回答 |
|---|---|
tasks.db はどこにできる? | uvicorn を実行したディレクトリ |
tasks.db を消すとどうなる? | データが全部消える。次の起動で空のテーブルが再作成される |
done が 0/1 で返るのが気になる | SQLite に真偽値型が無いため。0=未完 / 1=完了 |
commit() を忘れると? | 保存されない。INSERT/UPDATE/DELETE の後は必ず commit |
なぜ ? を使う? | 値を直接 SQL に埋めると SQL インジェクションの危険。? で安全に渡す |
| 422 が出る | リクエストの型違反。Swagger UI で再現させて確認 |
振り返り
「Task アプリのこの機能を作るために、この FastAPI + SQL の道具を使った」を一覧で:
| Task アプリの機能 | 使った道具 | STEP |
|---|---|---|
| 📋 一覧を見る | @app.get + SELECT * + テーブル作成 | 1 |
| ✏️ 登録する | @app.post + Pydantic + INSERT | 2 |
| 📂 1件開く | パスパラメータ + SELECT WHERE | 3 |
| ❓ 無いと伝える | fetchone() が None → HTTPException(404) | 3 |
| 🗑 消す | @app.delete + DELETE WHERE | 3 |
| ✓ 完了/直す | @app.patch + UPDATE + exclude_unset | 4 |
| 🔍 絞り込む | クエリパラメータ + WHERE done=? | 5 |
前回 (JSON ファイル) からの進化
| 前回 Memo (JSON) | 今回 Task (SQLite) | |
|---|---|---|
| データの置き場 | memos.json | tasks.db (DB) |
| 保存 | json.dump でファイル全体を書く | INSERT/UPDATE/DELETE で 1 行ずつ |
| id 採番 | max + 1 を自分で | AUTOINCREMENT で DB が |
| 絞り込み | リスト内包表記で自分で | WHERE で DB が |
API の形 (GET/POST/PATCH/DELETE) は同じ。中身の「保存方法」が進化した。
これが「FastAPI + DB」の実務の入り口。