在做 TodoApp(待办事项)项目时,你是不是有过这种困惑:
“一个用户有多个待办,两个表怎么关联?”
“查询用户的待办时,怎么精准过滤,不拿到别人的数据?”
很多新手会卡在 “多表关联” 这一步,要么数据混乱,要么查询报错,这些坑其实都能避免。
今天就把 “用户表 - 待办表” 的一对多关系设计讲透,附上修正后的代码,直接复制就能跑!
核心需求
接下来,用户表使用 “Users” 表示,待办事项表使用 “Todos” 表示。
需求描述:
- 一个用户可以有多个待办事项,一个待办事项只能属于一个用户(即 “一对多” 关系)。
- 密码不能明文存储,防止数据库泄露;
- 创建用户后,数据要永久保存到数据库;
- 用户能通过用户名和密码,进行登录认证。
新手常见错误「重点」:
- 外键没设对:Todos 表没关联 Users 表主键,导致 “一个用户多个待办” 变成空谈,数据混乱;
- 明文存密码:黑客攻破数据库后,用户账号直接被盗,面临合规风险;
- 数据存储错误:创建用户时没调用
db.add()和db.commit(),数据只停留在内存,重启就丢; - 认证函数错误:数据库存的是
hashed_password,但使用user.password对比密码,永远认证失败;
数据库中一对多关系与外键关联
如何正确理解一对多关系?
让我们回到基本面。在TodoApp 应用中,关系很简单:
- 一个用户 → 多个待办事项
- 一个待办事项 → 只能属于一个用户
01、先明确表结构
Users 表(用户表)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id(PK) | Integer | 主键,自增 |
| String(unique=True) | 邮箱,唯一约束 | |
| username | String(unique=True) | 用户名,唯一约束 |
| first_name | String | 名 |
| last_name | String | 姓 |
| hashed_password | String | 加密后的密码 |
| is_active | Boolean | 是否活跃,默认 True |
| role | String | 角色(管理员 / 普通用户 |
Todos 表(待办表)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id(PK) | Integer | 主键,自增 |
| title | String | 待办标题 |
| description | String | 待办描述 |
| priority | Integer | 优先级(1-5) |
| complete | Boolean | 是否完成,默认 False |
| owner(FK) | Integer | 关联 Users 表的 id |
02、代码实现
第一步、配置数据库(database.py)
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
# 规范URL格式,数据库名改为todosapp.db(避免混淆)
SQLALCHEMY_DATABASE_URL = 'sqlite:///./todosapp.db'
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
第二步、定义数据模型(models.py)
from database import Base
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
# 定义用户表
class Users(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True)
username = Column(String, unique=True)
first_name = Column(String)
last_name = Column(String)
hashed_password = Column(String) # 重要:存储哈希值而非明文密码
is_active = Column(Boolean, default=True)
role = Column(String)
# 定义待办事项表
class Todos(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String)
description = Column(String)
priority = Column(Integer)
complete = Column(Boolean, default=False)
# 外键关联 Users 表的 id,明确约束
owner_id = Column(Integer, ForeignKey("users.id"))
关键设计
- 使用 owner_id 字段作为外键(Foreign Key)指向用户表的主键
- 通过这种关联,每个待办事项明确归属于特定用户
- 数据库层级实现数据隔离,更加安全可靠
第三步、创建用户接口(auth.py)
首先,需要加个 CreateUserRequest(BaseModel) 类,主要用来验证 POST 请求接口参数。
from pydantic import BaseModel
class CreateUserRequest(BaseModel):
username: str
email: str
first_name: str
last_name: str
password: str
role: str
添加接口代码。
@router.post("/auth", status_code=status.HTTP_201_CREATED)
async def create_user(create_user_request: CreateUserRequest):
create_user_model = Users(
username=create_user_request.username,
email=create_user_request.email,
first_name=create_user_request.first_name,
last_name=create_user_request.last_name,
role=create_user_request.role,
hashed_password=create_user_request.password,
is_active=True
)
return create_user_model
注意这里的 hashed_password=create_user_request.password,我们暂时还没有对密码进行加密处理。
密码加密存储
在 Users 表中,我们使用 hashed_password 字段,来存储加密后的用户密码。
为什么要加密存储呢?
核心还是安全!试想一下,如果线上数据库被泄漏出去,密码还是明文存储。这相当于,整个系统用户都在裸奔。这将是多大的事故~
01、安装依赖
# 密码加密依赖
pip install passlib -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install bcrypt==4.0.1 -i https://pypi.tuna.tsinghua.edu.cn/simple # 固定版本避免兼容问题
02、密码加密(auth.py)
先导入 CryptContext ,并实例化。
from passlib.context import CryptContext
bcrypt_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
修改 create_user 方法,给明文密码加密。
@router.post("/auth", status_code=status.HTTP_201_CREATED)
async def create_user(create_user_request: CreateUserRequest):
create_user_model = Users(
username=create_user_request.username,
email=create_user_request.email,
first_name=create_user_request.first_name,
last_name=create_user_request.last_name,
role=create_user_request.role,
# 修改:加密处理
hashed_password=bcrypt_context.hash(
create_user_request.password),
is_active=True
)
return create_user_model
切回浏览器的 Swagger UI 页面,刷新页面。找到 POST /auth 接口,给 Request body 传入如下内容,点击 “Execute”,执行请求。
{
"username": "wangerge",
"email": "wangerge@qq.com",
"first_name": "wang",
"last_name": "erge",
"password": "test1234",
"role": "admin"
}
API 响应内容如下
{
"username": "wangerge",
"email": "wangerge@qq.com",
"first_name": "wang",
"last_name": "erge",
"role": "admin",
"hashed_password": "$2b$12$Hit2IP1l4REwWnPJDdtZfOpQpxKo8SzWraavGVleAxG19k1YivAqG",
"is_active": true
}
可以看到 hashed_password 字段已经被加密成一个长字符串了,不再是 “test1234” 明文了。
03、保存用户信息(auth.py)
接下来,我们将获取数据库的连接,并将用户信息保存到数据库。
我们使用 FastAPI 的Depends实现依赖注入,所有接口都可复用。
在 auth.py 中添加如下定义:
from database import SessionLocal
from typing import Annotated
from fastapi import Depends
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
db_dependency = Annotated[Session, Depends(get_db)]
这样,就可以在 create_user 方法中拿到数据库连接了。代码如下:
@router.post("/auth", status_code=status.HTTP_201_CREATED)
async def create_user(db: db_dependency,
create_user_request: CreateUserRequest):
create_user_model = Users(
username=create_user_request.username,
email=create_user_request.email,
first_name=create_user_request.first_name,
last_name=create_user_request.last_name,
role=create_user_request.role,
hashed_password=bcrypt_context.hash(create_user_request.password),
is_active=True
)
db.add(create_user_model)
db.commit()
切回浏览器 Swagger UI 页面,刷新页面,找到 POST /auth 接口,给 “Request body ”传入如下内容,执行请求。返回响应码 200。
{
"username": "wangerge",
"email": "wangerge@163.com",
"first_name": "wang",
"last_name": "erge",
"password": "test1234",
"role": "admin"
}
如何在数据库中,查询新加的用户信息呢?
我们可以在 sqlite 命令行,使用 sql 语句来查询新创建的用户信息:
(.venv) wangerge_notes: TodoApp$ sqlite3 todosapp.db
SQLite version 3.37.0 2021-12-09 01:34:53
Enter ".help" for usage hints.
sqlite> select * from users;
1|wangerge@163.com|wangerge|wang|erge|$2b$12$NmS1fzfGgS0BZXO6/9VB.OcVwZ5S/r/tHvAJ5TG83CkpdQSVtHLv6|1|admin
sqlite>
现在, 我们创建了一个完整的、合格的用户信息在数据库中。
验证用户信息
这里需求是这样的:用户通过 API 接口传入用户名和密码,FastAPI 服务器拿到用户名和密码,并将它与数据库中的用户信息进行比对,最后返回一个验证状态。
首先,需要安装依赖库:
pip install python-multipart -i https://pypi.tuna.tsinghua.edu.cn/simple
安装完成,就可以向应用程序提交表单。这里使用 OAuth2PasswordRequestForm,这是一种更安全的表单。能够从接口处获取请求中的用户名和密码。
添加 POST /token 接口代码:
from fastapi.security import OAuth2PasswordRequestForm
@router.post("/token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: db_dependency):
user = authenticate_user(form_data.username, form_data.password, db)
if not user:
return "Failed Authentication"
return "Successful Authentication"
def authenticate_user(username: str, password: str, db):
user = db.query(Users).filter(Users.username == username).first()
if not user:
return False
if not bcrypt_context.verify(password, user.hashed_password):
return False
return True
代码中的 authenticate_user 方法,接收用户名、密码、数据库连接。并对密码进行加密验证。
代码中的 login_for_access_token 方法中的 form_data 参数,接收用户提交的表单数据。
测试
切回浏览器 Swagger UI 页面,刷新页面,打开 POST /token 接口。
传入 username="wangerge" password="test1234"(其他参数,先略过),执行请求。返回响应码 200,响应体 "Successful Authentication"。
表示用户名和密码输入正确,接口验证成功。
传入 username="wangerge" password="test"(其他参数,先略过),执行请求。返回响应码 200,响应体 "Failed Authentication"
表示用户名和密码输入错误,接口验证失败。
下期预告
我们将引入 JWT(JSON Web Token)。 它是 FastAPI 生态中「用户认证 + 接口鉴权」的事实标准方案,也是 FastAPI 官方文档中主推的认证方式,更是 99% 的 FastAPI 后端项目必用的核心技术。
我们将就用「通俗语言 + 实战视角」,把 JWT 彻底讲透:是什么、为什么要用、核心优缺点、正确使用姿势、绝对不能踩的坑,所有内容贴合 FastAPI 开发场景,无废话、无冗余,新手也能一次吃透!
想要获取本章完整代码,请在评论区回复 【FastAPI】,代码直接复制就能跑。
关于 FastAPI 的其他疑问
FastAPI实战:3步搞定增删改查,你的第一个完整API来了!
别再堆if-else验参数了!FastAPI自带的参数验证器,至少省一半调试时间
3分钟搞定FastAPI的数据库设置,把数据存储玩明白,复制代码就能用
10分钟搞定FastAPI中“数据库连接管理、参数校验、文档维护”三大核心难题,让新手也能轻松地写出可落地的API
“警惕!FastAPI接口一夜「消失」” 95%程序员靠这招自救:我的路由分离血泪史
相关内容我都给大家做好了,感兴趣的朋友来「我的主页」找一找,直接就可以看到。
关注我,每天分享「Python」、「职场」有趣干货,千万不要错过!