Flask入门学习教程,从入门到精通,数据库操作 — 知识点详解与案例代码(4)

数据库操作 --- 知识点详解与案例代码


一、数据库概述

1.1 什么是数据库

数据库(Database)是按照数据结构来组织、存储和管理数据的仓库。Web应用中,数据库用于持久化存储用户信息、文章内容、订单记录等数据。

1.2 常见数据库类型

类型 代表产品 特点
关系型数据库 MySQL、PostgreSQL、SQLite 表结构,支持SQL,支持事务
非关系型数据库 MongoDB、Redis 文档/键值存储,灵活Schema

1.3 ORM(对象关系映射)

ORM(Object Relational Mapping)是一种技术,将数据库中的表映射为Python类,表中的行映射为类的实例,表中的列映射为类的属性。

优点:

  • 不需要手写SQL语句
  • 用面向对象的方式操作数据库
  • 可以方便地切换不同数据库

二、安装Flask-SQLAlchemy

2.1 Flask-SQLAlchemy简介

Flask-SQLAlchemy 是 Flask 框架的 SQLAlchemy 扩展,简化了在 Flask 中使用 SQLAlchemy 的流程。

2.2 安装命令

bash 复制代码
# 安装 Flask-SQLAlchemy 核心库
pip install flask-sqlalchemy

# 安装 MySQL 驱动(用于连接 MySQL 数据库)
pip install pymysql

# 如果需要数据库迁移功能,还需安装 Flask-Migrate
pip install flask-migrate

2.3 安装验证

python 复制代码
# 验证 Flask-SQLAlchemy 是否安装成功
# 在Python交互环境中执行以下代码

import flask_sqlalchemy   # 导入flask_sqlalchemy模块
print(flask_sqlalchemy.__version__)  # 打印版本号,若无报错则安装成功

import pymysql             # 导入pymysql模块
print(pymysql.__version__)  # 打印pymysql版本号,验证MySQL驱动安装成功

三、连接数据库

3.1 数据库URI格式

Flask-SQLAlchemy 通过 数据库URI 来指定连接的数据库类型和参数。

复制代码
数据库URI的通用格式:
数据库类型://用户名:密码@主机地址:端口号/数据库名?charset=编码

常见数据库URI写法:

数据库 URI格式
MySQL mysql+pymysql://用户名:密码@localhost:3306/数据库名?charset=utf8mb4
SQLite sqlite:///数据库文件路径.db
PostgreSQL postgresql://用户名:密码@localhost:5432/数据库名

3.2 连接MySQL数据库 --- 完整案例

python 复制代码
# ============================================================
# 案例:连接MySQL数据库的完整配置
# 文件名:app.py
# ============================================================

# ---------- 第一步:导入所需模块 ----------
from flask import Flask                          # 导入Flask核心类,用于创建Web应用实例
from flask_sqlalchemy import SQLAlchemy           # 导入Flask-SQLAlchemy扩展,用于操作数据库

# ---------- 第二步:创建Flask应用实例 ----------
app = Flask(__name__)                             # 创建Flask应用实例,__name__表示当前模块名

# ---------- 第三步:配置数据库连接参数 ----------

# 配置数据库连接URI(SQLAlchemy连接字符串)
# 格式:mysql+pymysql://用户名:密码@主机:端口/数据库名?charset=编码
# 'root' 是MySQL的用户名
# '123456' 是MySQL的密码
# 'localhost' 是数据库服务器地址(本机)
# '3306' 是MySQL默认端口号
# 'flask_db' 是要连接的数据库名称(需提前创建)
# 'charset=utf8mb4' 指定字符集为utf8mb4,支持中文和emoji
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'

# 设置数据库追踪修改(True会追踪对象修改并发送信号,会额外消耗内存)
# Flask-SQLAlchemy 3.x中此配置已废弃,默认不追踪,建议设为False
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# 设置SQL语句回显(True会在控制台打印所有执行的SQL语句,方便调试)
# 开发环境设为True,生产环境应设为False
app.config['SQLALCHEMY_ECHO'] = True

# 设置数据库连接池大小(默认值为5)
# 连接池维护的数据库连接数量,避免频繁创建和关闭连接
app.config['SQLALCHEMY_POOL_SIZE'] = 10

# 设置连接池超时时间(秒),超过此时间未使用的连接将被回收
app.config['SQLALCHEMY_POOL_RECYCLE'] = 3600

# ---------- 第四步:创建数据库对象 ----------
db = SQLAlchemy(app)     # 创建SQLAlchemy实例,传入Flask应用,绑定数据库配置

# ---------- 第五步:测试数据库连接 ----------
with app.app_context():               # 手动推送应用上下文(Flask-SQLAlchemy 3.x需要)
    try:
        # db.engine 是SQLAlchemy的数据库引擎对象
        # .connect() 尝试获取一个数据库连接
        with db.engine.connect() as conn:       # 获取数据库连接
            print("数据库连接成功!")             # 连接成功则打印提示
    except Exception as e:                       # 捕获所有异常
        print(f"数据库连接失败:{e}")             # 打印错误信息

# ---------- 启动应用 ----------
if __name__ == '__main__':                        # 判断是否是直接运行此文件
    app.run(debug=True)                           # 启动Flask开发服务器,开启调试模式

3.3 连接SQLite数据库 --- 完整案例

python 复制代码
# ============================================================
# 案例:连接SQLite数据库(无需安装额外驱动,适合学习和小型项目)
# ============================================================

from flask import Flask                           # 导入Flask核心类
from flask_sqlalchemy import SQLAlchemy            # 导入SQLAlchemy扩展

app = Flask(__name__)                              # 创建Flask应用实例

# SQLite数据库URI
# 'sqlite:///' 表示SQLite数据库协议
# './instance/mydb.db' 表示数据库文件的相对路径
# SQLite会自动创建数据库文件(如果不存在)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./instance/mydb.db'

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False  # 关闭修改追踪

db = SQLAlchemy(app)     # 创建数据库实例并绑定应用

# 打印确认信息
print("SQLite数据库配置完成")      # 输出配置完成提示
print(f"数据库URI: {app.config['SQLALCHEMY_DATABASE_URI']}")  # 打印数据库路径

if __name__ == '__main__':         # 判断是否直接运行
    app.run(debug=True)            # 启动应用

四、定义模型

4.1 模型基本概念

模型(Model) 是数据库表在Python中的映射。每个模型类对应数据库中的一张表,类属性对应表的字段(列)。

4.2 常用字段类型

字段类型 说明 对应Python类型
db.Integer 整数 int
db.String(n) 变长字符串,n为最大长度 str
db.Text 长文本,无长度限制 str
db.Float 浮点数 float
db.Boolean 布尔值 bool
db.Date 日期 datetime.date
db.DateTime 日期时间 datetime.datetime
db.Time 时间 datetime.time
db.LargeBinary 二进制数据 bytes
db.Enum 枚举类型 str
db.Numeric(p,s) 精确数值,p为总位数,s为小数位 Decimal

4.3 常用字段约束参数

参数 说明
primary_key=True 设为主键
autoincrement=True 自动递增(配合主键使用)
nullable=False 不允许为空(默认为True允许为空)
unique=True 值唯一
default=值 设置默认值
index=True 创建索引,加快查询速度
comment='说明' 字段注释

4.4 定义模型 --- 完整案例

python 复制代码
# ============================================================
# 案例:定义用户模型(User)和文章模型(Article)
# 完整展示了各种字段类型的使用
# ============================================================

from flask import Flask                           # 导入Flask核心类
from flask_sqlalchemy import SQLAlchemy            # 导入SQLAlchemy扩展
from datetime import datetime                      # 导入datetime模块,用于时间字段
import enum                                       # 导入enum模块,用于枚举类型

app = Flask(__name__)                              # 创建Flask应用

# 数据库配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False  # 关闭修改追踪

db = SQLAlchemy(app)     # 创建数据库实例

# ---------- 自定义枚举类型 ----------
# 定义用户状态的枚举类,继承enum.Enum
class UserStatus(enum.Enum):       # 定义枚举类
    ACTIVE = 'active'              # 活跃状态,值为'active'
    INACTIVE = 'inactive'          # 未激活状态,值为'inactive'
    BANNED = 'banned'              # 被封禁状态,值为'banned'


