正在学习

app_旧版.py (1)

# app_legacy.py (1)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import sqlite3
from typing import List, Optional

app = FastAPI(title="Tasks Legacy Monolith", version="0.1.0")

DB_PATH = "tasks.db"

class TaskIn(BaseModel):
    title: str
    description: Optional[str] = ""
    completed: bool = False

class Task(TaskIn):
    id: int

def _ensure_db():
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute(
        "CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT, description TEXT, completed INTEGER)"
    )
    conn.commit()
    conn.close()

_ensure_db()

@app.get("/health")
def health():
    return {"status": "ok", "service": "legacy"}

@app.get("/tasks", response_model=List[Task])
def list_tasks():
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute("SELECT id, title, description, completed FROM tasks ORDER BY id")
    rows = cur.fetchall()
    conn.close()
    return [
        {"id": r[0], "title": r[1], "description": r[2], "completed": bool(r[3])}
        for r in rows
    ]

@app.post("/tasks", response_model=Task, status_code=201)
def create_task(payload: TaskIn):
    if not payload.title or not payload.title.strip():
        raise HTTPException(status_code=400, detail="Title required")
    if len(payload.title) > 120:
        raise HTTPException(status_code=400, detail="Title too long")
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute(
        "INSERT INTO tasks (title, description, completed) VALUES (?, ?, ?)",
        (payload.title.strip(), payload.description or "", int(payload.completed)),
    )
    conn.commit()
    task_id = cur.lastrowid
    cur.execute("SELECT id, title, description, completed FROM tasks WHERE id = ?", (task_id,))
    row = cur.fetchone()
    conn.close()
    return {"id": row[0], "title": row[1], "description": row[2], "completed": bool(row[3])}

@app.put("/tasks/{task_id}", response_model=Task)
def put_task(task_id: int, payload: TaskIn):
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute("SELECT id FROM tasks WHERE id = ?", (task_id,))
    if not cur.fetchone():
        conn.close()
        raise HTTPException(status_code=404, detail="not found")
    cur.execute(
        "UPDATE tasks SET title=?, description=?, completed=? WHERE id=?",
        (payload.title.strip(), payload.description or "", int(payload.completed), task_id),
    )
    conn.commit()
    cur.execute("SELECT id, title, description, completed FROM tasks WHERE id = ?", (task_id,))
    row = cur.fetchone()
    conn.close()
    return {"id": row[0], "title": row[1], "description": row[2], "completed": bool(row[3])}

@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
    conn.commit()
    conn.close()

运行它:

python -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate
pip install fastapi uvicorn
uvicorn app_legacy:app --reload

你现在有了一个基线。接下来,重构为分层结构。完全按照所示创建以下文件和文件夹。

app/
  __init__.py
  app.py
  database.py
  models.py
  repositories.py
  services.py
  routes.py
tests/
  test_tasks.py
requirements.txt

requirements.txt

fastapi==0.114.0
uvicorn==0.30.5

app/database.py

import sqlite3
from typing import Iterator

DB_PATH = "tasks.db"

def init_db() -> None:
    with sqlite3.connect(DB_PATH) as conn:
        cur = conn.cursor()
        cur.execute(
            "CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT, description TEXT, completed INTEGER)"
        )
        conn.commit()

def get_conn() -> Iterator[sqlite3.Connection]:
    return sqlite3.connect(DB_PATH)

app/models.py

from pydantic import BaseModel, Field
from typing import Optional

class TaskCreate(BaseModel):
    title: str = Field(min_length=1, max_length=120)
    description: Optional[str] = ""
    completed: bool = False

class Task(TaskCreate):
    id: int

