数据库操作 --- 知识点详解与案例代码
一、数据库概述
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() 硬删除,推荐使用软删除策略 |
关键注意事项:
- Flask-SQLAlchemy 3.x 中必须使用
app.app_context()上下文 - 所有数据操作必须调用
db.session.commit()才能持久化 - 出现异常时使用
db.session.rollback()回滚事务 - 生产环境建议使用 Flask-Migrate 管理数据库迁移
- 推荐使用软删除保护重要数据