前后端开发基础:FastAPI数据库操作指南 1.后端1.文件api下的是每个文件的路由 可以自定义models 是模型 每个接口都需要模型支撑schemas 请求体文件 我们添加修改的请求体写在这里面要与模型字段一致services 接口方法 我们将接口分为了路由和接口config 配置文件 数据库配置 jwt redis 写在里面main 主路由 主要用来调用api中的子路由 以及跨域 其他migrations 数据迁移的文件requirements.txt 依赖文件夹 运行可下载文本中的所有依赖2.配置# app/core/database.py 数据库配置文件 这个文件定义了 Tortoise-ORM 连接 MySQL 数据库所需的所有配置信息 # from app.config.settings import settings # TORTOISE_ORM 是 Tortoise-ORM 规定的配置字典变量名 # 后面用 register_tortoise 或 Aerich 时都会引用这个字典 TORTOISE_ORM { # 1. 连接配置 —— 定义数据库连接信息 connections: { # default 是默认连接的名字必须有一个 default default: { # engine指定数据库后端引擎MySQL 使用 tortoise.backends.mysql engine: tortoise.backends.mysql, # credentials数据库连接凭证包含主机、端口、用户名、密码等 credentials: { host: 127.0.0.1, # MySQL 服务器地址 port: 3306, # MySQL 端口默认 3306 user: root, # 数据库用户名 password: 123456, # 数据库密码请根据实际情况修改 database: Fast_api_shx, # 数据库名称 minsize: 1, # 连接池最小连接数 maxsize: 5, # 连接池最大连接数 charset: utf8mb4, # 字符集支持 emoji echo: True # 是否打印 SQL 语句开发环境建议开启 } } }, # 2. 应用配置 —— 指定模型所在的模块 apps: { # models 是应用的名字可以自定义但 Aerich 需要使用这个名字 models: { # models 列表指定包含 Tortoise 模型类的 Python 模块路径 # aerich.models 是 Aerich 的内置模型用于记录迁移历史必须包含 models: [models.models, aerich.models], # default_connection指定这个应用使用哪个数据库连接 default_connection: default, } }, # 3. 时区配置 use_tz: False, # 是否使用时区 timezone: Asia/Shanghai, # 时区设置 echo: True # ✅ 关键打开 SQL 打印 }我们用来连接数据库的配置迁移命令第一次 aerich init -t config.TORTOISE_ORM aerich init-db后续 aerich migrate aerich upgrade迁移后就可以调用mysql数据库中的数据了3.接口models模型 简单模型创建from tortoise import fields, models class Category(models.Model): id fields.IntField(pkTrue) name fields.CharField(max_length255) order fields.IntField() class Meta: table category class Book(models.Model): id fields.IntField(pkTrue) name fields.CharField(max_length255) price fields.DecimalField(max_digits10, decimal_places2) numbers fields.IntField(default0) category fields.ForeignKeyField(models.Category, related_namebooks) class Meta: table book1.查询所有 分页book_api.get(/, summary分页查询图书列表) async def get_book_list(page: int 1, page_size: int 10): 根据页码和每页条数分页查询图书数据 page当前页码默认值1 page_size单页展示数据条数默认值10 返回内容当前页码、每页条数、当前页量、总数据量、当前页图书数 book, total, count_total await BookService.get_book_list(page, page_size) return { page: page, page_size: page_size, total: total, count_total: count_total, data: book }class BookService: # 分页查询图书列表 staticmethod async def get_book_list(page, page_size): 分页查询图书 # 计算数据偏移量 offset (page - 1) * page_size # 分页查询同时预加载关联分类信息避免多次查库 booke await Book.all().prefetch_related(category).offset(offset).limit(page_size) # 整理返回前端需要的字段 booke_list [ { id: book.id, name: book.name, price: book.price, numbers: book.numbers, category_id: book.category.id, category: book.category.name } for book in booke ] total len(booke_list) # 查询数据库全部图书总数量 count_total await Book.all().count() return booke_list, total, count_total从前端接受要分页的page和page_size 如果没有就是默认第一页的10条数据如果有传到serviceoffset (page - 1) * page_size 计算偏移量 然后分页查询定义book_list将数据加入 然后下面是分类的链接表 返回查询列表 总数量 当前查询数量2.查询指定数据book_api.get(/{book_id}, summary根据ID查询图书详情) async def get_book_detail(book_id: int): 依据图书id查询单本图书完整信息 book_id图书主键id路径参数 返回包含状态、提示、图书详情 return await BookService.get_detail_by_id(book_id)staticmethod async def get_detail_by_id(book_id: int): 根据ID查询图书详情 try: # 根据id查图书顺带查出分类名称 book await Book.get(idbook_id).prefetch_related(category) return { code: 200, msg: 查询成功, data: { id: book.id, name: book.name, price: book.price, numbers: book.numbers, category_id: book.category.id, category: book.category.name } } except DoesNotExist: # 不存在 return {code: 400, msg: 图书不存在, data: None}从前端返回要查询的book_id 然后传到接口 进行查询 如果book_id存在就返回信息 如果不再就返回不存在的提示信息就可以了3.添加数据book_api.post(/, summary新增图书) async def add_book(form: BookCreateSchema): 新增一本图书请求体BookCreateSchema做字段校验所有字段必须全部传入 form前端提交的图书表单信息 返回新增成功/失败的状态提示 return await BookService.create_book(form.model_dump())staticmethod async def create_book(info: dict): 新增图书入库 await Book.create(**info) return {code: 200, msg: 新增成功}请求体from pydantic import BaseModel class BookCreateSchema(BaseModel): name: str price: float numbers: int category_id: int class BookUpdateSchema(BaseModel): name: str | None None price: float | None None numbers: int | None None category_id: int | None None将前端返回添加的信息 直接添加到数据库中4.修改数据book_api.put(/{book_id}, summary编辑修改图书) async def edit_book(book_id: int, form: BookUpdateSchema): 根据图书id修改图书信息支持局部更新不传的字段不会改动 book_id需要修改的图书id路径参数 form前端提交的修改字段字段全部可选 返回修改成功或失败提示 return await BookService.update_book(book_id, form.model_dump())staticmethod async def update_book(book_id: int, info: dict): 修改图书自动过滤None空字段只更新传入的参数 update_info {k: v for k, v in info.items() if v is not None} update_count await Book.filter(idbook_id).update(**update_info) if update_count 0: return {code: 400, msg: 图书不存在修改失败} return {code: 200, msg: 修改成功}从前端返回两个值 一个是要修改的id另外一个是修改的数据 在请求体中 都可以为空None 先判断修改的数据是否存在 在判断我们要修改的数据都有哪些然后1进行update修改5.删除数据book_api.delete(/{book_id}, summary删除图书) async def delete_book(book_id: int): 根据图书id删除对应图书数据 book_id待删除图书主键id 返回删除成功/失败提示 return await BookService.delete_book(book_id)staticmethod async def delete_book(book_id: int): 根据ID删除图书 del_count await Book.filter(idbook_id).delete() if del_count 0: return {code: 400, msg: 图书不存在删除失败} return {code: 200, msg: 删除成功}传要删除的数据id判断是否存在然后删除2.前端vite.config.js进行配置import { defineConfig } from vite import vue from vitejs/plugin-vue import { resolve } from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : resolve(__dirname, src) } }, server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, } } } })然后是app页面template div stylepadding: 24px div styledisplay: flex; justify-content: space-between; align-items: center; margin-bottom: 16px h2图书列表/h2 el-button typeprimary clickopenAdd新增图书/el-button /div el-table :datatableData border stylewidth: 100%; margin: 20px 0 el-table-column fixed propid label图书ID width100 / el-table-column propname label书名 width220 / el-table-column propprice label价格 width120 / el-table-column propnumbers label库存数量 width120 / el-table-column propcategory_id label分类ID width120 / el-table-column propcategory label分类名称 width180 / el-table-column fixedright label操作 min-width200 template #defaultscope el-button link typeprimary sizesmall clickhandleDetail(scope.row)详情/el-button el-button link typeprimary sizesmall clickhandleEdit(scope.row)编辑/el-button el-button link typedanger sizesmall clickhandleDelete(scope.row)删除/el-button /template /el-table-column /el-table div styledisplay:flex;justify-content:flex-end el-pagination v-model:current-pagequery.page v-model:page-sizequery.page_size :page-sizes[5,10,20,50] background layouttotal, sizes, prev, pager, next, jumper :totaltotalNum size-changehandleSizeChange current-changehandleCurrentChange / /div !-- 新增、编辑共用弹窗 -- el-dialog v-modeldialogVisible title图书表单 width520px el-form label-width90px el-form-item label书名 el-input v-modelform.name/el-input /el-form-item el-form-item label价格 el-input v-model.numberform.price/el-input /el-form-item el-form-item label库存数量 el-input v-model.numberform.numbers/el-input /el-form-item !-- 分类下拉选择 -- el-form-item label分类 el-select v-modelform.category_id placeholder请选择分类 stylewidth: 100% el-option label文学小说 value1 / el-option label计算机编程 value2 / el-option label历史人文 value3 / el-option label心理学 value4 / el-option label美食菜谱 value5 / /el-select /el-form-item /el-form template #footer el-button clickdialogVisible false取消/el-button el-button typeprimary clicksubmitForm确定提交/el-button /template /el-dialog !-- 详情弹窗样式和编辑弹窗完全一致 -- el-dialog v-modeldetailDialogVisible title图书详情 width520px el-form label-width90px el-form-item label书名 el-input v-modeldetailForm.name disabled/el-input /el-form-item el-form-item label价格 el-input v-model.numberdetailForm.price disabled/el-input /el-form-item el-form-item label库存数量 el-input v-model.numberdetailForm.numbers disabled/el-input /el-form-item el-form-item label分类 el-input v-modeldetailForm.category disabled/el-input /el-form-item /el-form template #footer el-button clickdetailDialogVisible false关闭/el-button /template /el-dialog /div /template script setup import { ref, onMounted } from vue import axios from axios import { ElMessage, ElMessageBox } from element-plus const totalNum ref(0) const tableData ref([]) const query ref({ page: 1, page_size: 5 }) // 新增编辑弹窗控制 const dialogVisible ref(false) // 区分新增/编辑模式 add新增 edit编辑 const operateMode ref(add) // 编辑行id const editBookId ref(null) // 新增编辑表单 const form ref({ name: , price: 0, numbers: 0, category_id: }) // 详情弹窗控制 const detailDialogVisible ref(false) // 详情只读表单 const detailForm ref({ name: , price: 0, numbers: 0, category_id: , category: }) // 加载图书列表 1.在挂载时加载 // const loadBook () { // axios.get(/api/book/?page${query.value.page}page_size${query.value.page_size}) // .then(res { // console.log(后端返回, res.data) // tableData.value res.data.data // totalNum.value res.data.count_total // }) // .catch(err console.error(err)) // } // 加载图书列表 2.在事件中加载 const loadBook () { axios.get(/api/book/, {params: query.value}) .then(res { console.log(res.data) tableData.value res.data.data totalNum.value res.data.count_total }) .catch(err console.error(err)) } // 每页条数切换 const handleSizeChange (newSize) { query.value.page_size newSize query.value.page 1 loadBook() } // 页码切换 const handleCurrentChange (newPage) { query.value.page newPage loadBook() } // 详情调用接口打开弹窗全部.then写法 const handleDetail (row) { console.log(详情数据, row) axios.get(/api/book/${row.id}) .then(res { console.log(详情完整数据, res.data) // 回填详情表单 detailForm.value res.data.data // 打开详情弹窗 detailDialogVisible.value true }) .catch(err { console.error(详情查询失败, err) ElMessage.error(详情加载失败) }) } // 编辑 const handleEdit (row) { console.log(编辑当前行, row) // 打开弹窗回填数据 operateMode.value edit editBookId.value row.id form.value { name: row.name, price: row.price, numbers: row.numbers, category_id: row.category_id } dialogVisible.value true } // 打开新增弹窗 const openAdd () { operateMode.value add editBookId.value null // 清空表单 form.value { name: , price: 0, numbers: 0, category_id: } dialogVisible.value true } // 提交新增/编辑表单 const submitForm () { if (operateMode.value add) { // 新增 axios.post(/api/book/, form.value) .then(() { ElMessage.success(新增成功) dialogVisible.value false loadBook() }) .catch(err { ElMessage.error(新增失败) console.error(err) }) } else { // 编辑 axios.put(/api/book/${editBookId.value}, form.value) .then(() { ElMessage.success(修改成功) dialogVisible.value false loadBook() }) .catch(err { ElMessage.error(修改失败) console.error(err) }) } } // 删除按钮事件 全部then写法 const handleDelete (row) { ElMessageBox.confirm(确认删除当前图书删除不可恢复, 删除提示, { type: warning }) .then(() { return axios.delete(/api/book/${row.id}) }) .then(() { ElMessage.success(删除完成) loadBook() }) .catch(() { ElMessage.info(已取消删除) }) } onMounted(loadBook) /script