前端转后端生存指南(中):化身架构师,用 ORM 魔法掌控数据库
在上一篇中,我们打破了面对空白 Python 文件的恐惧,成功跑通了 FastAPI 的第一层脚手架,并学会了如何优雅地读取 .env 配置。
但作为一个数字孪生/3D项目,不管是堆垛机的坐标,还是传送带的状态,如果在前端,页面一刷新数据就全丢了(因为它们活在内存或 Pinia 里)。但在后端,我们的核心使命是:把数据安全、持久地存进硬盘里。
这一篇,我们将化身架构师,接入 MySQL 数据库。我们将使用后端的终极黑魔法------ORM(对象关系映射) 。学会它,你甚至不需要写一行难懂的 SQL 语句,就能像操作 JS 对象一样操纵数据库。
概念映射:保安与库管员的协同
在写代码之前,我们要先跨越所有前端转后端都会遇到的第一个"认知门槛":为什么数据流转需要两套不同的模型?
| 前端 / 现实世界 | FastAPI 后端对应概念 | 它的核心职责 |
|---|---|---|
TypeScript interface / 门口保安 |
schemas.py (Pydantic 模型) |
校验外部输入。它负责检查前端发来的 JSON 对不对,有没有少传字段。只有它放行的数据,才会进入系统。 |
| 数据库表结构 / 库管员 | models.py (SQLAlchemy 模型) |
管理内部存储。它只关心数据库里真实的"货架"长什么样,负责把数据真正写入物理硬盘。 |
理解了这个分工,后面的增删改查就只是一场"前端发请求 -> 保安校验 -> 库管库存库"的接力赛。
一、 准备材料:安装依赖与拼装"数据库钥匙"
1. 安装 ORM 与 MySQL 驱动 在终端运行以下命令。这就好比你在 Node.js 里安装 mysql2。
csharp
uv add sqlalchemy pymysql
2. 在 .env 中添加数据库信息 打开你的 .env 文件,补充以下真实信息:
ini
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=123
MYSQL_DATABASE=3d_mqtt
(注意:请确保你已经在本地 MySQL 里提前建好了 3d_mqtt 这个空数据库!)
3. 在 config.py 中拼装连接字符串 我们需要一个极长的字符串来连接数据库,别自己拼,让程序自动算出来:
python
# config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_name: str
nano_mq_url: str
# 声明从 .env 读取的 MySQL 配置
mysql_host: str
mysql_port: int
mysql_user: str
mysql_password: str
mysql_database: str
# 【魔法属性】:自动拼装出 sqlalchemy 要求的格式
# 比如:mysql+pymysql://root:123@localhost:3306/3d_mqtt
@property
def database_url(self) -> str:
return f"mysql+pymysql://{self.mysql_user}:{self.mysql_password}@{self.mysql_host}:{self.mysql_port}/{self.mysql_database}"
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
settings = Settings()
二、 搭建基建:创建数据库引擎与图纸
我们需要创建一个专门的 database.py,把这里想象成一个"建筑工地司令部"。
ini
# database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from config import settings
# 1. 创建引擎 (雇佣一支能连接数据库的施工队)
# echo=True 是新手神器!它会把底层偷偷执行的 SQL 语句全打印在终端里。
engine = create_engine(settings.database_url, echo=True)
# 2. 创建会话工厂 (造一把能够打开数据库大门的万能钥匙工厂)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 3. 创建基类 (创建一个图纸总文件夹)
Base = declarative_base()
接下来,我们来画真实的表结构图纸。新建 models.py:
sql
# models.py
from sqlalchemy import Column, Integer, String, Float
from database import Base
# 定义堆垛机 (Stacker) 在数据库里的样子。只要继承了 Base,就会被放进图纸总文件夹。
class Stacker(Base):
__tablename__ = "stacker" # 数据库里的真实表名
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
device_id = Column(String(50), unique=True, index=True, comment="设备编号")
status = Column(String(20), comment="当前状态: running/idle/error")
current_x = Column(Float, default=0.0, comment="当前X坐标")
current_y = Column(Float, default=0.0, comment="当前Y坐标")
最后,回到我们的主入口 main.py,给施工队下达终极建表命令:
python
# main.py 的开头补充这些:
from fastapi import FastAPI
from database import engine
import models
# 【终极魔法】:FastAPI 启动前,查阅图纸文件夹,把没建的表全都自动建好!
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
# ... 下面是你之前写的 /health 接口 ...
💡 避坑指南: 施工队有个坏脾气,它只建新楼,绝不改旧楼 。如果你以后在
models.py里加了新字段,最简单的办法就是去数据库里手动把stacker表删掉,然后重启服务让它重建。(企业级项目会使用Alembic工具来管理版本,但本地开发直接删表最爽快)。
三、 编写校验规则:门口的"保安"
新建 schemas.py,我们来定义前端应该怎么传数据。这里全是 Pydantic 的代码,非常像 TS 的 interface。
python
# schemas.py
from pydantic import BaseModel
from typing import Optional
# 这是针对前端发来 POST 请求 (新增设备) 的格式校验
class StackerCreate(BaseModel):
device_id: str # 必填项
status: Optional[str] = "idle" # 选填项,默认值
current_x: Optional[float] = 0.0
current_y: Optional[float] = 0.0
# 这是针对 PUT 请求 (修改设备) 的格式校验
class StackerUpdate(BaseModel):
status: Optional[str] = None
current_x: Optional[float] = None
current_y: Optional[float] = None
四、 增删改查 (CRUD) 全家桶
万事俱备!回到 main.py,我们将保安(schemas)、库管(models)和钥匙(db)组合在一起。
1. 准备自动借还钥匙的机制:
python
# 修改 main.py 的引入
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from database import SessionLocal, engine
import models, schemas
# ... (保留上面的 app 实例化和建表代码) ...
# 每次接口被调用时,临时借一把钥匙;用完后(finally)必须归还!
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
2. 编写四个核心接口: 将下面这段代码贴在 main.py 的底部:
python
# 1. 增 (Create)
@app.post("/stackers")
def create_stacker(stacker_data: schemas.StackerCreate, db: Session = Depends(get_db)):
# 检查是否有重复设备
db_stacker = db.query(models.Stacker).filter(models.Stacker.device_id == stacker_data.device_id).first()
if db_stacker:
raise HTTPException(status_code=400, detail="该设备号已存在!")
# 包装成数据库模型并入库
new_stacker = models.Stacker(**stacker_data.model_dump()) # 相当于解构赋值
db.add(new_stacker)
db.commit() # 提交保存
db.refresh(new_stacker) # 获取数据库生成的 ID
return {"message": "创建成功", "data": new_stacker}
# 2. 查 (Read) - 获取所有列表
@app.get("/stackers")
def get_stackers(db: Session = Depends(get_db)):
stackers = db.query(models.Stacker).all()
return {"data": stackers}
# 3. 改 (Update)
@app.put("/stackers/{stacker_id}")
def update_stacker(stacker_id: int, update_data: schemas.StackerUpdate, db: Session = Depends(get_db)):
db_stacker = db.query(models.Stacker).filter(models.Stacker.id == stacker_id).first()
if not db_stacker:
raise HTTPException(status_code=404, detail="找不到这个设备!")
# 遍历更新有变化的值
for key, value in update_data.model_dump(exclude_unset=True).items():
setattr(db_stacker, key, value)
db.commit()
db.refresh(db_stacker)
return {"message": "更新成功", "data": db_stacker}
# 4. 删 (Delete)
@app.delete("/stackers/{stacker_id}")
def delete_stacker(stacker_id: int, db: Session = Depends(get_db)):
db_stacker = db.query(models.Stacker).filter(models.Stacker.id == stacker_id).first()
if not db_stacker:
raise HTTPException(status_code=404, detail="找不到这个设备!")
db.delete(db_stacker)
db.commit()
return {"message": "删除成功"}
五、 见证奇迹:告别 Postman
代码写完了!在终端运行:
arduino
uv run fastapi dev main.py
(注意看终端,如果这是第一次运行,你能清晰地看到黄色的一长串 CREATE TABLE stacker...,说明建表成功了!)
重点来了: 不要去折腾 Postman。直接打开浏览器访问:http://127.0.0.1:8000/docs。
你面前会出现 FastAPI 自动为你生成的 Swagger UI 交互式文档。你的四个接口整齐地排列在上面。
- 点击
POST /stackers-> 点击Try it out。 - 在 JSON 框里修改一下
device_id,点击Execute。 - 返回 200!你去数据库里看一眼,数据已经真真切切地存进了你的电脑硬盘里。
总结
恭喜!到这里,你已经掌握了传统后端开发 80% 的日常工作:写接口、连数据库、增删改查。
但作为数字孪生项目,设备坐标怎么能靠人手敲接口来更新呢?下一篇,我们将迎来最终的 Boss 战:引入 MQTT,让设备自动发报,后台接线员自动拦截并存入数据库!