Skip to content

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.6
python
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    # 进入带上下文的交互 Shell

PyCharm 中直接点底部 Terminal 即可进入项目目录的命令行。

Flask-SQLAlchemy:定义模型

bash
pip install flask-sqlalchemy==2.4.1
python
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.2
python
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 即可;生成的脚本可手工定制。

实践建议

  1. 生产环境把 SQLALCHEMY_DATABASE_URI 换成 MySQL/PostgreSQL,代码无需改动(ORM 的好处)
  2. 一对多关联:数据量大时用"反向关联赋值"(article.author = user)避免加载整个列表
  3. 模型结构会变 → 从一开始就用 Flask-Migrate,别用 create_all 硬建表
  4. 外键字段加 index=True 提升查询效率

一句话总结

用 ORM 别手写 SQL——模型继承 db.Model,增删改查是 db.session.add/commitModel.query.filter_by().all();改表结构一定用 Flask-Migrate(db migrate + db upgrade),别指望 create_all 保住已有数据。

相关概念