# ---------- 定义用户模型 ----------
class User(db.Model):              # 继承db.Model,表示这是一个数据库模型类
    """用户模型,对应数据库中的user表"""

    # 指定表名(如果不指定,Flask-SQLAlchemy会自动使用类名的小写形式作为表名)
    __tablename__ = 'user'         # 显式指定表名为'user'

    # id字段:主键,整数类型,自动递增
    id = db.Column(
        db.Integer,                # 字段类型为整数
        primary_key=True,          # 设为主键
        autoincrement=True,        # 自动递增
        comment='用户ID,主键'       # 字段注释
    )

    # username字段:用户名,字符串类型,最大长度50,不可为空,唯一
    username = db.Column(
        db.String(50),             # 字段类型为字符串,最大长度50
        nullable=False,            # 不允许为空
        unique=True,               # 值必须唯一(不允许重复用户名)
        index=True,                # 创建索引,加快按用户名查询的速度
        comment='用户名'            # 字段注释
    )

    # email字段:邮箱,字符串类型,最大长度120,不可为空,唯一
    email = db.Column(
        db.String(120),            # 字段类型为字符串,最大长度120
        nullable=False,            # 不允许为空
        unique=True,               # 邮箱必须唯一
        comment='邮箱地址'          # 字段注释
    )

    # password字段:密码,字符串类型,最大长度255,不可为空
    password = db.Column(
        db.String(255),            # 字段类型为字符串,最大长度255
        nullable=False,            # 不允许为空
        comment='密码(加密存储)'    # 字段注释
    )

    # age字段:年龄,整数类型,允许为空
    age = db.Column(
        db.Integer,                # 字段类型为整数
        nullable=True,             # 允许为空(年龄可选)
        comment='年龄'              # 字段注释
    )

    # salary字段:薪水,精确数值(总共10位,小数2位),默认值0
    salary = db.Column(
        db.Numeric(10, 2),         # 精确数值类型,总10位,小数2位
        default=0.00,              # 默认值为0.00
        comment='薪水'              # 字段注释
    )

    # is_active字段:是否激活,布尔类型,默认为True
    is_active = db.Column(
        db.Boolean,                # 字段类型为布尔值
        default=True,              # 默认值为True(新用户默认激活)
        comment='是否激活'          # 字段注释
    )

    # status字段:用户状态,枚举类型,默认为ACTIVE
    status = db.Column(
        db.Enum(UserStatus),       # 字段类型为枚举
        default=UserStatus.ACTIVE, # 默认值为活跃状态
        comment='用户状态'          # 字段注释
    )

    # avatar字段:头像,二进制数据,允许为空
    avatar = db.Column(
        db.LargeBinary,            # 字段类型为二进制(存储图片等)
        nullable=True,             # 允许为空
        comment='头像数据'          # 字段注释
    )

    # bio字段:个人简介,长文本类型,允许为空
    bio = db.Column(
        db.Text,                   # 字段类型为长文本,无长度限制
        nullable=True,             # 允许为空
        default='',                # 默认为空字符串
        comment='个人简介'          # 字段注释
    )

    # created_at字段:创建时间,默认为当前时间
    created_at = db.Column(
        db.DateTime,               # 字段类型为日期时间
        default=datetime.now,      # 默认值为记录创建时的当前时间(注意:是datetime.now,不是datetime.now())
        comment='注册时间'          # 字段注释
    )

    # updated_at字段:更新时间,每次更新自动设置为当前时间
    updated_at = db.Column(
        db.DateTime,               # 字段类型为日期时间
        default=datetime.now,      # 默认值为当前时间
        onupdate=datetime.now,     # 当记录被修改时,自动更新为当前时间
        comment='最后更新时间'       # 字段注释
    )

    # ---------- 定义 __repr__ 方法,方便调试时查看对象信息 ----------
    def __repr__(self):
        """返回对象的可读字符串表示,用于调试"""
        return f'<User {self.username}>'   # 返回格式:<User 用户名>


# ---------- 定义文章模型 ----------
class Article(db.Model):           # 继承db.Model
    """文章模型,对应数据库中的article表"""

    __tablename__ = 'article'      # 指定表名为'article'

    # id字段:主键
    id = db.Column(
        db.Integer,                # 整数类型
        primary_key=True,          # 主键
        autoincrement=True,        # 自动递增
        comment='文章ID'            # 字段注释
    )

    # title字段:文章标题
    title = db.Column(
        db.String(200),            # 字符串,最大200个字符
        nullable=False,            # 不允许为空
        comment='文章标题'          # 字段注释
    )

    # content字段:文章内容
    content = db.Column(
        db.Text,                   # 长文本类型
        nullable=False,            # 不允许为空
        comment='文章内容'          # 字段注释
    )

    # view_count字段:浏览次数
    view_count = db.Column(
        db.Integer,                # 整数类型
        default=0,                 # 默认浏览次数为0
        comment='浏览次数'          # 字段注释
    )

    # is_published字段:是否已发布
    is_published = db.Column(
        db.Boolean,                # 布尔类型
        default=False,             # 默认未发布(草稿状态)
        comment='是否发布'          # 字段注释
    )

    # created_at字段:创建时间
    created_at = db.Column(
        db.DateTime,               # 日期时间类型
        default=datetime.now,      # 默认当前时间
        comment='创建时间'          # 字段注释
    )

    # ---------- 外键字段 ----------
    # user_id字段:外键,关联到user表的id字段
    user_id = db.Column(
        db.Integer,                # 整数类型(必须与关联的主键类型一致)
        db.ForeignKey('user.id'),  # 外键约束,引用user表的id字段
        nullable=False,            # 不允许为空(每篇文章必须属于一个用户)
        comment='作者ID(外键)'     # 字段注释
    )

    def __repr__(self):
        """返回文章对象的可读字符串表示"""
        return f'<Article {self.title}>'   # 返回格式:<Article 标题>

五、创建数据表

5.1 创建表的三种方式

方式一:使用 db.create_all() 创建(简单直接)
python 复制代码
# ============================================================
# 案例:使用 db.create_all() 创建所有数据表
# 这是最简单的创建方式,适合开发和学习阶段
# ============================================================

from flask import Flask                           # 导入Flask核心类
from flask_sqlalchemy import SQLAlchemy            # 导入SQLAlchemy扩展
from datetime import datetime                      # 导入datetime模块

app = Flask(__name__)                              # 创建Flask应用实例

# 配置数据库连接
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)     # 创建数据库实例

# ---------- 定义模型 ----------
class Student(db.Model):         # 学生模型,对应student表
    __tablename__ = 'student'    # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键,自增
    name = db.Column(db.String(50), nullable=False)                   # 姓名,不可为空
    age = db.Column(db.Integer)                                       # 年龄
    grade = db.Column(db.String(20))                                  # 班级
    created_at = db.Column(db.DateTime, default=datetime.now)         # 创建时间


# ---------- 创建所有数据表 ----------
with app.app_context():                # 推送应用上下文(Flask-SQLAlchemy 3.x必须)
    db.create_all()                    # 创建所有继承自db.Model的模型对应的表
                                       # 注意:如果表已存在,不会重新创建或修改
    print("所有数据表创建成功!")         # 打印创建成功提示
方式二:使用 Flask-Migrate 迁移工具创建(推荐用于正式项目)
python 复制代码
# ============================================================
# 案例:使用 Flask-Migrate 管理数据库迁移(推荐方式)
# 可以跟踪模型变化,自动生成ALTER TABLE等SQL语句
# ============================================================

# ---------- app.py 文件 ----------
from flask import Flask                           # 导入Flask核心类
from flask_sqlalchemy import SQLAlchemy            # 导入SQLAlchemy扩展
from flask_migrate import Migrate                  # 导入Flask-Migrate扩展,用于数据库迁移
from datetime import datetime                      # 导入datetime模块

app = Flask(__name__)                              # 创建Flask应用

# 数据库配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)                               # 创建数据库实例
migrate = Migrate(app, db)                         # 创建迁移实例,将Flask应用和数据库绑定

# ---------- 定义Teacher模型 ----------
class Teacher(db.Model):                           # 教师模型
    __tablename__ = 'teacher'                      # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)    # 主键
    name = db.Column(db.String(50), nullable=False)                     # 姓名
    subject = db.Column(db.String(100))                                 # 教学科目
    created_at = db.Column(db.DateTime, default=datetime.now)           # 创建时间


