Flask + MySQL ORM 实战:从环境配置到增删改查完整教程

Flask + MySQL ORM 实战:从环境配置到增删改查完整教程 做后端开发最绕不开的一件事就是数据库。不管是用户注册登录、订单记录还是内容管理数据总得找地方存下来。之前几篇我们把 Flask 的基础路由、模板渲染、表单提交都过了一遍但那些数据都存在内存里程序一重启就全没了。这一篇我们彻底解决持久化问题——让 Flask 连上 MySQL并且用 ORM 的方式完成增删改查把数据稳稳当当落进数据库里。这篇内容我按自己实际开发项目的节奏来写不会只丢给你几个代码片段。从 MySQL 环境怎么装、Flask 怎么配置连接到 ORM 怎么定义表、增删改查全套代码怎么写再到常见的坑和排错方法一次讲透。无论你是刚开始学 Flask 的后端新手还是已经在写接口但一直用裸 SQL 想换 ORM 的同学这篇都能直接照着操作。1. 项目准备先搞定 MySQL 和依赖1.1 MySQL 安装与基础配置先说数据库本身。很多人卡在第一步——MySQL 装不上或者装好了连不上。其实 MySQL 的安装没有想象中复杂重点是把几处配置搞清楚。如果你是 Windows 系统直接去 MySQL 官网下载 MySQL Community Server 的安装包选 MSI Installer 版本安装过程中选择 Server only 即可。版本建议选 8.x 系列目前 8.0 是主流稳定版性能和功能都比 5.7 有提升。安装到配置步骤时有几个关键选项需要注意端口默认 3306不用改除非你本机已有服务占用。认证方式8.x 默认是 caching_sha2_password建议选这个。如果选了 mysql_native_password后面用新版连接驱动也没问题但既然都用新版本了就没必要向后兼容。Root 密码设置一个自己记得住的强密码开发环境建议不要带特殊字符避免在连接字符串里转义麻烦。macOS 的话建议直接用 Homebrew 安装一条命令搞定brew install mysql brew services start mysql装完 MySQL 之后记得验证一下服务是否正常启动。Windows 下可以在服务里查看 MySQL80 是否处于正在运行状态。然后在命令行执行mysql -u root -p输入密码能进到mysql交互界面就说明安装成功了。接下来创建我们后续要用的数据库。在 MySQL 命令行里执行CREATE DATABASE flask_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这里我强烈建议建库时显式指定utf8mb4字符集。原因很简单utf8mb4是完整的 UTF-8 实现能存 emoji、生僻字而老的utf8只支持基本多语言平面存个笑脸表情都会报错。我自己早期项目就吃过这个亏后来所有库都统一用utf8mb4。还有一件事很多教程会让你创建一个单独的 MySQL 用户给项目用不要用 root。这个建议是对的。开发环境无所谓但如果你以后部署到服务器用 root 连数据库是很大的安全隐患。我们创建一个专用用户CREATE USER flask_userlocalhost IDENTIFIED BY flask_password; GRANT ALL PRIVILEGES ON flask_demo.* TO flask_userlocalhost; FLUSH PRIVILEGES;这样 Flask 应用就用flask_user身份连接flask_demo库权限只限制在项目库内不会误操作其他数据库。1.2 安装 Flask-SQLAlchemy数据库准备好了接下来在 Python 环境里装依赖。我们需要两个核心包Flask-SQLAlchemy和PyMySQL。pip install flask flask-sqlalchemy pymysql为什么需要 PyMySQL因为 Flask-SQLAlchemy 本身只是一个 ORM 框架它不直接和数据库通信底层需要数据库驱动。PyMySQL 就是 Python 连接 MySQL 的驱动纯 Python 实现兼容性好不用单独编译。当然你也可以用mysqlclient它性能更好但在 Windows 上编译容易出各种问题新手我统一推荐 PyMySQL省心。装完之后在 Flask 项目里初始化数据库连接。我先给一个最小化的配置示例from flask import Flask from flask_sqlalchemy import SQLAlchemy app Flask(__name__) # 配置数据库连接 app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://flask_user:flask_passwordlocalhost:3306/flask_demo?charsetutf8mb4 app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db SQLAlchemy(app)这里逐行解释一下配置项。SQLALCHEMY_DATABASE_URI是数据库连接字符串格式是数据库驱动://用户名:密码主机:端口/数据库名。以我的配置为例mysqlpymysql表示用 PyMySQL 驱动连接 MySQLflask_user:flask_password是刚建的用户名和密码localhost:3306是数据库地址和端口最后是数据库名。末尾的?charsetutf8mb4告诉连接层用utf8mb4编码确保中文和特殊字符不出乱码。SQLALCHEMY_TRACK_MODIFICATIONS设置为False可以关闭 SQLAlchemy 的对象修改追踪机制。这个东西本来是用来发信号、做调试用的但实际开发中你用不到还会白白消耗内存直接关掉。1.3 数据库连接配置有了最小配置还不够我自己做项目时会稍微规整一下配置信息单独建一个config.py方便多环境切换。这里分享一个常用的做法# config.py import os class Config: 基础配置 SECRET_KEY os.environ.get(SECRET_KEY) or dev-key-please-change SQLALCHEMY_TRACK_MODIFICATIONS False class DevConfig(Config): 开发环境配置 SQLALCHEMY_DATABASE_URI mysqlpymysql://flask_user:flask_passwordlocalhost:3306/flask_demo?charsetutf8mb4 DEBUG True class ProdConfig(Config): 生产环境配置 SQLALCHEMY_DATABASE_URI os.environ.get(DATABASE_URL) or mysqlpymysql://flask_user:flask_passwordlocalhost:3306/flask_demo?charsetutf8mb4 DEBUG False然后在app.py里from flask import Flask from flask_sqlalchemy import SQLAlchemy from config import DevConfig app Flask(__name__) app.config.from_object(DevConfig) db SQLAlchemy(app)为什么要把配置拆出来因为开发环境、测试环境、生产环境的数据库地址、密码通常都不一样。如果你把连接串硬编码在代码里每次部署都要改代码很容易出错。用环境变量加配置文件的方式切换环境只需要改from_object这一行或者设置环境变量代码本身一行不用动。注意上面代码里的flask_password是示例用的弱密码实际项目一定要用强密码并且不要把真实密码提交到 Git 仓库。建议用环境变量或.env文件管理敏感信息。一个小技巧如果你不想被from_object的选择困住可以这样写app.config.from_object(os.environ.get(FLASK_CONFIG) or config.DevConfig)这样通过设置环境变量FLASK_CONFIG就能切换环境非常灵活。2. 定义模型把数据库表变成 Python 类2.1 模型字段与类型ORM 的核心思想是把数据库表映射成一个 Python 类表中的每一行就是类的一个实例每一列就是实例的属性。这样你在代码里操作对象SQLAlchemy 会自动帮你翻译成 SQL 语句发到 MySQL 执行。你不再需要拼 SQL 字符串也不用担心 SQL 注入。我以用户表和文章表为例演示模型定义。创建一个models.pyfrom datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from app import db class User(db.Model): 用户模型 __tablename__ users id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) username db.Column(db.String(80), uniqueTrue, nullableFalse) email db.Column(db.String(120), uniqueTrue, nullableFalse) password_hash db.Column(db.String(128), nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow) def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) def __repr__(self): return fUser {self.username} class Post(db.Model): 文章模型 __tablename__ posts id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) title db.Column(db.String(200), nullableFalse) content db.Column(db.Text, nullableFalse) user_id db.Column(db.Integer, db.ForeignKey(users.id), nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow) author db.relationship(User, backrefdb.backref(posts, lazyTrue)) def __repr__(self): return fPost {self.title}这里涉及几个关键概念我逐个解释。db.Column用来定义字段第一个参数是数据类型。Integer对应 MySQL 里的intString(80)对应varchar(80)Text对应textDateTime对应datetime。看到这个映射关系你应该就明白了——ORM 不是魔法它只是把数据库类型做了封装你在 Python 世界里直觉地定义字段类型数据库里也会生成对应类型。primary_keyTrue和autoincrementTrue组合起来就是数据库里的主键自增字段。这个是 ORM 模型里必不可少的每张表都要有一个主键否则后面增删改查会遇到很多无法预料的坑。uniqueTrue表示该字段的值在表中不能重复对应 MySQL 的唯一索引。nullableFalse表示字段非空。这些约束条件必须在模型定义阶段就考虑到位一旦表已经在数据库里创建了再改约束就比较麻烦因为需要ALTER TABLE操作。defaultdatetime.utcnow是给created_at字段设默认值。注意这里传的是datetime.utcnow这个函数本身不是在导入时执行一次。这样每次插入新记录时SQLAlchemy 会调用datetime.utcnow()获取当前时间。如果你写成defaultdatetime.utcnow()就会变成固定值所有记录的时间戳都一样这是个非常隐蔽的坑。2.2 表关系和约束上面User和Post之间是一对多关系一个用户可以有多个文章。实现这种关系的关键是db.ForeignKey(users.id)和db.relationship。db.ForeignKey(users.id)定义在Post.user_id字段上告诉数据库这个字段的值必须来自users表的id列。这就是数据库外键约束它保证了数据的引用完整性。比如你删除一个用户时如果数据库里还存在他写的文章MySQL 默认会拒绝删除除非你先清理文章。db.relationship(User, backrefdb.backref(posts, lazyTrue))是 ORM 层面的关系映射作用是让你在 User 和 Post 实例之间建立关联属性。定义了这个关系后你可以直接通过post.author获取文章对应的用户对象也可以通过user.posts获取该用户的所有文章列表。backref自动给User模型加了一个posts属性lazyTrue表示只有在访问user.posts时才真正去数据库查询不会提前加载。还需要注意一个点我在关系定义中没有显式指定foreign_keys参数因为这里只有user_id一个外键SQLAlchemy 能自动识别。但如果一张表里有多个外键指向同一张表就必须显式指定foreign_keys否则会报 Could not determine join condition 的错误。这个我们在遇到实际场景时再细说。2.3 创建表结构模型定义好了怎么在 MySQL 里生成对应的表最简单的方式是在 Python 交互环境中执行from app import app, db from models import User, Post with app.app_context(): db.create_all()然后去 MySQL 里看一下USE flask_demo; SHOW TABLES;能看到users和posts两张表就说明成功了。需要提醒的一点是db.create_all()只会创建表中不存在的表不会更新已存在表的结构。也就是说如果你给User模型加了一个nickname字段再执行create_all()数据库里的表并不会自动增加这个字段。这是因为 SQLAlchemy 的设计哲学偏向显式迁移而不是自动修改表结构。想要方便地管理表结构变更可以用Flask-Migrate这个库它基于Alembic支持生成迁移脚本、升级和回滚数据库结构。这个属于进阶话题篇幅有限不展开但我要强调正式项目里一定要用迁移工具管理数据库结构create_all()只适合教学和验证场景。3. 增删改查实战ORM 的核心操作3.1 新增数据三种方式对比有了模型写增删改查就很直观了。先看新增数据核心就三行代码创建对象、添加会话、提交事务。from app import db from models import User # 创建一个用户对象 user User(usernamealice, emailaliceexample.com) # 密码加密存储 user.set_password(password123) # 添加到会话并提交 db.session.add(user) db.session.commit()执行完这段代码SQLAlchemy 会生成一条INSERT INTO users (username, email, password_hash, created_at) VALUES (...)语句自动提交到 MySQL。这里有几个我踩过的坑值得说明。第一个是set_password(password123)不是随便写的。出于安全考虑用户密码绝对不能明文存数据库。我用werkzeug.security的generate_password_hash对密码做了哈希处理存进password_hash字段。这样数据库泄露了攻击者拿到的也只是哈希值无法直接得到原始密码。如果你自己写登录功能强烈建议也这样做别偷懒。第二个是事务提交的问题。db.session.add()之后数据并没有立刻写进数据库而是在会话里排队。直到你调用db.session.commit()SQLAlchemy 才会把这一批操作提交成一个事务。如果中途报错事务会回滚数据不会写入。这种设计的好处是你可以把多个操作放在同一个事务里要么全部成功要么全部失败保证数据一致性。第三个是批量新增。如果有多条数据要插入循环调用db.session.add()再统一commit()的效率远高于每条都commit()一次。因为每次 commit 都会触发一次网络往返和事务提交耗时严重。正确写法users_data [ {username: bob, email: bobexample.com}, {username: carol, email: carolexample.com}, {username: dave, email: daveexample.com}, ] for item in users_data: user User(usernameitem[username], emailitem[email]) user.set_password(initial_password) db.session.add(user) db.session.commit() # 只提交一次如果你追求极致性能还可以用db.session.bulk_save_objects()或bulk_insert_mappings()做段式批量插入。但这两个方法默认不会触发 ORM 的before_insert等事件也不维护对象状态建议只是纯插入时用。日常开发里循环 add 然后一次性 commit 已经够用没必要为了这点性能差异牺牲可维护性。3.2 查询数据最常用的部分查询是日常开发中使用频率最高的操作。Flask-SQLAlchemy 的查询 API 设计得很顺手核心是Model.query对象配合各种方法。查所有数据users User.query.all() # 返回 User 对象的列表按主键查user User.query.get(1) # 返回 id1 的用户没有则 None注意query.get()在 Flask-SQLAlchemy 2.x 里已经变更为db.session.get(User, 1)或User.query.get(1)。不同版本 API 有差异建议看一下你安装的版本。新版推荐from app import db user db.session.get(User, 1)按条件过滤# 等值查询 user User.query.filter_by(usernamealice).first() # 更灵活的过滤方式 user User.query.filter(User.username alice).first() # 模糊查询 users User.query.filter(User.username.like(%li%)).all() # 范围查询 from datetime import datetime, timedelta week_ago datetime.utcnow() - timedelta(days7) recent_users User.query.filter(User.created_at week_ago).all() # IN 查询 users User.query.filter(User.id.in_([1, 2, 3])).all()filter_by和filter有什么区别filter_by只支持等值条件用法更简洁适合简单场景filter支持所有的比较运算和逻辑组合功能更强大。实际开发中我两种都会用简单等值条件用filter_by复杂条件用filter。排序# 按创建时间倒序 users User.query.order_by(User.created_at.desc()).all() # 多字段排序 posts Post.query.order_by(Post.user_id.asc(), Post.created_at.desc()).all()分页Flask-SQLAlchemy 提供了paginate方法不用自己写LIMIT OFFSET。# 第 2 页每页 10 条 page_obj User.query.paginate(page2, per_page10, error_outFalse) users page_obj.items # 当前页的数据列表 total page_obj.total # 总记录数 pages page_obj.pages # 总页数 has_prev page_obj.has_prev # 是否有上一页 has_next page_obj.has_next # 是否有下一页error_outFalse表示当页码超出范围时不报 404而是返回空列表。这个参数在编写 API 时很实用因为前端传过来的页码可能超出范围你不想每次都得 try-except。聚合和计数user_count User.query.count()关联查询# 查询某用户的文章 user User.query.filter_by(usernamealice).first() posts user.posts # 通过 relationship 属性获取 # 反过来查文章作者 post Post.query.filter_by(titleFlask 入门教程).first() author_name post.author.username关联查询是 ORM 最爽的部分。想想裸 SQL 要写JOIN语句而现在你直接通过对象属性就能取到关联数据代码可读性提升了一个档次。3.3 更新数据千万别忘了提交更新数据在 ORM 里分两步查出来改再提交。其中很多人刚学时容易忘掉第二步以为对象改了数据库就改了结果数据没变。# 第一步查到要更新的对象 user User.query.filter_by(usernamealice).first() # 第二步修改属性 user.email alice_newexample.com # 第三步提交事务 db.session.commit()这里的工作机制是SQLAlchemy 会持续跟踪从会话中取出的对象。当你修改它的属性时SQLAlchemy 在内部标记这个对象为脏状态。调用commit()时它会对比当前状态和数据库中的原始状态发现有变化就自动生成UPDATE语句执行。如果你要原子性地更新多条记录不需要先查到每条再改。可以直接用update()方法# 将所有用户的邮箱域名统一替换 User.query.filter(User.email.endswith(old.com)).update( {email: func.replace(User.email, old.com, new.com)} ) db.session.commit()query.update()直接在数据库层面执行批量更新不会把每条记录都加载到 Python 内存中性能好很多。不过要注意批量更新默认不会触发 ORM 的before_update事件回调所以如果模型里有需要在更新时操作的方法就不适合用这种方式。3.4 删除数据外键约束的坑删除操作和更新类似也是三步走。# 查出来 user User.query.filter_by(usernamedave).first() # 删除 db.session.delete(user) # 提交 db.session.commit()但这里经常会遇到一个报错IntegrityError: (pymysql.err.IntegrityError) (1451, Cannot delete or update a parent row: a foreign key constraint fails)原因很简单这个用户还关联着文章数据库外键约束禁止直接删除。解决方式有几种根据业务需求选择方案一先删子表数据再删父表# 先删掉该用户的所有文章 Post.query.filter_by(user_iduser.id).delete() db.session.commit() # 再删用户 db.session.delete(user) db.session.commit()方案二给外键设置 ON DELETE CASCADE在模型的外键定义中设置级联删除这样删除用户时关联的文章会自动删除。user_id db.Column(db.Integer, db.ForeignKey(users.id, ondeleteCASCADE), nullableFalse)同时要在relationship里也设置cascadeauthor db.relationship(User, backrefdb.backref(posts, lazyTrue, cascadeall, delete-orphan))这个方案适合子表数据从属于父表父表没了子表也没意义的场景。但要注意delete-orphan的意思是从列表中移除的文章对象也会被自动删除使用时要确认业务逻辑确实如此。方案三软删除给模型加一个is_deleted字段删除时只是把该字段置为True查询时默认过滤。这种方式在真实项目里越来越流行因为数据不会物理消失便于审计和恢复而且不会触发外键约束问题。class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(80), uniqueTrue, nullableFalse) is_deleted db.Column(db.Boolean, defaultFalse) deleted_at db.Column(db.DateTime, nullableTrue) def soft_delete(self): self.is_deleted True self.deleted_at datetime.utcnow() db.session.commit()然后每次查询时都记得带上is_deletedFalse条件。为了避免每次手写可以在模型上定义一个类方法classmethod def query_active(cls): return cls.query.filter_by(is_deletedFalse)这样写业务代码时调用User.query_active()就行比较省事。4. 实操注意事项与问题排查4.1 会话管理与事务边界用 Flask-SQLAlchemy 做增删改查最大的坑不是增删改查本身而是会话管理和事务边界。每个请求进来Flask-SQLAlchemy 会创建一个独立的数据库会话session请求结束时自动关闭。你所有的db.session.add()、db.session.commit()都在这个会话里进行。这个机制本身很贴心但有个问题如果你在一个请求里做了一个耗时的操作数据一直没有commit()这个连接就一直被占用着。MySQL 的默认连接数是有限的通常 151 个高并发情况下连接池很快耗尽请求就开始排队甚至报错。我的建议是增删改操作尽量早 commit不让事务长时间持有连接。如果你在一个请求里需要做多个写操作又希望它们是原子的可以在业务函数里用事务控制块from sqlalchemy.exc import SQLAlchemyError def create_user_with_post(username, email, title, content): try: user User(usernameusername, emailemail) user.set_password(temp_password) db.session.add(user) db.session.flush() # 获取 user.id post Post(titletitle, contentcontent, user_iduser.id) db.session.add(post) db.session.commit() return user, post except SQLAlchemyError: db.session.rollback() raise这里用db.session.flush()把 SQL 发送到数据库但不 commit这样可以拿到自增主键user.id然后再创建关联的文章。最后的commit()会把两个操作一起提交任何一个失败都会回滚。rollback()也很重要。程序报错后如果不执行rollback()会话会处于异常状态后续的数据库操作都会失败。所以写代码时一定要保证出错时能回滚。4.2 常见错误与排错方法我把实际开发中经常遇到的操作错误整理成一个速查表方便你快速定位问题。错误现象报错关键信息原因解决办法连接失败Cant connect to MySQL serverMySQL 服务未启动启动 MySQL 服务认证失败Access denied for user用户名或密码错误检查连接串中的用户名密码数据库不存在Unknown database flask_demo数据库未创建执行CREATE DATABASE中文乱码问号或Incorrect string value字符集设置不一致建库时指定utf8mb4连接串加charsetutf8mb4表不存在Table flask_demo.users doesnt exist未执行create_all()执行建表操作字段不存在Unknown column users.nickname模型加了字段但表没更新使用迁移工具ALTER TABLE或重建表删除外键报错foreign key constraint fails有关联数据未处理级联删除或先删子表超时连接Lost connection to MySQL server during query单条查询耗时过长优化 SQL、加索引先查后改失败Transaction has been rolled back上一操作出错未回滚调用db.session.rollback()遇到数据库报错第一件事是看完整堆栈不要只看最后一行错误消息。很多时候真正的原因在 PyMySQL 报错信息的第一行比如卷进来的原始 SQL 语句和参数值。看到 SQL 语句你会更容易定位是不是 ORM 生成的语句和预期不符。另外开发环境建议设置SQLALCHEMY_ECHO Trueapp.config[SQLALCHEMY_ECHO] True这个配置会把所有执行的 SQL 语句打印到控制台。调试时打开能直观看到 ORM 把 Python 操作翻译成了什么 SQL很多莫名其妙的问题一眼就能看清。生产环境记得关掉毕竟日志里全输出 SQL 会有安全和性能问题。4.3 性能优化索引与查询效率最后聊一下性能因为增删改查是后端接口里最频繁的操作如果性能不好接口响应就是灾难。先说索引。ORM 不会帮你自动建索引除了主键和唯一约束所以查询慢的时候不要怪 ORM先想想是不是缺索引。比如Post.user_id这个字段外键约束虽然有了但如果不建索引按user_id查询文章列表会全表扫描数据量大时非常慢。在 SQLAlchemy 模型里建索引很简单class Post(db.Model): __tablename__ posts __table_args__ ( db.Index(idx_posts_user_created, user_id, created_at), ) id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(users.id), nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow)idx_posts_user_created是一个组合索引覆盖了user_id和created_at两个字段。当你执行按用户筛选文章并按时间排序的查询时这个索引能大幅提升效率。索引不是越多越好因为每次插入、更新数据时索引也要同步维护会拖慢写操作。只给常用的查询条件建索引别贪多。还有一个常见的性能问题是 N1 查询。比如你循环查询每篇文章的作者posts Post.query.all() for post in posts: print(post.author.username) # 每访问一次 author 就发一次 SQL这段代码会产生 1 条查询文章列表的 SQL 加 N 条查作者的 SQL总共 N1 次查询。文章多的时候性能极差。解决办法是在查询时用joinedload或selectinload预先加载关联数据from sqlalchemy.orm import joinedload posts Post.query.options(joinedload(Post.author)).all()这样 SQLAlchemy 会生成一条带 JOIN 的查询把作者信息一起查出来N1 问题就解决了。自己写接口时凡是循环里访问relationship属性的都要警惕这个陷阱。5. 经验总结与扩展建议这一篇从 MySQL 环境准备、Flask-SQLAlchemy 配置、模型定义到增删改查完整走了一遍配合代码示例和排错表希望能帮你在自己的项目里顺利落地 ORM。我自己从裸 SQL 切到 ORM 时最大的感受是开发效率提高明显但并不代表可以完全不写 SQL。遇到复杂查询我还是会直接用db.session.execute()写原生 SQL然后再用 ORM 处理结果。SQLAlchemy 提供了text()方法可以很好地和 ORM 混用from sqlalchemy import text result db.session.execute(text(SELECT COUNT(*) FROM users WHERE created_at :date), {date: week_ago}) count result.scalar()这也是一个值得记住的技巧ORM 不是银弹但也不是束缚。该用 ORM 的对象操作就用该写原生 SQL 时也别犹豫两者结合才是最高效的。最后想给你一个后续学习方向的建议。这一篇用的是 Flask-SQLAlchemy 的Model基类也就是所谓的经典风格 映射简单直观适合快速开发和中小型项目。如果你的项目结构比较复杂尤其是既要支持 MySQL 又可能切换到 PostgreSQL 或其他数据库可以了解一下 SQLAlchemy 的声明式映射和核心 API。此外建议尽早把Flask-Migrate用起来管理数据库结构变更避免后面在表结构更新上吃大亏。写接口时建议配合序列化库如Flask-RESTful或Marshmallow处理 JSON 输出配合自定义错误处理器实现统一的异常返回格式这样整个项目的开发体验会再上一个台阶。后续有时间我再单独写一篇关于 Flask 如何配置项目结构、校验请求参数、统一返回格式的文章这些在真实项目里比增删改查本身复杂得多。