Appearance
Flask 应用存数据靠 SQL 数据库,但直接写 SQL 不便于维护且有注入风险——本章用 Flask-SQLAlchemy(ORM) 以对象方式操作 SQLite:定义模型、增删改查、模型关联、分页,并用 Flask-Script 提供命令行入口、Flask-Migrate 做数据库版本迁移。
为什么用 ORM 而非直接写 SQL
- SQL 数据库:数据存在数据表(表格)中,字段=列、对象=行
- 直接写 SQL 的痛点:每个模块要写多条增删改查语句、不便于复杂操作、存在 SQL 注入风险
- ORM(对象关系映射):把数据映射为对象,SQL 生成高度封装,操作对象属性即可完成增删改查,无需学 SQL 语法
本章以 SQLite 为例(无需服务器,单文件 data.sqlite),配 SQLiteStudio 可视化工具查看。
Flask-Script:命令行托管
Flask-Script 提供命令行工具管理 Flask 应用(启动、shell、自定义命令):
bash
pip install flask-script==2.0.6python
from flask_script import Manager, Shell
app = Flask(__name__)
manager = Manager(app) # 用 Manager 托管 app
@manager.command # 注册自定义命令
def test():
print('这是一个测试用的命令')
def make_shell_context(): # 向 Shell 注册上下文对象(类似环境变量)
return dict(db=db, UserModel=UserModel)
manager.add_command("shell", Shell(make_context=make_shell_context))
if __name__ == '__main__':
manager.run() # 替代 app.run()bash
python app.py # 查看可用命令列表
python app.py shell # 进入带上下文的交互 ShellPyCharm 中直接点底部 Terminal 即可进入项目目录的命令行。
Flask-SQLAlchemy:定义模型
bash
pip install flask-sqlalchemy==2.4.1python
import os
from flask_sqlalchemy import SQLAlchemy
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 关闭修改追踪
db = SQLAlchemy(app)
db.init_app(app)模型继承 db.Model,字段用 db.Column 定义:
python
class UserModel(db.Model):
__tablename__ = 'user'
id = db.Column(db.INTEGER, primary_key=True, autoincrement=True)
username = db.Column(db.VARCHAR, unique=True)
password = db.Column(db.VARCHAR)
is_admin = db.Column(db.BOOLEAN, default=False, nullable=False)
class ArticleModel(db.Model):
__tablename__ = 'article'
id = db.Column(db.INTEGER, primary_key=True)
title = db.Column(db.VARCHAR)
content = db.Column(db.VARCHAR)建表:进入 python app.py shell 后执行 db.create_all()(删除则 db.drop_all()),项目目录生成 data.sqlite。
模型基类(可选,优化输出)
定义 BaseModel(Model) 重写 __repr__,让 print(user) 直接显示字段值(默认只显示 <UserModel 1>):
python
class BaseModel(db.Model):
__abstract__ = True
query: BaseQuery
def __init__(self, **kwargs):
super().__init__(**kwargs)
def __repr__(self):
fields = []
for k, v in self.__dict__.items():
if k[0] != '_':
if isinstance(v, BaseModel):
fields.append('%s=<%s ...>' % (k, v.__class__.__name__))
elif isinstance(v, str):
fields.append("%s='%s'" % (k, v))
else:
fields.append('%s=%a' % (k, v))
return '<%s %s>' % (self.__class__.__name__, ' '.join(fields))数据操作(增删改查)
增
python
user_1 = UserModel(username='admin', password='123456', is_admin=True)
db.session.add(user_1) # 加入操作队列(此时未入库)
db.session.commit() # 提交到数据库查
python
UserModel.query.all() # 全部
UserModel.query.filter_by(is_admin=True).all() # 条件查询
UserModel.query.filter_by(is_admin=True).count() # 数量
UserModel.query.filter(UserModel.id > 1).first() # 取第一个
UserModel.query.get(2) # 按主键获取改 / 删
python
user = UserModel.query.get(2)
user.password = 'admin' # 改:赋值属性
db.session.add(user)
db.session.commit()
db.session.delete(user) # 删
db.session.commit()高级查询
python
ArticleModel.query.offset(2).all() # 跳过前 2 条
## 排序、包含判断、and/or 复合条件查询同理,见教材示例模型关联
一对多(用户 ↔ 文章)
文章模型加外键,用户模型加反向关联:
python
class ArticleModel(db.Model, BaseModel):
__tablename__ = 'article'
# ...
author_id = db.Column(db.INTEGER, db.ForeignKey(UserModel.id), index=True)
author: UserModel # 类型声明,便于 IDE 识别
class UserModel(db.Model, BaseModel):
# ...
articles: list = db.relationship('ArticleModel', backref='author', uselist=True)两种关联添加方式:
| 方式 | 代码 | 特点 |
|---|---|---|
| 通过用户关联属性 | user_1.articles.append(ArticleModel(...)) | 直观简洁;关联列表长时查询多、效率低 |
| 通过文章反向关联 | article.author = user_2 | 不加载整个列表、效率高;需设反向属性 |
一对一(用户 ↔ 用户信息)
不修改用户模型即可扩展信息:UserInfoModel 外键设为主键(一对一),用户模型 uselist=False + cascade='all'(级联删除):
python
class UserInfoModel(db.Model, BaseModel):
__tablename__ = 'user_info'
user_id = db.Column(db.INTEGER, db.ForeignKey(UserModel.id), primary_key=True)
sex = db.Column(db.INTEGER)
introduce = db.Column(db.TEXT)
class UserModel(db.Model, BaseModel):
# ...
info = db.relationship('UserInfoModel', backref='user', uselist=False, cascade='all')三种关联方式:构造参数直接关联(简洁)/ 属性赋值关联(灵活)/ 反向关联(需理解模型关系)。
数据分页
python
@app.route('/paginator')
def paginator():
pagination = ArticleModel.query.paginate(per_page=3) # 每页 3 条
return render_template('paginator.html', pagination=pagination)模板用 Flask-Bootstrap 现成分页控件:
html
{% extends 'bootstrap/base.html' %}
{% import 'bootstrap/pagination.html' as pagi %}
{% for item in pagination.items %}
<h1>{{ item.title }}</h1>
{% endfor %}
{{ pagi.render_pagination(pagination) }}Flask-Migrate:数据库迁移
用 db.create_all() 无法应对"改表结构且不丢数据"的场景;Flask-Migrate 提供数据库版本控制:自动生成迁移脚本、升级/回滚、不丢已有数据。
bash
pip install flask-migrate==2.5.2python
from flask_migrate import Migrate, MigrateCommand
migrate = Migrate(app, db)
migrate.init_app(app, render_as_batch=True) # batch 渲染,性能优化
manager.add_command('db', MigrateCommand) # 挂到 Flask-Script 的 db 命令核心命令(python app.py 下):
bash
python app.py db init # 初始化迁移仓库(生成 migrations/ 目录)
python app.py db migrate # 检测模型改动,生成迁移脚本(versions/ 下)
python app.py db upgrade # 应用升级脚本到数据库
python app.py db downgrade # 回滚(降级)每次修改模型后重复 db migrate + db upgrade 即可;生成的脚本可手工定制。
实践建议
- 生产环境把
SQLALCHEMY_DATABASE_URI换成 MySQL/PostgreSQL,代码无需改动(ORM 的好处) - 一对多关联:数据量大时用"反向关联赋值"(
article.author = user)避免加载整个列表 - 模型结构会变 → 从一开始就用 Flask-Migrate,别用 create_all 硬建表
- 外键字段加
index=True提升查询效率
一句话总结
用 ORM 别手写 SQL——模型继承 db.Model,增删改查是 db.session.add/commit 与 Model.query.filter_by().all();改表结构一定用 Flask-Migrate(db migrate + db upgrade),别指望 create_all 保住已有数据。