class TaskUpdate(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    completed: Optional[bool] = None

app/repositories.py

import sqlite3
from typing import List, Optional, Dict, Any

class TaskRepository:
    def __init__(self, conn: sqlite3.Connection):
        self.conn = conn

    def list(self) -> List[Dict[str, Any]]:
        cur = self.conn.cursor()
        cur.execute("SELECT id, title, description, completed FROM tasks ORDER BY id")
        return [
            {"id": r[0], "title": r[1], "description": r[2], "completed": bool(r[3])}
            for r in cur.fetchall()
        ]

    def get(self, task_id: int) -> Optional[Dict[str, Any]]:
        cur = self.conn.cursor()
        cur.execute("SELECT id, title, description, completed FROM tasks WHERE id=?", (task_id,))
        r = cur.fetchone()
        if not r:
            return None
        return {"id": r[0], "title": r[1], "description": r[2], "completed": bool(r[3])}

    def create(self, title: str, description: str, completed: bool) -> Dict[str, Any]:
        cur = self.conn.cursor()
        cur.execute(
            "INSERT INTO tasks (title, description, completed) VALUES (?, ?, ?)",
            (title, description, int(completed)),
        )
        self.conn.commit()
        return self.get(cur.lastrowid)
def put(self, task_id: int, title: str, description: str, completed: bool) -> Optional[Dict[str, Any]]:
    cur = self.conn.cursor()
    cur.execute("UPDATE tasks SET title=?, description=?, completed=? WHERE id=?",
                (title, description, int(completed), task_id))
    self.conn.commit()
    return self.get(task_id)

def delete(self, task_id: int) -> None:
    cur = self.conn.cursor()
    cur.execute("DELETE FROM tasks WHERE id=?", (task_id,))
    self.conn.commit()

app/services.py

from typing import Dict, Any, Optional

from .repositories import TaskRepository
from .models import TaskCreate, TaskUpdate

class TaskService:
    def __init__(self, repo: TaskRepository):
        self.repo = repo

    def list_tasks(self):
        return self.repo.list()

    def create_task(self, payload: TaskCreate):
        title = payload.title.strip()
        desc = (payload.description or "").strip()
        return self.repo.create(title, desc, payload.completed)

    def get_task(self, task_id: int) -> Optional[Dict[str, Any]]:
        return self.repo.get(task_id)

    def put_task(self, task_id: int, payload: TaskCreate):
        title = payload.title.strip()
        desc = (payload.description or "").strip()
        return self.repo.put(task_id, title, desc, payload.completed)

    def patch_task(self, task_id: int, payload: TaskUpdate):
        current = self.repo.get(task_id)
        if not current:
            return None
        title = current["title"] if payload.title is None else payload.title.strip()
        desc = current["description"] if payload.description is None else (payload.description or "").strip()
        completed = current["completed"] if payload.completed is None else bool(payload.completed)
        return self.repo.put(task_id, title, desc, completed)

    def delete_task(self, task_id: int):
        self.repo.delete(task_id)

app/routes.py

from fastapi import APIRouter, HTTPException
from typing import List
from .models import Task, TaskCreate, TaskUpdate
from .services import TaskService

router = APIRouter()

def mount_routes(service: TaskService) -> APIRouter:
    @router.get("/health")
    def health():
        return {"status": "ok", "service": "refactored"}

    @router.get("/tasks", response_model=List[Task])
    def list_tasks():
        return service.list_tasks()

    @router.post("/tasks", response_model=Task, status_code=201)
    def create_task(payload: TaskCreate):
        return service.create_task(payload)

    @router.get("/tasks/{task_id}", response_model=Task)
    def get_task(task_id: int):
        task = service.get_task(task_id)
        if not task:
            raise HTTPException(status_code=404, detail="未找到")
        return task

    @router.put("/tasks/{task_id}", response_model=Task)
    def put_task(task_id: int, payload: TaskCreate):
        if not service.get_task(task_id):
            raise HTTPException(status_code=404, detail="未找到")
        return service.put_task(task_id, payload)

    @router.patch("/tasks/{task_id}", response_model=Task)
    def patch_task(task_id: int, payload: TaskUpdate):
        updated = service.patch_task(task_id, payload)
        if not updated:
            raise HTTPException(status_code=404, detail="未找到")
        return updated

    @router.delete("/tasks/{task_id}", status_code=204)
    def delete_task(task_id: int):
        if not service.get_task(task_id):
            raise HTTPException(status_code=404, detail="未找到")
        service.delete_task(task_id)

    return router

app/app.py

from fastapi import FastAPI
from .database import init_db, get_conn
from .repositories import TaskRepository
from .services import TaskService
from .routes import mount_routes

def create_app() -> FastAPI:
    init_db()
    app = FastAPI(title="任务重构版", version="1.0.0")

    # 简单的组合根
    conn = get_conn()
    repo = TaskRepository(conn)
    service = TaskService(repo)
    app.include_router(mount_routes(service))

    return app

app = create_app()

tests/test_tasks.py

from fastapi.testclient import TestClient
from app.app import app

client = TestClient(app)

def test_health():
    r = client.get("/health")
    assert r.status_code == 200
    assert r.json()["status"] == "ok"

def test_create_list_get_put_patch_delete():
    # 创建
    r = client.post("/tasks", json={"title": "Refactor", "description": "split layers", "completed": False})
    assert r.status_code == 201
    tid = r.json()["id"]

    # 列表
    r = client.get("/tasks")
    assert r.status_code == 200
    assert any(t["id"] == tid for t in r.json())

    # 获取
    r = client.get(f"/tasks/{tid}")
    assert r.status_code == 200
    assert r.json()["title"] == "Refactor"

    # 替换
    r = client.put(f"/tasks/{tid}", json={"title": "Refactor All", "description": "", "completed": True})
    assert r.status_code == 200
    assert r.json()["completed"] is True

    # 部分更新
    r = client.patch(f"/tasks/{tid}", json={"description": "done"})
    assert r.status_code == 200
    assert r.json()["description"] == "done"

    # 删除
    r = client.delete(f"/tasks/{tid}")
    assert r.status_code == 204

    # 删除后应为 404
    r = client.get(f"/tasks/{tid}")
    assert r.status_code == 404

运行重构后的服务和测试:

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt pytest
uvicorn app.app:app --reload

练习题

在旧版 FastAPI 应用程序中,_ensure_db() 函数的用途是什么?

A. 创建一个新任务
B. 确保数据库表存在
C. 连接到数据库
D. 删除一个任务

哪个端点负责返回遗留 FastAPI 应用程序的健康状态?

A. /tasks
B. /health
C. /create
D. /status

TaskIn 模型中的有效字段有哪些?

A. id
B. title
C. description
D. completed

以下哪些是旧版 FastAPI 应用程序端点中使用的有效 HTTP 方法?

A. GET
B. POST
C. PUT
D. DELETE

旧版 FastAPI 应用程序中的 create_task 端点会在 title 为空或超过 120 个字符时抛出 HTTPException。

在旧版 FastAPI 应用程序中的 delete_task 端点在删除任务后返回响应体。

数据库连接路径由变量 ___ 定义。

put_task 端点使用 ___ 方法更新任务的详细信息。

解释遗留 FastAPI 应用程序中 list_tasks 端点的用途。

在 FastAPI 应用的分层结构中,init_db() 函数的作用是什么?

以下哪项最能描述在分层结构中 get_conn() 函数的用途?

A. 执行 SQL 查询
B. 初始化数据库
C. 提供数据库连接迭代器
D. 提交对数据库的更改

将 FastAPI 应用程序重构为分层结构的主要好处是什么?(选择所有适用的)

A. 隔离数据访问
B. 将规则移至服务层
C. 将 I/O 保留在边界处
D. 引入最小化的启动路径

在重构后的分层结构中,哪个组件负责处理数据库连接细节和 SQL 查询?

A. routes.py 文件
B. repositories.py 中的 TaskRepository 类
C. services.py 文件
D. app.py 文件

在 FastAPI 应用程序中,将业务规则和验证检查移动到服务层有哪些好处?(选择所有适用的)

A. 使路由更简洁,更专注于 HTTP 处理
B. 便于对核心业务逻辑进行测试
C. 减少对数据库连接的需求
D. 允许在多个路由之间复用业务逻辑
E. 通过减少网络延迟来提升性能

在重构后的结构中,routes.py 文件应包含用于任务操作的所有 SQL 查询。

repositories.py 中的 _____ 类应用于封装所有任务相关的数据库操作,遵循数据访问隔离原则。

解释重构后的结构相比遗留的单体应用如何提高了可维护性。

登录后解锁笔记、知识点解析、AI 问答

立即登录