# ---------- 使用 Flask-Migrate 的命令行操作(在终端中执行)----------
# 步骤1:初始化迁移仓库(只需执行一次)
# >>> flask db init
# 说明:在项目根目录下创建migrations文件夹,用于存放迁移脚本

# 步骤2:生成迁移脚本(检测模型变化,生成对应SQL)
# >>> flask db migrate -m "创建teacher表"
# 说明:自动检测模型与数据库的差异,生成迁移脚本

# 步骤3:执行迁移(将变更应用到数据库)
# >>> flask db upgrade
# 说明:执行迁移脚本,在数据库中创建或修改表结构

# 其他常用命令:
# flask db downgrade    -- 回退到上一个迁移版本
# flask db history      -- 查看迁移历史
# flask db current      -- 查看当前数据库版本
方式三:直接使用SQLAlchemy原生方式创建
python 复制代码
# ============================================================
# 案例:使用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_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

class Course(db.Model):                          # 课程模型
    __tablename__ = 'course'                     # 表名
    id = db.Column(db.Integer, primary_key=True) # 主键
    name = db.Column(db.String(100))             # 课程名

# 使用SQLAlchemy原生方式创建表
with app.app_context():                          # 推送应用上下文
    # db.metadata 包含了所有已注册的模型元数据
    # create_all() 方法根据元数据创建所有表
    db.metadata.create_all(db.engine)            # 使用引擎创建所有表
    print("使用SQLAlchemy原生方式创建表成功!")

5.2 删除数据表

python 复制代码
# ============================================================
# 案例:删除数据表
# 警告:此操作会删除表及其中所有数据,无法恢复!
# ============================================================

with app.app_context():                          # 推送应用上下文
    db.drop_all()                                # 删除所有数据表(慎用!)
    # 只删除指定的表:
    # Course.__table__.drop(db.engine)           # 只删除course表

    print("数据表已删除")                         # 打印提示

六、模型关系

6.1 一对多关系(最常用)

说明: 一个用户可以有多篇文章(User 1 ------ N Article)

python 复制代码
# ============================================================
# 案例:一对多关系 ------ 一个用户可以有多篇文章
# ============================================================

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)


# ---------- 用户模型("一"的一方) ----------
class Author(db.Model):                          # 作者模型
    __tablename__ = 'author'                     # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    name = db.Column(db.String(50), nullable=False)                   # 作者名

    # ---------- 关键:定义关系属性 ----------
    # db.relationship() 定义模型之间的关系(注意:这不是数据库字段,是Python属性)
    # 第一个参数 'BlogPost':关联的目标模型类名(字符串形式)
    # backref='author':在BlogPost模型上自动添加一个反向引用属性,BlogPost.author 可获取对应的作者
    # lazy=True(默认):懒加载,访问 articles 时才执行SQL查询
    #   lazy='select'    ------ 懒加载(默认),访问时才查询
    #   lazy='joined'    ------ 立即使用JOIN加载(减少SQL查询次数)
    #   lazy='subquery'  ------ 使用子查询加载
    #   lazy='dynamic'   ------ 返回查询对象,可进一步链式查询(适合大量数据)
    # cascade='all, delete-orphan':级联操作,删除作者时同时删除其所有文章
    articles = db.relationship(
        'BlogPost',                              # 关联的目标模型
        backref='author',                        # 反向引用名
        lazy='dynamic',                          # 动态加载,返回查询对象
        cascade='all, delete-orphan'             # 级联删除
    )

    def __repr__(self):
        return f'<Author {self.name}>'


# ---------- 文章模型("多"的一方) ----------
class BlogPost(db.Model):                        # 博客文章模型
    __tablename__ = 'blog_post'                  # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    title = db.Column(db.String(200), nullable=False)                 # 标题
    body = db.Column(db.Text, nullable=False)                         # 正文
    created_at = db.Column(db.DateTime, default=datetime.now)         # 创建时间

    # ---------- 外键字段 ----------
    # db.ForeignKey('author.id') 表示此外键关联到 author 表的 id 字段
    # 这是建立一对多关系的数据库层面的关键
    author_id = db.Column(
        db.Integer,                            # 外键字段类型必须与被引用字段类型一致
        db.ForeignKey('author.id'),            # 引用 author 表的 id 字段
        nullable=False                         # 不允许为空
    )

    def __repr__(self):
        return f'<BlogPost {self.title}>'


# ---------- 演示一对多关系的操作 ----------
with app.app_context():
    db.create_all()                              # 创建所有表

    # 创建作者
    author1 = Author(name='张三')                # 创建作者"张三"
    db.session.add(author1)                      # 添加到会话
    db.session.commit()                          # 提交事务,此时 author1.id 已自动生成

    # 创建文章并关联到作者(方式一:通过外键字段直接设置)
    post1 = BlogPost(title='Flask入门', body='Flask是一个轻量级Web框架...', author_id=author1.id)
    post2 = BlogPost(title='Python基础', body='Python是一种简洁的语言...', author_id=author1.id)
    db.session.add_all([post1, post2])           # 批量添加多条记录
    db.session.commit()                          # 提交事务

    # 创建文章并关联到作者(方式二:通过 relationship 关系属性)
    post3 = BlogPost(title='数据库设计', body='数据库设计的基本原则...')
    author1.articles.append(post3)               # 通过关系属性直接追加文章
    db.session.commit()                          # 提交事务

    # 查询:通过作者获取其所有文章(正向查询)
    author = Author.query.get(1)                 # 获取id为1的作者
    print(f"作者:{author.name}")                 # 打印作者名
    all_posts = author.articles.all()            # 获取该作者的所有文章(.all()因lazy='dynamic'需要)
    for post in all_posts:                       # 遍历文章列表
        print(f"  文章:{post.title}")            # 打印每篇文章标题

    # 查询:通过文章获取其作者(反向查询,使用backref定义的属性)
    post = BlogPost.query.get(1)                 # 获取id为1的文章
    print(f"文章'{post.title}'的作者是:{post.author.name}")  # 通过反向引用获取作者

6.2 多对多关系

说明: 一个学生可以选多门课,一门课可以被多个学生选(Student N ------ N Course)

python 复制代码
# ============================================================
# 案例:多对多关系 ------ 学生选课系统
# 多对多关系需要通过一张中间表(关联表)来实现
# ============================================================

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# ---------- 方式一:使用 db.Table 创建中间表(简单场景推荐) ----------
# db.Table 创建的是一张纯关联表,没有对应的模型类
student_course = db.Table(
    'student_course',                            # 中间表的表名

    # 第一个外键:关联到student表
    db.Column(
        'student_id',                            # 中间表中的字段名
        db.Integer,                              # 字段类型
        db.ForeignKey('students.id'),            # 外键,引用students表的id
        primary_key=True                         # 设为联合主键的一部分
    ),

    # 第二个外键:关联到course表
    db.Column(
        'course_id',                             # 中间表中的字段名
        db.Integer,                              # 字段类型
        db.ForeignKey('courses.id'),             # 外键,引用courses表的id
        primary_key=True                         # 设为联合主键的一部分
    )
)


# ---------- 学生模型 ----------
class Student(db.Model):                         # 学生模型
    __tablename__ = 'students'                   # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    name = db.Column(db.String(50), nullable=False)                   # 学生姓名

    # 多对多关系定义
    # secondary=student_course:指定中间表(关联表)
    # backref='students':在Course模型上自动添加 students 反向引用
    # lazy='select':懒加载
    courses = db.relationship(
        'Course',                                # 关联的目标模型
        secondary=student_course,                # 指定中间表
        backref=db.backref('students', lazy=True), # 反向引用,lazy=True为懒加载
        lazy='select'                            # 懒加载
    )

    def __repr__(self):
        return f'<Student {self.name}>'


# ---------- 课程模型 ----------
class Course(db.Model):                          # 课程模型
    __tablename__ = 'courses'                    # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    name = db.Column(db.String(100), nullable=False)                  # 课程名称
    credit = db.Column(db.Integer, default=3)                          # 学分,默认3

    def __repr__(self):
        return f'<Course {self.name}>'


