作为写过不少Flask项目的开发,我越来越意识到一个现实:能跑通页面的Flask代码,和能真正做成业务的Flask代码,中间隔着的往往是数据库。很多初学者把Flask模板、路由玩得很溜,一遇到数据持久化就卡壳——文件存储读到头发麻,SQL语句拼到崩溃,最后只能对着内存里的字典数据发呆。这篇博文就来填这个坑:怎么用Flask连接MySQL,并用ORM(对象关系映射)方式完成增删改查,让你彻底告别手写SQL拼接的原始生活,直接站上正规军的赛道。
这篇内容适合三类人:刚把Flask基础语法学完、想给项目加数据库的新手;写爬虫攒了一堆数据但只会存CSV、想升级到关系型数据库的工程师;以及做毕设项目需要完整增删改查功能、却对数据库交互一脸懵的同学。我会从环境搭建讲到模型设计,再到完整的增删改查实操,最后附上我踩过的几个深坑。学完这一篇,你能独立实现基于MySQL的用户数据、商品数据、列表分页这类常见功能,而且是可维护、可扩展的那种写法。
1. 内容整体设计与技术选型思路
1.1 为什么是Flask + MySQL + ORM这套组合
先聊清楚一个问题:为什么不直接写原生SQL连MySQL,非要绕一层ORM?我的真实体验是:原生SQL在小项目里确实快,但项目一复杂就变灾难。表关联一多,SQL语句全是字符串拼接,维护起来像在玩火。ORM的核心价值是把一张表映射成一个Python类,把一行记录映射成一个Python对象,你操作对象就是操作数据库,这个心智负担低太多了。
Flask本身不强制数据库方案,官方文档给的也是自由选择,但社区事实标准就是Flask-SQLAlchemy。它是在SQLAlchemy之上做的Flask集成封装,你不需要关心连接池怎么建、会话怎么管理,只要集中于业务代码,这部分杂事全被框架处理了。MySQL作为存储端也很合理:它成熟、开源、资料多,部署到服务器上几乎没有认知成本,出了问题一搜全是答案。三个组件各司其职,Flask管Web交互,MySQL管数据落地,SQLAlchemy管两边的翻译,这就是最务实的组合。
1.2 ORM和原生SQL到底差在哪
用一张表来对比,最直观:
| 维度 | 原生MySQL | 使用ORM(SQLAlchemy) |
|---|---|---|
| 开发效率 | 慢,每条业务都要手写SQL | 快,直接操作Python对象 |
| 可读性 | 一般,复杂SQL难理解 | 高,面向对象语义清晰 |
| 防注入 | 需要自己处理或参数化 | 自动参数化,默认安全 |
| 跨数据库 | 几乎不可移植 | 底层驱动替换即可切换 |
| 性能 | 精细操作可控性强 | 略有余量,但对常规业务无感知差异 |
这里我多说一句,ORM不是神,它并不是不用SQL了,而是帮你把SQL封装好。极端高性能场景下你依然可以用原生SQL绕过ORM,但99%的业务场景里,ORM的简洁性和安全性绝对值得这点性能换来的开发效率。
1.3 为什么不用SQLite而坚持MySQL
很多人一开始图省事用SQLite,但SQLite本质是单文件数据库,不支持并发写,线上部署容易出问题,而且表结构变更管理非常别扭。MySQL自带完善的事务机制、连接管理、权限体系,更贴近真实业务环境。从学习成本看,SQLite和MySQL的SQL语法基本兼容,迁移难度很小。与其项目写完再含泪替换数据库,不如第一步就走上正轨。
2. 环境准备与核心配置
2.1 安装依赖
先创建虚拟环境,这是Python开发的基本功。虚拟环境隔离项目依赖,免得不同项目之间包版本冲突。进入虚拟环境后安装三个包:
pip install flask pip install Flask-SQLAlchemy pip install PyMySQLPyMySQL是MySQL的纯Python驱动,相比mysqlclient最大的优势是安装简单,不需要编译依赖,Windows和Linux环境都友好,对新手最省心。如果你用的是Web服务器部署,生产环境想追求极致性能,可以换mysqlclient,但这里先以PyMySQL为标准实践。
装完后可以用
pip list确认一下版本。我建议Flask-SQLAlchemy装 3.x 以上版本,新版API更简洁,旧版2.x的用法很多教程还在用,但现在已经不推荐了。
2.2 数据库连接URI配置拆解
这是整套配置的核心,很多新手死在这一步。连接URI是一个字符串,告诉SQLAlchemy该连哪里、用什么驱动、怎么认证。格式如下:
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://用户名:密码@主机IP:端口/数据库名?charset=utf8mb4'逐段拆开解释:
mysql+pymysql表示用PyMySQL驱动连接MySQL数据库用户名:密码填写你MySQL里的账号和密码,注意中间用英文冒号分隔@主机IP:端口本地开发一般写localhost:3306,3306是MySQL默认端口/数据库名必须是你已经创建好的库名,连接前先在MySQL客户端建库,比如CREATE DATABASE flask_demo DEFAULT CHARACTER SET utf8mb4;?charset=utf8mb4指定字符集为utf8mb4,这个参数非常重要,能存表情符号和各类特殊字符
这里有个我踩过的经典坑:如果密码里包含@、:、#这类特殊符号,直接写进URI会解析失败。解决办法是用Python自带的urllib.parse.quote_plus编码一下密码:
from urllib.parse import quote_plus password = quote_plus('密码@2024') app.config['SQLALCHEMY_DATABASE_URI'] = f'mysql+pymysql://root:{password}@localhost:3306/flask_demo?charset=utf8mb4'2.3 Flask-SQLAlchemy初始化方式
连接URI配置好之后,还需要实例化SQLAlchemy对象并绑定到Flask应用上。新版Flask-SQLAlchemy推荐的写法是:
from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_demo?charset=utf8mb4' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app)SQLALCHEMY_TRACK_MODIFICATIONS这个配置很多人不理解,它控制是否追踪对象修改并发送信号。设成False可以减少内存开销,是官方推荐的性能优化项,如果你没特殊需求,照抄关闭就行。
如果你的项目用了大型Flask应用结构(比如蓝本模式),就用另一种方式,先不绑定,初始化以后再绑定:
db = SQLAlchemy() def create_app(): app = Flask(__name__) app.config.from_object(Config) db.init_app(app) db.create_all() return app两种方式殊途同归,小项目用第一种最简单,但结构复杂时用第二种更规范。我个人建议在一开始就养成db = SQLAlchemy()加db.init_app(app)的习惯,后面拆模块会轻松很多。
2.4 建库与连接测试
写代码前先在MySQL里把库建好。这一步不要偷懒,库名、字符集都要确认。命令行进去,或者用可视化工具都可以:
CREATE DATABASE IF NOT EXISTS flask_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;建好库之后,在代码里加上启动后的表创建逻辑:
with app.app_context(): db.create_all()db.create_all()必须放在应用上下文里执行,否则会报 "Working outside of application context" 的错误。这是Flask-SQLAlchemy 3.x里常见的坑,因为3.x不再自动处理上下文。执行完这行代码,你可以去MySQL客户端里SHOW TABLES;看看有没有生成对应的表。
注意:
create_all()只会创建不存在的表,不会更新已存在的表的字段。也就是说,字段新增了,表结构并不会自动同步。这个特性后面我会单独说,初学阶段只要知道它不是迁移工具就好。
3. ORM模型设计
3.1 模型字段与约束设计
ORM里,一张表就是Python中的一个类。定义一个模型,就是写一个继承自db.Model的类,类属性就是字段。下面是一个典型的用户模型:
class User(db.Model): __tablename__ = 'user' id = db.Column(db.Integer, primary_key=True, autoincrement=True) username = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) def __repr__(self): return f'<User {self.username}>'__tablename__指定数据库里的真实表名,如果不写,SQLAlchemy会按类的默认规则生成表名,但显式指定更可控。db.Column里每个参数都有讲究:
db.Integer/db.String(80)是字段类型,决定了MySQL里对应什么数据类型,String必须指定长度primary_key=True设置主键autoincrement=True表示自增,MySQL里就是AUTO_INCREMENT,适合整型主键unique=True生成唯一索引,用户名和邮箱都不该重复nullable=False表示非空,插入时缺了这字段会直接报错default设置默认值,db.func.current_timestamp()是数据库当前时间
建完模型,在Python交互环境或者代码中调用db.create_all()后,MySQL里就会出现一张user表。
3.2 关联关系的简单建模
日常项目不会只有一张表,用户和文章、订单和商品都会发生关联。以用户和文章为例,一个用户能拥有多篇文章,这就是典型的一对多关系:
class Article(db.Model): __tablename__ = 'article' id = db.Column(db.Integer, primary_key=True, autoincrement=True) title = db.Column(db.String(200), nullable=False) content = db.Column(db.Text) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) author = db.relationship('User', backref=db.backref('articles', lazy='dynamic'))这里db.ForeignKey('user.id')是数据库层面的外键约束,db.relationship是在ORM层面建立关系。加了这行以后,你可以通过user.articles拿到这个用户的全部文章,也可以通过article.author直接拿到作者对象,非常方便。
3.3 模型建好后如何使用
模型定义好,只是创建了一张空表。真正做事要进入db.session的世界。这里要先建立一个概念:db.session就像是一个工作区的缓冲区,你往里面add对象,相当于把数据放进了工作区,还没真正写入磁盘。只有执行db.session.commit(),你才对数据库提交了事务。这个设计类似你编辑文档时先保存在内存里,再点"保存"按钮,道理是一样的。
理解了session,接下来就是增删改查的完整实操。
4. 增删改查完整实操
4.1 增加数据的标准流程
新增一条用户记录的代码是最经典的:
user1 = User(username='张三', email='zhangsan@example.com', password_hash='hashed_pwd') db.session.add(user1) db.session.commit()两步缺一不可:add把对象加入会话,commit提交事务。忘了commit的话,服务一重启数据就没了,这是新手最常犯的错误之一。
如果要批量插入多条记录,用add_all更高效:
user2 = User(username='李四', email='lisi@example.com', password_hash='hashed_pwd_2') user3 = User(username='王五', email='wangwu@example.com', password_hash='hashed_pwd_3') db.session.add_all([user2, user3]) db.session.commit()也是一次commit就全部落库,但相比逐条add再多次commit,add_all提交的是一个事务,要么全成功,要么全失败,不会出现一半写入一半丢失的数据裂痕。
4.2 查询数据的各种姿势
查询是CRUD中使用频率最高的操作,SQLAlchemy提供的API足够覆盖绝大多数需求:
# 查询所有用户 users = User.query.all() # 查询第一条记录 user = User.query.first() # 按主键查询,不存在返回None user = User.query.get(1) # 按某个字段精确查询,返回第一条或None user = User.query.filter_by(username='张三').first() # 按条件过滤,返回列表 users = User.query.filter(User.email.like('%@example.com')).all() # 多条件且关系 users = User.query.filter(User.username != '李四', User.id > 1).all()filter_by和filter的区别要记牢:filter_by只支持等于号这种简单的等值比较,用法像字典字段名;filter支持大于、小于、模糊、各种复杂条件,字段必须用类名引用。简单说,复杂查询用filter,简单等值查询用filter_by。
分页查询也是高频需求,SQLAlchemy自带paginate方法:
page_obj = User.query.paginate(page=1, per_page=10, error_out=False) items = page_obj.items total = page_obj.total has_prev = page_obj.has_preverror_out=False很关键,表示页码越界时返回空列表而不是抛404错误,否则传个离谱页码页面直接崩。
4.3 更新数据的两种路径
更新数据的思路有两种,一种是先查出来再改,适合单条精细修改:
user = User.query.get(1) if user: user.username = '张三丰' db.session.commit()修改对象属性后,一样要commit,这叫"编辑后保存"。SQLAlchemy会自动追踪对象变化,提交时生成UPDATE语句。
另一种是批量更新,直接通过查询条件更新,不需要先把对象加载出来:
User.query.filter_by(username='张三').update({'username': '张三丰'}) db.session.commit()批量更新比较适合用户改密码、批量改状态这种场景,注意传的是字典,键名是字段名。两种方式没有绝对优劣,单条修改用第一种直观,批量修改用第二种高效。
4.4 删除数据与级联关系
删除同样有两条路径。先查后删,适合精确删除单条:
user = User.query.get(1) if user: db.session.delete(user) db.session.commit()批量删除:
User.query.filter(User.id > 5).delete() db.session.commit()如果你建了外键关联,比如User外键到Article,删除有文章的用户时,MySQL会因外键约束报错。你需要在关联字段上配置级联删除行为,或者在业务代码里先清掉依赖数据。这里的关键是:外键不是越多越好,生产环境反而经常通过业务代码控制,不用数据库级联约束,具体情况具体取舍。
4.5 事务回滚与异常处理
数据操作不是永远成功的,重名用户、重复邮箱、字段超长都会导致提交失败。实际开发里,最稳妥的写法是把操作包在异常处理里:
try: user1 = User(username='张三', email='zhangsan@example.com', password_hash='pwd') db.session.add(user1) db.session.commit() except Exception as e: db.session.rollback() print(f'操作失败,已回滚:{e}')rollback()是事务的保护伞。一旦操作失败,你调用它就能把数据恢复到操作前的状态,避免脏数据残留在session里。不调用的话,后续的业务操作都会被这个失败的session污染,出现各种诡异报错。我建议所有涉及写入的操作,都带上异常处理加回滚。
5. 常见问题与排查技巧
5.1 数据库连接报错排查
我整理了一张我自己经常对照的排查表,遇到连接类问题先别慌,对着查:
| 现象 | 原因 | 解决方案 |
|---|---|---|
Access denied for user | 用户名或密码错误 | 检查URI里的账号密码,特别是特殊字符需要URL编码 |
Unknown database | 数据库没有创建 | 先用MySQL客户端建库,别指望SQLAlchemy帮你造库 |
Can't connect to MySQL server | MySQL服务没启动或端口不对 | 确认MySQL启动状态,核对端口号 |
ModuleNotFoundError: No module named 'pymysql' | 没安装PyMySQL | pip install PyMySQL补齐依赖 |
charset相关报错 | URI缺字符集参数 | URI末尾加上?charset=utf8mb4 |
还有一个隐藏问题:MySQL8.0默认的认证插件是caching_sha2_password,老版本的PyMySQL有可能认不了这个认证方式。遇到Authentication plugin 'caching_sha2_password' cannot be loaded这类报错,优先升级PyMySQL最新版,或者把MySQL用户的认证插件改回mysql_native_password,二选一即可。
5.2 表一直不生成怎么办
db.create_all()执行后,表没出现,常见原因有三个。第一,app_context没包住,解决办法是用with app.app_context():包住调用。第二,模型类没被导入,Python不导入源文件就不会注册模型,你需要显式from models import User这种操作。第三,数据库连接成功但表被判定为已存在,如果你改过表结构再跑create_all(),它不会修改旧表,这时候你得手动DROP TABLE或者用迁移工具。
5.3 Flask-SQLAlchemy版本导致的API差异
网上大量教程还在用旧版写法,难免会遇到版本不匹配的坑。2.x版本里db.session可以直接在应用启动时解析,3.x版本则必须放到应用上下文里。还有一个经典差异:2.x里db.engine可以直接访问,3.x则没那么方便。我的建议是:安装时指定版本pip install Flask-SQLAlchemy==3.1.1,跟着3.x官方习惯走,别被网上老教程带偏。遇到AttributeError找不到某个属性的时候,优先去查当前版本对应文档。
5.4 数据乱码与字符集问题
往MySQL写中文出现???或乱码,基本是字符集没对齐。确保三处都是utf8mb4:数据库建库时指定、连接URI带?charset=utf8mb4、MySQL配置文件里character-set-server=utf8mb4。我见过好多次,代码写得完全没问题,但建库时没指定字符集,默认落在latin1上,中文全变问号。检查字符集时用这个SQL确认:
SHOW CREATE DATABASE flask_demo; SHOW CREATE TABLE user;如果发现不是utf8mb4,可以用ALTER DATABASE flask_demo CHARACTER SET utf8mb4;修正。
6. 实操经验与建议
数据库这块踩过几次坑之后,我养成了几个习惯,同样建议你保持。
开发阶段打开SQLAlchemy的日志输出,能看到每次操作实际执行的SQL语句,排查问题效率翻倍:
app.config['SQLALCHEMY_ECHO'] = TrueSQLALCHEMY_ECHO设为True之后,控制台会打印所有SQL语句,你会发现ORM帮你生成的查询和你预期一致与否,这个调试技巧非常香,生产环境记得关掉。
还有一个经验:不要在生产环境用db.create_all()。它在开发初期方便,但表结构根本不能优雅演进。项目一旦进入正式阶段,迁移工具才是正解。SQLAlchemy官方推荐的做法是配合Alembic,它可以像Git一样记录表结构变更历史,执行迁移升级和回滚。Alembic虽然初学难懂,但它是你从半吊子选手变成正规军的必经之路。
最后提一嘴性能问题。ORM虽然方便,但不要在循环里逐条commit,那是最笨的写法,频繁建立事务会拖垮性能。批量写操作用add_all合并提交,大批量数据更推荐用原生INSERT或者分批次提交。查询时尽量指定需要的列,避免无意义加载大字段。这些习惯不需要刻意学,写多了自然就懂了。
这一套下来,你已经有能力围绕Flask搭建一个基本的MySQL数据服务了。从配置连接到模型设计,再到增删改查实操,全部打通之后,你会发现复杂的业务逻辑其实就建立在简单的数据操作之上。接下来可以继续研究分页优化、查询性能、多表关联设计,技术树一旦铺开,能做的事情就多起来了。