# ---------- 演示多对多关系操作 ----------
with app.app_context():
    db.create_all()                              # 创建所有表(包括中间表)

    # 创建学生
    s1 = Student(name='小明')                    # 创建学生小明
    s2 = Student(name='小红')                    # 创建学生小红
    s3 = Student(name='小刚')                    # 创建学生小刚

    # 创建课程
    c1 = Course(name='Python程序设计', credit=4) # 创建课程Python
    c2 = Course(name='数据库原理', credit=3)     # 创建课程数据库
    c3 = Course(name='Web开发', credit=3)        # 创建课程Web开发

    db.session.add_all([s1, s2, s3, c1, c2, c3]) # 批量添加所有记录
    db.session.commit()                          # 提交,获取各自的id

    # ---------- 给学生选课(建立多对多关系) ----------

    # 小明选了 Python 和 数据库
    s1.courses.append(c1)                        # 小明选Python(通过关系属性的append方法)
    s1.courses.append(c2)                        # 小明选数据库

    # 小红选了 Python 和 Web开发
    s2.courses.append(c1)                        # 小红选Python
    s2.courses.append(c3)                        # 小红选Web开发

    # 小刚选了所有课程
    s3.courses.append(c1)                        # 小刚选Python
    s3.courses.append(c2)                        # 小刚选数据库
    s3.courses.append(c3)                        # 小刚选Web开发

    db.session.commit()                          # 提交选课结果(自动插入中间表记录)

    # ---------- 查询:学生选了哪些课(正向查询) ----------
    student = Student.query.filter_by(name='小明').first()  # 查询小明
    print(f"\n{student.name}选的课程:")           # 打印标题
    for course in student.courses:               # 遍历小明选的所有课程
        print(f"  - {course.name}({course.credit}学分)")  # 打印课程名和学分

    # ---------- 查询:某门课有哪些学生选了(反向查询) ----------
    course = Course.query.filter_by(name='Python程序设计').first()  # 查询Python课程
    print(f"\n选了'{course.name}'的学生:")        # 打印标题
    for student in course.students:              # 通过反向引用查询选了此课的学生
        print(f"  - {student.name}")             # 打印学生姓名

    # ---------- 取消选课(移除关系) ----------
    s1.courses.remove(c2)                        # 小明退选数据库
    db.session.commit()                          # 提交变更
    print(f"\n小明退选数据库后,剩余课程:{[c.name for c in s1.courses]}")  # 打印剩余课程

6.3 一对一关系

说明: 一个用户对应一个用户详情(User 1 ------ 1 Profile)

python 复制代码
# ============================================================
# 案例:一对一关系 ------ 用户与其个人资料
# 一对一关系 = 一对多关系 + uselist=False
# ============================================================

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)


# ---------- 用户模型(主表) ----------
class User(db.Model):                            # 用户模型
    __tablename__ = 'user'                       # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    username = db.Column(db.String(50), unique=True, nullable=False)  # 用户名

    # 一对一关系
    # uselist=False 是关键:将一对多变成一对一
    # 表示User.profile返回的是单个Profile对象(而非列表)
    # backref='user':Profile对象可以通过 .user 访问对应的User
    profile = db.relationship(
        'Profile',                               # 关联的目标模型
        backref=db.backref('user', uselist=False), # 反向引用,也是一对一
        uselist=False,                            # 关键参数:设为False表示一对一
        cascade='all, delete-orphan'              # 级联删除,删除用户时删除其资料
    )


# ---------- 用户资料模型(从表) ----------
class Profile(db.Model):                         # 用户资料模型
    __tablename__ = 'profile'                    # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    real_name = db.Column(db.String(50))         # 真实姓名
    phone = db.Column(db.String(20))             # 电话号码
    address = db.Column(db.String(200))          # 地址
    user_id = db.Column(
        db.Integer,                              # 外键字段类型
        db.ForeignKey('user.id'),                # 外键,关联user表
        unique=True,                             # 唯一约束(保证一对一)
        nullable=False                           # 不允许为空
    )


# ---------- 演示一对一关系操作 ----------
with app.app_context():
    db.create_all()                              # 创建所有表

    # 创建用户
    user = User(username='zhangsan')             # 创建用户zhangsan
    db.session.add(user)                         # 添加到会话
    db.session.commit()                          # 提交,获取用户ID

    # 创建资料并关联(方式一:直接设置外键)
    profile = Profile(
        real_name='张三',                         # 真实姓名
        phone='13800138000',                     # 电话
        address='北京市朝阳区',                    # 地址
        user_id=user.id                          # 关联到用户
    )
    db.session.add(profile)                      # 添加资料到会话
    db.session.commit()                          # 提交

    # 创建资料并关联(方式二:通过关系属性)
    user2 = User(username='lisi')                # 创建另一个用户lisi
    db.session.add(user2)                        # 添加到会话
    db.session.commit()                          # 提交

    new_profile = Profile(real_name='李四', phone='13900139000')  # 创建资料(不设user_id)
    user2.profile = new_profile                  # 通过关系属性直接赋值(自动设置外键)
    db.session.commit()                          # 提交

    # 查询:通过用户获取资料
    u = User.query.filter_by(username='zhangsan').first()  # 查询zhangsan
    print(f"用户:{u.username}")                  # 打印用户名
    print(f"真实姓名:{u.profile.real_name}")      # 通过关系属性获取资料
    print(f"电话:{u.profile.phone}")              # 打印电话

    # 查询:通过资料获取用户(反向引用)
    p = Profile.query.filter_by(real_name='张三').first()  # 查询张三的资料
    print(f"资料所属用户:{p.user.username}")       # 通过反向引用获取用户名

七、数据操作 --- 增删改查(CRUD)

7.1 增加数据

python 复制代码
# ============================================================
# 案例:增加数据的多种方式
# ============================================================

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)


class Product(db.Model):                         # 商品模型
    __tablename__ = 'product'                    # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    name = db.Column(db.String(100), nullable=False)                  # 商品名
    price = db.Column(db.Float, nullable=False)                       # 价格
    stock = db.Column(db.Integer, default=0)                          # 库存
    category = db.Column(db.String(50))                               # 分类
    created_at = db.Column(db.DateTime, default=datetime.now)         # 创建时间

    def __repr__(self):
        return f'<Product {self.name} ¥{self.price}>'


# ---------- 演示各种增加数据的方式 ----------
with app.app_context():
    db.create_all()                              # 创建表

    # ============ 方式一:添加单条记录 ============
    # 步骤:创建对象 -> add() -> commit()
    product1 = Product(
        name='Python编程入门',                    # 商品名
        price=59.90,                             # 价格
        stock=100,                               # 库存
        category='图书'                           # 分类
    )
    db.session.add(product1)                     # 将对象添加到数据库会话中
    db.session.commit()                          # 提交会话,数据才真正写入数据库
    print(f"新增商品ID: {product1.id}")           # commit后,自增主键会自动赋值到对象的id属性

    # ============ 方式二:批量添加多条记录 ============
    # 使用 add_all() 一次添加多个对象(效率更高,减少数据库交互)
    products = [
        Product(name='无线鼠标', price=89.00, stock=200, category='电子产品'),
        Product(name='机械键盘', price=299.00, stock=50, category='电子产品'),
        Product(name='咖啡杯', price=35.00, stock=300, category='生活用品'),
        Product(name='笔记本', price=15.00, stock=500, category='办公用品'),
    ]
    db.session.add_all(products)                 # 批量添加所有对象到会话
    db.session.commit()                          # 一次性提交,效率高于多次add+commit
    for p in products:                           # 遍历所有新增商品
        print(f"  新增: {p.name} (ID: {p.id})")  # 打印每个商品的名称和自动生成的ID

    # ============ 方式三:先查询再添加(避免重复插入) ============
    existing = Product.query.filter_by(name='Python编程入门').first()  # 查询是否已存在
    if not existing:                             # 如果不存在
        new_product = Product(name='Python编程入门', price=59.90)  # 创建新商品
        db.session.add(new_product)              # 添加到会话
        db.session.commit()                      # 提交
        print("新商品已添加")                      # 打印提示
    else:                                        # 如果已存在
        print(f"商品已存在: {existing}")           # 打印已存在的商品信息

    # ============ 方式四:使用 session.add() 的字典方式创建 ============
    # 使用模型的构造函数传入字典参数
    data = {
        'name': '台灯',                          # 商品名
        'price': 128.00,                         # 价格
        'stock': 80,                             # 库存
        'category': '家居'                        # 分类
    }
    product_dict = Product(**data)               # **解包字典,等同于Product(name='台灯', price=128.00, ...)
    db.session.add(product_dict)                 # 添加到会话
    db.session.commit()                          # 提交
    print(f"通过字典创建商品: {product_dict}")      # 打印新商品

    # ============ 添加操作的注意事项 ============
    # 1. 如果 commit() 前发生异常,可以用 db.session.rollback() 回滚
    # 2. 添加重复的唯一字段值会抛出 IntegrityError 异常
    # 3. 建议使用 try-except 包裹数据库操作
    try:
        dup = Product(name='Python编程入门', price=49.90)  # 尝试插入重复商品名(如果有unique约束)
        db.session.add(dup)                      # 添加到会话
        db.session.commit()                      # 提交
    except Exception as e:                       # 捕获异常
        db.session.rollback()                    # 回滚事务,撤销未提交的变更
        print(f"添加失败,已回滚: {e}")             # 打印错误信息

7.2 查询数据

python 复制代码
# ============================================================
# 案例:查询数据的各种方式(最常用、最重要的部分)
# ============================================================

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime, timedelta          # 导入timedelta用于时间计算

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)


class Employee(db.Model):                        # 员工模型
    __tablename__ = 'employee'                   # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    name = db.Column(db.String(50), nullable=False)                   # 姓名
    department = db.Column(db.String(50))                             # 部门
    salary = db.Column(db.Float)                                      # 薪水
    age = db.Column(db.Integer)                                       # 年龄
    is_manager = db.Column(db.Boolean, default=False)                 # 是否是经理
    hire_date = db.Column(db.DateTime, default=datetime.now)          # 入职日期

    def __repr__(self):
        return f'<Employee {self.name} - {self.department}>'


# ---------- 准备测试数据 ----------
with app.app_context():
    db.create_all()                              # 创建表

    # 添加测试数据
    employees = [
        Employee(name='张三', department='技术部', salary=15000, age=30, is_manager=True),
        Employee(name='李四', department='技术部', salary=12000, age=25, is_manager=False),
        Employee(name='王五', department='市场部', salary=10000, age=28, is_manager=False),
        Employee(name='赵六', department='市场部', salary=18000, age=35, is_manager=True),
        Employee(name='孙七', department='人事部', salary=9000, age=26, is_manager=False),
        Employee(name='周八', department='技术部', salary=20000, age=32, is_manager=True),
        Employee(name='吴九', department='人事部', salary=11000, age=29, is_manager=False),
        Employee(name='郑十', department='市场部', salary=8500, age=23, is_manager=False),
    ]
    db.session.add_all(employees)                # 批量添加
    db.session.commit()                          # 提交

    # ============ 1. 查询所有记录 ============
    # all() 返回列表,包含所有记录
    all_employees = Employee.query.all()         # 查询所有员工
    print("=== 所有员工 ===")                     # 打印标题
    for emp in all_employees:                    # 遍历结果列表
        print(f"  {emp.name} | {emp.department} | ¥{emp.salary}")  # 打印每条记录

    # ============ 2. 根据主键查询单条记录 ============
    # get() 方法根据主键ID查询,不存在则返回None
    emp = Employee.query.get(1)                  # 查询主键为1的员工
    print(f"\n主键查询结果: {emp}")               # 打印结果

    # ============ 3. 条件查询 --- filter_by(等值查询) ============
    # filter_by 使用关键字参数进行等值过滤
    tech_employees = Employee.query.filter_by(department='技术部').all()  # 查询技术部所有员工
    print("\n=== 技术部员工 ===")                 # 标题
    for emp in tech_employees:                   # 遍历
        print(f"  {emp.name} - ¥{emp.salary}")   # 打印

    # ============ 4. 条件查询 --- filter(复杂条件查询) ============
    # filter 使用模型属性进行更复杂的条件判断
    # .like() 模糊查询:%匹配任意字符,_匹配单个字符
    result = Employee.query.filter(Employee.name.like('张%')).all()  # 查询姓"张"的员工
    print(f"\n姓张的员工: {[e.name for e in result]}")  # 打印

    # ============ 5. 比较查询 ============
    # 比较运算符:>, <, >=, <=, ==, !=
    high_salary = Employee.query.filter(Employee.salary > 12000).all()  # 查询薪水>12000的员工
    print(f"\n高薪员工: {[(e.name, e.salary) for e in high_salary]}")  # 打印

    # ============ 6. 多条件查询 --- and_ / or_ / not_ ============
    from sqlalchemy import and_, or_, not_       # 导入逻辑运算符

    # and_:所有条件都满足(也可以连续调用filter实现)
    result_and = Employee.query.filter(
        and_(                                    # AND组合条件
            Employee.department == '技术部',      # 部门是技术部
            Employee.salary > 13000               # 薪水大于13000
        )
    ).all()
    print(f"\n技术部高薪员工: {[(e.name, e.salary) for e in result_and]}")

    # or_:满足任一条件
    result_or = Employee.query.filter(
        or_(                                     # OR组合条件
            Employee.department == '技术部',      # 部门是技术部
            Employee.department == '市场部'       # 或部门是市场部
        )
    ).all()
    print(f"\n技术部+市场部员工: {[(e.name, e.department) for e in result_or]}")

    # not_:取反
    result_not = Employee.query.filter(
        not_(Employee.is_manager)                # 不是经理的员工
    ).all()
    print(f"\n非经理员工: {[e.name for e in result_not]}")

    # ============ 7. in_ 查询(包含在列表中) ============
    depts = ['技术部', '人事部']                   # 要查询的部门列表
    result_in = Employee.query.filter(
        Employee.department.in_(depts)            # department在列表中
    ).all()
    print(f"\n技术部+人事部员工: {[e.name for e in result_in]}")

    # ============ 8. between 查询(范围查询) ============
    result_between = Employee.query.filter(
        Employee.salary.between(10000, 15000)     # 薪水在10000到15000之间(包含边界)
    ).all()
    print(f"\n薪水10000-15000: {[(e.name, e.salary) for e in result_between]}")

    # ============ 9. 排序查询 order_by ============
    # 升序(默认)
    result_asc = Employee.query.order_by(Employee.salary.asc()).all()     # 按薪水升序
    print("\n按薪水升序:")
    for emp in result_asc:
        print(f"  {emp.name}: ¥{emp.salary}")

    # 降序
    result_desc = Employee.query.order_by(Employee.salary.desc()).all()   # 按薪水降序
    print("\n按薪水降序:")
    for emp in result_desc:
        print(f"  {emp.name}: ¥{emp.salary}")

    # 多字段排序
    result_multi = Employee.query.order_by(
        Employee.department.asc(),                # 先按部门升序
        Employee.salary.desc()                    # 再按薪水降序
    ).all()
    print("\n按部门升序+薪水降序:")
    for emp in result_multi:
        print(f"  [{emp.department}] {emp.name}: ¥{emp.salary}")

    # ============ 10. 限制数量 limit 和偏移 offset ============
    # 分页查询常用
    result_limit = Employee.query.order_by(Employee.salary.desc()).limit(3).all()   # 薪水最高的3人
    print(f"\n薪水最高的3人: {[(e.name, e.salary) for e in result_limit]}")

    result_offset = Employee.query.order_by(Employee.salary.desc()).offset(3).limit(3).all()  # 跳过前3个,取接下来3个
    print(f"第4-6名: {[(e.name, e.salary) for e in result_offset]}")

    # ============ 11. 查询单条记录 ============
    first_emp = Employee.query.filter_by(department='技术部').first()   # 返回第一条记录(无结果返回None)
    print(f"\n技术部第一个员工: {first_emp}")

    # one() ------ 必须恰好有一条记录,否则抛出异常
    try:
        one_emp = Employee.query.filter_by(id=1).one()                 # 正确:恰好一条
        print(f"唯一查询: {one_emp}")
    except Exception as e:
        print(f"查询异常: {e}")

    # ============ 12. 计数 count ============
    count = Employee.query.filter_by(department='技术部').count()       # 技术部员工总数
    print(f"\n技术部员工总数: {count}")

    # ============ 13. 判断是否存在 exists ============
    exists = Employee.query.filter_by(name='张三').first() is not None  # 判断张三是否存在
    print(f"张三是否存在: {exists}")

    # ============ 14. 只查询部分字段 values() ============
    # 只查询姓名和薪水,返回的是字典形式的行
    result_values = Employee.query.with_entities(
        Employee.name,                            # 只查姓名
        Employee.salary                           # 只查薪水
    ).all()
    print(f"\n只查姓名和薪水: {result_values}")

    # ============ 15. 去重查询 distinct ============
    result_distinct = Employee.query.with_entities(
        Employee.department                       # 只查部门字段
    ).distinct().all()                            # 去重
    print(f"\n所有部门(去重): {[d[0] for d in result_distinct]}")

    # ============ 16. 聚合查询(分组统计) ============
    from sqlalchemy import func                  # 导入聚合函数

    # func.count() 计数,func.sum() 求和,func.avg() 平均值,func.max() 最大值,func.min() 最小值
    result_group = db.session.query(
        Employee.department,                      # 分组字段
        func.count(Employee.id).label('count'),   # 每组人数,label设置别名
        func.avg(Employee.salary).label('avg_salary'),   # 每组平均薪水
        func.max(Employee.salary).label('max_salary'),   # 每组最高薪水
        func.min(Employee.salary).label('min_salary')    # 每组最低薪水
    ).group_by(                                 # GROUP BY 分组
        Employee.department                       # 按部门分组
    ).all()                                      # 执行查询

    print("\n=== 部门统计 ===")
    for dept, count, avg_sal, max_sal, min_sal in result_group:  # 遍历统计结果
        print(f"  {dept}: {count}人, 平均¥{avg_sal:.0f}, 最高¥{max_sal:.0f}, 最低¥{min_sal:.0f}")

    # ============ 17. having 分组过滤 ============
    # having 对分组后的结果进行过滤(类似WHERE但用于聚合结果)
    result_having = db.session.query(
        Employee.department,                      # 分组字段
        func.avg(Employee.salary).label('avg_sal')  # 平均薪水
    ).group_by(
        Employee.department                       # 按部门分组
    ).having(
        func.avg(Employee.salary) > 11000         # 只保留平均薪水>11000的组
    ).all()

    print(f"\n平均薪水>11000的部门: {[(dept, f'¥{avg:.0f}') for dept, avg in result_having]}")

    # ============ 18. 分页查询 paginate ============
    # paginate(page, per_page, error_out)
    # page: 当前页码(从1开始)
    # per_page: 每页显示的记录数
    # error_out: 页码超出范围时是否报错(默认True)
    pagination = Employee.query.order_by(Employee.id).paginate(
        page=1,                                   # 第1页
        per_page=3,                               # 每页3条记录
        error_out=False                           # 页码超出不报错,返回空列表
    )

    print(f"\n=== 分页查询(第{pagination.page}页,共{pagination.pages}页)===")
    print(f"  总记录数: {pagination.total}")       # 总记录数
    print(f"  当前页记录数: {len(pagination.items)}")  # 当前页的记录数
    print(f"  是否有上一页: {pagination.has_prev}")  # 是否有上一页
    print(f"  是否有下一页: {pagination.has_next}")  # 是否有下一页
    for emp in pagination.items:                  # 遍历当前页的数据
        print(f"  {emp.name} - {emp.department}")

    # ============ 19. 链式查询(方法链) ============
    # 可以将多个查询条件和方法串联起来
    result_chain = Employee.query\
        .filter(Employee.department == '技术部')   # 过滤:技术部
        .filter(Employee.salary > 10000)          # 过滤:薪水>10000
        .order_by(Employee.salary.desc())         # 排序:薪水降序
        .limit(2)                                 # 限制:最多2条
        .all()                                    # 执行并返回所有结果
    print(f"\n链式查询结果: {[(e.name, e.salary) for e in result_chain]}")

7.3 更新数据

python 复制代码
# ============================================================
# 案例:更新数据的多种方式
# ============================================================

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)


class Book(db.Model):                            # 图书模型
    __tablename__ = 'book'                       # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    title = db.Column(db.String(200), nullable=False)                 # 书名
    price = db.Column(db.Float, nullable=False)                       # 价格
    stock = db.Column(db.Integer, default=0)                          # 库存
    is_available = db.Column(db.Boolean, default=True)                # 是否上架

    def __repr__(self):
        return f'<Book {self.title} ¥{self.price} 库存:{self.stock}>'


with app.app_context():
    db.create_all()                              # 创建表

    # 添加测试数据
    books = [
        Book(title='红楼梦', price=45.00, stock=50),
        Book(title='西游记', price=38.00, stock=30),
        Book(title='水浒传', price=42.00, stock=40),
        Book(title='三国演义', price=50.00, stock=20),
    ]
    db.session.add_all(books)                    # 批量添加
    db.session.commit()                          # 提交

    # ============ 方式一:直接修改对象属性 ============
    # 最直观的方式:查询对象 -> 修改属性 -> 提交
    book = Book.query.get(1)                     # 查询ID为1的图书
    if book:                                     # 确保图书存在
        print(f"修改前: {book}")                  # 打印修改前的信息
        book.price = 55.00                       # 修改价格属性
        book.stock = 60                          # 修改库存属性
        db.session.commit()                      # 提交事务,SQL自动执行UPDATE语句
        print(f"修改后: {book}")                  # 打印修改后的信息

    # ============ 方式二:使用 update() 批量更新 ============
    # 适用于对符合条件的多条记录进行统一更新
    # 效率更高:只执行一条UPDATE SQL语句
    rows_updated = Book.query.filter(
        Book.price < 45                           # 条件:价格小于45的图书
    ).update({                                   # update() 接收字典参数
        'price': Book.price * 1.1,               # 将价格提升10%(注意:用模型属性而非Python值)
        'is_available': True                      # 同时设置为上架状态
    })
    db.session.commit()                          # 提交更新
    print(f"\n批量更新了 {rows_updated} 条记录")    # update()返回受影响的行数

    # ============ 方式三:使用 synchronize_session 参数 ============
    # synchronize_session 控如何更新Session缓存中的对象
    # 'fetch'(默认):更新前先查询旧值,更新Session中的对象
    # 'evaluate':使用SQL表达式在Python层面评估更新Session(默认推荐)
    # False:不更新Session中的对象(最快但可能导致Session数据不一致)
    rows = Book.query.filter(
        Book.stock < 30                           # 条件:库存<30
    ).update(
        {'stock': Book.stock + 100},             # 库存加100
        synchronize_session='evaluate'           # 使用evaluate方式同步Session
    )
    db.session.commit()                          # 提交
    print(f"补充库存 {rows} 本图书")

    # ============ 方式四:先查询再逐一修改(适合需要复杂逻辑的场景) ============
    all_books = Book.query.filter(Book.is_available == True).all()  # 查询所有上架图书
    for b in all_books:                          # 遍历每本书
        if b.price > 50:                         # 如果价格>50
            b.price = round(b.price * 0.9, 2)    # 打9折(保留2位小数)
            print(f"  {b.title} 打折至 ¥{b.price}")  # 打印折扣信息
    db.session.commit()                          # 提交所有修改

    # ============ 方式五:使用 merge() 更新 ============
    # merge() 可以将一个"游离"的对象合并到Session中
    # 如果数据库中已存在对应记录则更新,不存在则插入(类似"upsert")
    detached_book = Book(id=1, title='红楼梦(精装版)', price=88.00, stock=20)  # 创建一个游离对象
    merged_book = db.session.merge(detached_book)  # 合并到Session
    db.session.commit()                          # 提交
    print(f"\nmerge更新: {merged_book}")           # 打印结果

    # ============ 验证最终结果 ============
    print("\n=== 更新后的所有图书 ===")
    for b in Book.query.all():                   # 查询并遍历所有图书
        print(f"  {b}")

7.4 删除数据

python 复制代码
# ============================================================
# 案例:删除数据的多种方式
# 注意:删除操作是不可逆的,建议在生产环境中使用"软删除"
# ============================================================

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)


# ---------- 硬删除示例模型 ----------
class Category(db.Model):                        # 分类模型
    __tablename__ = 'category'                   # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    name = db.Column(db.String(50), nullable=False)                   # 分类名
    sort_order = db.Column(db.Integer, default=0)                     # 排序序号

    def __repr__(self):
        return f'<Category {self.name}>'


# ---------- 软删除示例模型 ----------
class Post(db.Model):                            # 文章模型(支持软删除)
    __tablename__ = 'post'                       # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    title = db.Column(db.String(200), nullable=False)                 # 标题
    content = db.Column(db.Text)                                      # 内容
    is_deleted = db.Column(db.Boolean, default=False)                 # 是否已删除(软删除标志)
    deleted_at = db.Column(db.DateTime, nullable=True)                # 删除时间

    def soft_delete(self):
        """软删除方法:不真正删除记录,只标记为已删除"""
        self.is_deleted = True                   # 标记为已删除
        self.deleted_at = datetime.now()         # 记录删除时间

    def restore(self):
        """恢复方法:将软删除的记录恢复"""
        self.is_deleted = False                  # 取消删除标记
        self.deleted_at = None                   # 清空删除时间

    @staticmethod
    def query_active():
        """查询所有未删除的记录(自定义查询方法)"""
        return Post.query.filter_by(is_deleted=False)  # 返回未删除的文章

    @staticmethod
    def query_deleted():
        """查询所有已删除的记录(回收站)"""
        return Post.query.filter_by(is_deleted=True)   # 返回已删除的文章

    def __repr__(self):
        status = '[已删除]' if self.is_deleted else ''
        return f'<Post {self.title}{status}>'


with app.app_context():
    db.create_all()                              # 创建表

    # 添加测试数据
    categories = [
        Category(name='前端开发', sort_order=1),    # 前端
        Category(name='后端开发', sort_order=2),    # 后端
        Category(name='人工智能', sort_order=3),    # AI
        Category(name='测试分类', sort_order=99),   # 测试用
        Category(name='临时分类', sort_order=100),  # 临时的
    ]
    db.session.add_all(categories)               # 批量添加
    db.session.commit()                          # 提交

    posts = [
        Post(title='Flask教程', content='Flask是...'),
        Post(title='Django教程', content='Django是...'),
        Post(title='测试文章', content='这是测试内容'),
        Post(title='待删除文章', content='这篇文章将被删除'),
    ]
    db.session.add_all(posts)                    # 批量添加
    db.session.commit()                          # 提交

    # ============ 方式一:删除单条记录 ============
    # 查询对象 -> delete() -> commit()
    target = Category.query.filter_by(name='测试分类').first()  # 查找"测试分类"
    if target:                                   # 如果找到了
        print(f"删除: {target}")                 # 打印即将删除的记录
        db.session.delete(target)                # 从Session中标记删除
        db.session.commit()                      # 提交事务,真正执行DELETE SQL
        print("删除成功!")                       # 打印成功提示

    # ============ 方式二:批量删除 ============
    # 使用 filter().delete() 批量删除(效率更高,只执行一条SQL)
    rows_deleted = Category.query.filter(
        Category.name.like('%临时%')              # 条件:名称包含"临时"
    ).delete(synchronize_session='evaluate')     # 批量删除,evaluate同步Session
    db.session.commit()                          # 提交
    print(f"\n批量删除了 {rows_deleted} 个分类")   # 打印删除数量

    # ============ 方式三:删除所有记录(清空表) ============
    # 警告:这会删除表中所有数据!
    # Category.query.delete()                     # 删除category表的所有记录
    # db.session.commit()                         # 提交

    # ============ 方式四:软删除(推荐在生产环境中使用) ============
    # 软删除不真正删除数据,只是标记为"已删除"状态
    # 好处:可以恢复误删数据、保留数据完整性、支持"回收站"功能

    # 软删除一篇文章
    post = Post.query.filter_by(title='待删除文章').first()  # 查找文章
    if post:
        post.soft_delete()                       # 调用软删除方法
        db.session.commit()                      # 提交
        print(f"\n软删除: {post}")               # 打印

    # 查询活跃文章(排除已软删除的)
    print("\n=== 活跃文章 ===")
    for p in Post.query_active().all():          # 使用自定义查询方法
        print(f"  {p}")

    # 查询已删除的文章(回收站)
    print("\n=== 回收站 ===")
    for p in Post.query_deleted().all():         # 查询已删除文章
        print(f"  {p}")

    # 恢复软删除的文章
    deleted_post = Post.query_deleted().first()  # 从回收站取出第一条
    if deleted_post:
        deleted_post.restore()                   # 调用恢复方法
        db.session.commit()                      # 提交
        print(f"\n恢复文章: {deleted_post}")       # 打印

    # ============ 删除操作的安全建议 ============
    # 1. 删除前始终先查询确认
    # 2. 重要数据使用软删除
    # 3. 使用 try-except 包裹删除操作
    # 4. 考虑级联删除的影响
    try:
        item = Category.query.get(999)           # 查询一个不存在的ID
        if item:                                 # 只有存在时才删除
            db.session.delete(item)              # 删除
            db.session.commit()                  # 提交
        else:
            print("\n记录不存在,无需删除")        # 提示记录不存在
    except Exception as e:                       # 捕获异常
        db.session.rollback()                    # 回滚事务
        print(f"删除失败: {e}")                   # 打印错误

八、综合实战案例 --- 完整的博客系统

python 复制代码
# ============================================================
# 综合案例:完整的博客系统模型和CRUD操作
# 整合本章所有知识点
# ============================================================

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
from sqlalchemy import and_, or_, func

# ---------- 应用初始化 ----------
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@localhost:3306/flask_db?charset=utf8mb4'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = False            # 关闭SQL回显(生产环境)

db = SQLAlchemy(app)                             # 创建数据库实例

# ---------- 关联表:文章与标签的多对多关系 ----------
article_tag = db.Table(
    'article_tag',                               # 中间表名
    db.Column('article_id', db.Integer, db.ForeignKey('articles.id'), primary_key=True),  # 文章ID外键
    db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True)           # 标签ID外键
)


# ---------- 用户模型 ----------
class User(db.Model):                            # 用户模型
    __tablename__ = 'users'                      # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    username = db.Column(db.String(50), unique=True, nullable=False)  # 用户名(唯一)
    email = db.Column(db.String(120), unique=True, nullable=False)    # 邮箱(唯一)
    password_hash = db.Column(db.String(255), nullable=False)         # 密码哈希值
    bio = db.Column(db.Text, default='')                             # 个人简介
    created_at = db.Column(db.DateTime, default=datetime.now)         # 注册时间

    # 一对多关系:一个用户有多篇文章
    articles = db.relationship('Article', backref='author', lazy='dynamic')
    # 一对多关系:一个用户有多条评论
    comments = db.relationship('Comment', backref='user', lazy='dynamic')

    def __repr__(self):
        return f'<User {self.username}>'


# ---------- 文章模型 ----------
class Article(db.Model):                         # 文章模型
    __tablename__ = 'articles'                   # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    title = db.Column(db.String(200), nullable=False)                 # 标题
    content = db.Column(db.Text, nullable=False)                      # 内容
    view_count = db.Column(db.Integer, default=0)                     # 浏览量
    is_published = db.Column(db.Boolean, default=False)               # 是否发布
    created_at = db.Column(db.DateTime, default=datetime.now)         # 创建时间
    updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)  # 更新时间
    author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)      # 作者外键

    # 多对多关系:文章与标签
    tags = db.relationship('Tag', secondary=article_tag, backref=db.backref('articles', lazy='dynamic'))

    # 一对多关系:文章与评论
    comments = db.relationship('Comment', backref='article', lazy='dynamic', cascade='all, delete-orphan')

    def __repr__(self):
        return f'<Article {self.title}>'


# ---------- 标签模型 ----------
class Tag(db.Model):                             # 标签模型
    __tablename__ = 'tags'                       # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    name = db.Column(db.String(50), unique=True, nullable=False)      # 标签名

    def __repr__(self):
        return f'<Tag {self.name}>'


# ---------- 评论模型 ----------
class Comment(db.Model):                         # 评论模型
    __tablename__ = 'comments'                   # 表名

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键
    body = db.Column(db.Text, nullable=False)                         # 评论内容
    created_at = db.Column(db.DateTime, default=datetime.now)         # 评论时间
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)        # 用户外键
    article_id = db.Column(db.Integer, db.ForeignKey('articles.id'), nullable=False)  # 文章外键

    def __repr__(self):
        return f'<Comment by User#{self.user_id}>'


# ---------- 完整的业务操作演示 ----------
with app.app_context():
    db.create_all()                              # 创建所有表

    # ===== 1. 创建用户 =====
    user1 = User(username='alice', email='alice@example.com', password_hash='hashed_pwd_1', bio='Python爱好者')
    user2 = User(username='bob', email='bob@example.com', password_hash='hashed_pwd_2', bio='全栈工程师')
    user3 = User(username='charlie', email='charlie@example.com', password_hash='hashed_pwd_3')

    db.session.add_all([user1, user2, user3])    # 批量添加用户
    db.session.commit()                          # 提交

    # ===== 2. 创建标签 =====
    tag_python = Tag(name='Python')              # Python标签
    tag_flask = Tag(name='Flask')                # Flask标签
    tag_mysql = Tag(name='MySQL')                # MySQL标签
    tag_web = Tag(name='Web开发')                # Web开发标签

    db.session.add_all([tag_python, tag_flask, tag_mysql, tag_web])
    db.session.commit()

    # ===== 3. 创建文章并关联标签 =====
    article1 = Article(
        title='Flask入门指南',
        content='Flask是一个轻量级的Python Web框架...',
        is_published=True,
        author=user1                             # 通过关系属性设置作者
    )
    article1.tags = [tag_python, tag_flask, tag_web]  # 设置文章的标签(多对多)

    article2 = Article(
        title='SQLAlchemy高级用法',
        content='SQLAlchemy是Python中最流行的ORM框架...',
        is_published=True,
        author=user1
    )
    article2.tags = [tag_python, tag_mysql]

    article3 = Article(
        title='RESTful API设计',
        content='RESTful API设计的最佳实践...',
        is_published=False,                      # 未发布的草稿
        author=user2
    )
    article3.tags = [tag_web, tag_flask]

    db.session.add_all([article1, article2, article3])
    db.session.commit()

    # ===== 4. 创建评论 =====
    comments = [
        Comment(body='写得很好!', user_id=user2.id, article_id=article1.id),
        Comment(body='很有帮助,谢谢分享', user_id=user3.id, article_id=article1.id),
        Comment(body='能详细讲讲关系映射吗?', user_id=user3.id, article_id=article2.id),
        Comment(body='期待更多内容', user_id=user1.id, article_id=article3.id),
    ]
    db.session.add_all(comments)
    db.session.commit()

    # ===== 5. 复杂查询演示 =====

    # 查询1:获取某用户的所有已发布文章
    print("=== Alice的已发布文章 ===")
    alice_articles = Article.query.filter(
        and_(                                    # AND条件
            Article.author_id == user1.id,       # 作者是Alice
            Article.is_published == True          # 已发布
        )
    ).order_by(Article.created_at.desc()).all()  # 按时间降序

    for article in alice_articles:               # 遍历文章
        tag_names = [t.name for t in article.tags]  # 获取标签名列表
        comment_count = article.comments.count()     # 获取评论数
        print(f"  《{article.title}》 标签:{tag_names} 评论:{comment_count} 浏览:{article.view_count}")

    # 查询2:按标签查找文章
    print(f"\n=== 标签'Python'的文章 ===")
    python_tag = Tag.query.filter_by(name='Python').first()  # 获取Python标签
    for article in python_tag.articles.all():    # 通过反向引用查询
        print(f"  《{article.title}》 by {article.author.username}")

    # 查询3:统计每个用户的发文数
    print("\n=== 用户发文统计 ===")
    stats = db.session.query(
        User.username,                           # 用户名
        func.count(Article.id).label('count')   # 文章数量
    ).join(                                      # JOIN连接
        Article, User.id == Article.author_id    # 连接条件
    ).group_by(
        User.id                                  # 按用户分组
    ).all()                                      # 执行

    for username, count in stats:                # 遍历统计结果
        print(f"  {username}: {count}篇文章")

    # 查询4:热门文章TOP3(按浏览量排序)
    print("\n=== 热门文章TOP3 ===")
    hot_articles = Article.query.filter_by(
        is_published=True                        # 只看已发布的
    ).order_by(
        Article.view_count.desc()                # 按浏览量降序
    ).limit(3).all()                             # 取前3

    for idx, article in enumerate(hot_articles, 1):  # enumerate从1开始编号
        print(f"  第{idx}名: 《{article.title}》 浏览:{article.view_count}")

    # 查询5:带分页的文章列表
    print("\n=== 文章分页(每页2条,第1页)===")
    page_result = Article.query.filter_by(
        is_published=True                        # 已发布
    ).order_by(
        Article.created_at.desc()                # 最新的在前
    ).paginate(page=1, per_page=2, error_out=False)  # 第1页,每页2条

    print(f"  总{page_result.total}篇, 第{page_result.page}/{page_result.pages}页")
    for article in page_result.items:            # 当前页的文章
        print(f"  《{article.title}》 - {article.author.username}")

    # ===== 6. 更新操作 =====
    article1.view_count += 10                    # 模拟增加浏览量
    db.session.commit()                          # 提交
    print(f"\n文章《{article1.title}》浏览量更新为: {article1.view_count}")

    # ===== 7. 删除操作(级联删除) =====
    # 删除一篇文章会自动删除其所有评论(cascade='all, delete-orphan')
    target = Article.query.filter_by(title='RESTful API设计').first()
    if target:
        print(f"\n删除文章: {target.title}")
        print(f"  关联评论数: {target.comments.count()}")
        db.session.delete(target)                # 删除文章(级联删除评论)
        db.session.commit()                      # 提交
        print("  删除成功(含级联评论删除)")

    # 验证级联删除
    remaining_comments = Comment.query.count()   # 查询剩余评论数
    print(f"  剩余评论数: {remaining_comments}")  # 应该少了一条评论

    print("\n=== 所有操作完成! ===")

九、本章小结

知识点 核心要点
数据库概述 ORM将数据库表映射为Python类,避免手写SQL
安装 pip install flask-sqlalchemy pymysql flask-migrate
连接数据库 通过 SQLALCHEMY_DATABASE_URI 配置连接字符串
定义模型 继承 db.Model,使用 db.Column 定义字段及约束
创建表 db.create_all() 或 Flask-Migrate 迁移工具
模型关系 一对多用 db.relationship + 外键,多对多用中间表,一对一加 uselist=False
增加数据 db.session.add() / add_all() + commit()
查询数据 query.filter_by() / filter() / order_by() / limit() / paginate() / 聚合函数
更新数据 直接修改属性 + commit(),或 query.update() 批量更新
删除数据 db.session.delete() 硬删除,推荐使用软删除策略

关键注意事项:

  1. Flask-SQLAlchemy 3.x 中必须使用 app.app_context() 上下文
  2. 所有数据操作必须调用 db.session.commit() 才能持久化
  3. 出现异常时使用 db.session.rollback() 回滚事务
  4. 生产环境建议使用 Flask-Migrate 管理数据库迁移
  5. 推荐使用软删除保护重要数据
相关推荐
我是一颗柠檬1 小时前
【MySQL全面教学】MySQL基础SQL语句Day3(2026年)
数据库·后端·sql·mysql·oracle
XS0301062 小时前
MyBatis动态SQL
数据库·sql·mybatis
MandalaO_O2 小时前
MyBatis 与 MySQL 执行流程
数据库·mysql·mybatis
wubba lubba dub dub7502 小时前
第四十八周学习周报
学习
生成论实验室3 小时前
用事件关系网络重新理解AI(三):激活函数、微调与元学习
人工智能·学习·算法·语言模型·可信计算技术
l1t3 小时前
DeepSeek总结的将 Rust Delta Kernel 集成到 ClickHouse
数据库·clickhouse·rust
qq_283720053 小时前
万字深度:Chroma 向量数据库全解析 — 核心原理、实战操作、性能优化与工程最佳实践
数据库·性能优化
辰海Coding3 小时前
MiniSpring框架学习-为什么一个请求访问 /helloworld,最后能调用到某个 Controller 方法?原始 MVC实现
java·学习·程序人生·spring·mvc
黄筱筱筱筱筱筱筱3 小时前
二进制包安装MySql服务
数据库