flask-sqlalchemy扩展

1. 基本使用

1.1 扩展介绍

1. 认识flask-sqlalchemy

  • Flask-SQLAlchemy是一个为Flask应用增加 SQLAlchemy 支持的扩展。它致力于简化在Flask中 SQLAlchemy 的使用
  • SQLAlchemy 是目前python最强大的 ORM框架 ,功能全面,使用简单
ORM优缺点
  • 优点
    • 有语法提示,省去自己拼写SQL,保证语法的正确性
    • orm提供方言功能(dialect,可以转化为多种数据库的语法),减少学习成本
    • 面向对象,可读性强,开发效率高
    • 防止sql注入攻击
  • 缺点
    • 需要语法转化,效率比原生sql低
    • 复杂的查询往往语法比较复杂(可以使用原生sql替换)

2. 环境安装

pip install flask-sqlalchemy
  • flask-sqlalchemy 在安装使用过程中,如果出现 ModuleNotFoundError: No module named 'MySQLdb 错误,则表示缺少mysql依赖包
  • 方案1:安装 mysqlclinet 依赖包
pip install mysqlclient
  • 方案2:安装 pymysql 依赖包
pip install pymysql
https://flask-sqlalchemy.palletsprojects.com/en/2.x/
  • mysqlclient和pymysql都是用于mysql访问的依赖包,前者有C语言实现的,而后者有python实现,前者的执行效率比后者高,但前者在windows系统中兼容性较差,推荐前者

3. 文档

  • 官方文档https://flask-sqlalchemy.palletsprojects.com/en/2.x/
  • 中文翻译http://www.pythondoc.com/flask-sqlalchemy/index.html

1.2 组件初始化

1. 基本配置

  • flask-sqlalchemy 的相关配置也封装到了 flask 的配置项中, 可以通过app.config属性 或 配置加载方案 (如config.from_object) 进行设置
  • 主要配置:
    flask-sqlalchemy扩展_第1张图片
  • 数据库URI(连接地址)格式: 协议名://用户名:密码@数据库IP:端口号/数据库名, 如:
    app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
  • 注意点:
    • 如果数据库驱动使用的是 pymysql, 则协议名需要修改为 mysql+pymysql://xxxxxxx
    • sqlalchemy 支持多种关系型数据库, 其他数据库的URI可以查阅 官方文档https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/#connection-uri-format
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 设置数据库连接地址
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
# 是否追踪数据库修改(开启后会触发一些钩子函数)  一般不开启, 会影响性能
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# 是否显示底层执行的SQL语句
app.config['SQLALCHEMY_ECHO'] = True

2. 两种初始化方式

  • flask-sqlalchemy 支持两种组件初始化方式:
    • 方式1: 创建组件时, 直接关联Flask应用
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 应用配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True

# 方式1: 初始化组件对象, 直接关联Flask应用
db = SQLAlchemy(app)
* 方式2: 先创建组件, 延后关联Flass应用
from flask import Flask
from flask_sqlalchemy import SQLAlchemy


# 方式2: 初始化组件对象, 延后关联Flask应用
db = SQLAlchemy()


def create_app(config_type):
    """工厂函数"""

    # 创建应用
    flask_app = Flask(__name__)
    # 加载配置
    config_class = config_dict[config_type]
    flask_app.config.from_object(config_class)

    # 关联flask应用
    db.init_app(app)

    return flask_app
  • 方式2主要针对的是 动态创建应用 的场景
    flask框架 官方建议 所有的扩展包 都支持这两种初始化方案, 其他flask扩展 也可以尝试使用

1.3 构建模型类

  • ORM映射关系
    flask-sqlalchemy扩展_第2张图片
  • flask-sqlalchemy 的关系映射和 Django-orm 类似:
    • 类 对应 表
    • 类属性 对应 字段
    • 实例对象 对应 记录
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True

# 创建组件对象
db = SQLAlchemy(app)


# 构建模型类  类->表  类属性->字段  实例对象->记录
class User(db.Model):
    __tablename__ = 't_user'  # 设置表名, 表名默认为类名小写
    id = db.Column(db.Integer, primary_key=True)  # 设置主键, 默认自增
    name = db.Column('username', db.String(20), unique=True)  # 设置字段名 和 唯一约束
    age = db.Column(db.Integer, default=10, index=True)  # 设置默认值约束 和 索引


if __name__ == '__main__':
    # 删除所有继承自db.Model的表
    db.drop_all()
    # 创建所有继承自db.Model的表
    db.create_all()
    app.run(debug=True)
  • 注意点:
    • 模型类必须继承 db.Model, 其中 db 指对应的组件对象
    • 表名默认为类名小写, 可以通过 __tablename__类属性 进行修改
    • 类属性对应字段, 必须是通过 db.Column() 创建的对象
    • 可以通过 create_all() 和 drop_all()方法 来创建和删除所有模型类对应的表
      常用的字段类型
      常用的字段类型
      常用的字段选项
      flask-sqlalchemy扩展_第3张图片
  • 注意点: 如果没有给对应字段的类属性设置default参数, 且添加数据时也没有给该字段赋值, 则 sqlalchemy会给该字段设置默认值 None

2. 数据操作

2.1 增加数据

  • 增加数据主要需要三步操作
    • 创建模型对象 模型对象 = 模型类(字段名 = 字段值)
    • 将模型对象添加到会话中 组件对象.session.add(模型对象)
    • 提交会话 组件对象.session.commit()
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True

# 创建组件对象
db = SQLAlchemy(app)


# 构建模型类  
class User(db.Model):
    __tablename__ = 't_user'  
    id = db.Column(db.Integer, primary_key=True)  
    name = db.Column('username', db.String(20), unique=True)  
    age = db.Column(db.Integer, index=True)  


@app.route('/')
def index():
    """增加数据"""

    # 1.创建模型对象
    user1 = User(name='zs', age=20)
    # user1.name = 'zs'
    # user1.age = 20

    # 2.将模型对象添加到会话中 
    db.session.add(user1)
    # 添加多条记录
    # db.session.add_all([user1, user2, user3])

    # 3.提交会话 (会提交事务)
    # sqlalchemy会自动创建隐式事务
    # 事务失败会自动回滚
    db.session.commit()

    return "index"


if __name__ == '__main__':
    db.drop_all()
    db.create_all()
    app.run(debug=True)
  • 注意点:
    • 给模型对象设置数据 可以通过 初始化参数 或者 赋值属性 两种方式
    • session.add(模型对象) 添加单条数据到会话中, session.add_all(列表) 添加多条数据到会话中
    • 这里的 会话 并不是 状态保持机制中的 session,而是 sqlalchemy 的会话。它被设计为 数据操作的执行者, 从SQL角度则可以理解为是一个 加强版的数据库事务
    • sqlalchemy自动创建事务, 并将数据操作包含在事务中, 提交会话时就会提交事务
    • 事务提交失败会自动回滚

2.2 查询数据

  • sqlalachemy 的查询语法较多, 接下来通过一个案例来进行综合演练
  • 案例说明
    • 案例中包含一个模型类 User, 对应 users表, 包含四个字段: id(主键), name, email, age
    • 首先运行案例代码, 生成测试数据
      • 程序启动后会重置 users表, 并向其中添加10条用户数据
    • 为了方便展示查询结果, 建议使用 交互模式 测试查询语句
    • 推荐使用 ipython 包, 相比 python 自带的交互模式 有语法提示
    • 安装包 pip install ipython
    • 关于输出结果
      • 内置方法__repr__() 是 str()方法 的升级版, 可以修改 print(对象)交互模式下对象 的输出结果
      • 案例中将 模型对象的输出结果 修改为 输出模型对象的所有属性值 (记录的数据) , 以便验证查询结果
# hm_03_数据查询.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy


app = Flask(__name__)

# 相关配置
app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://root:[email protected]:3306/test31"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SQLALCHEMY_ECHO"] = False
db = SQLAlchemy(app)

# 自定义类 继承db.Model  对应 表
class User(db.Model):
    __tablename__ = "users"  # 表名 默认使用类名的小写
    # 定义类属性 记录字段
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64))
    email = db.Column(db.String(64))
    age = db.Column(db.Integer)

    def __repr__(self):  # 自定义 交互模式 & print() 的对象打印
        return "(%s, %s, %s, %s)" % (self.id, self.name, self.email, self.age)


@app.route('/')
def index():
    """
    查询所有用户数据


    查询有多少个用户


    查询第1个用户


    查询id为4的用户[3种方式]


    查询名字结尾字符为g的所有用户[开始 / 包含]


    查询名字和邮箱都以li开头的所有用户[2种方式]


    查询age是25 或者 `email`以`itheima.com`结尾的所有用户


    查询名字不等于wang的所有用户[2种方式]


    查询id为[1, 3, 5, 7, 9]的用户


    所有用户先按年龄从小到大, 再按id从大到小排序, 取前5个


    查询年龄从小到大第2-5位的数据


    分页查询, 每页3个, 查询第2页的数据


    查询每个年龄的人数    select age, count(name) from t_user group by age  分组聚合


    只查询所有人的姓名和邮箱  优化查询   默认使用select *


    """


    return 'index'


if __name__ == '__main__':
    # 删除所有表
    db.drop_all()
    # 创建所有表
    db.create_all()
    # 添加测试数据
    user1 = User(name='wang', email='[email protected]', age=20)
    user2 = User(name='zhang', email='[email protected]', age=33)
    user3 = User(name='chen', email='[email protected]', age=23)
    user4 = User(name='zhou', email='[email protected]', age=29)
    user5 = User(name='tang', email='[email protected]', age=25)
    user6 = User(name='wu', email='[email protected]', age=25)
    user7 = User(name='qian', email='[email protected]', age=23)
    user8 = User(name='liu', email='[email protected]', age=30)
    user9 = User(name='li', email='[email protected]', age=28)
    user10 = User(name='sun', email='[email protected]', age=26)

    # 一次添加多条数据
    db.session.add_all([user1, user2, user3, user4, user5, user6, user7, user8, user9, user10])
    db.session.commit()
    app.run(debug=True)

交互模式测试

$ workon flask_env  # 进入虚拟环境
$ ipython  # 进入交互模式

In [1]: from hm_03_数据查询 import *  # 导入项目环境
In [2]: User.query.all()  # 测试查询语句: 查询所有用户
Out[2]: 
[(1, wang, wang@163.com, 20),
(2, zhang, zhang@189.com, 33),
(3, chen, chen@126.com, 23),
(4, zhou, zhou@163.com, 29),
(5, tang, tang@itheima.com, 25),
(6, wu, wu@gmail.com, 25),
(7, qian, qian@gmail.com, 23),
(8, liu, liu@itheima.com, 30),
(9, li, li@163.com, 28),
(10, sun, sun@163.com, 26)]
# 查询所有用户数据
User.query.all() 返回列表, 元素为模型对象

# 查询有多少个用户
User.query.count()

# 查询第1个用户
User.query.first()  返回模型对象/None

# 查询id为4的用户[3种方式]
# 方式1: 根据id查询  返回模型对象/None
User.query.get(4)  

# 方式2: 等值过滤器 关键字实参设置字段值  返回BaseQuery对象
# BaseQuery对象可以续接其他过滤器/执行器  如 all/count/first等
User.query.filter_by(id=4).all()  

# 方式3: 复杂过滤器  参数为比较运算/函数引用等  返回BaseQuery对象
User.query.filter(User.id == 4).first()  

# 查询名字结尾字符为g的所有用户[开始 / 包含]
User.query.filter(User.name.endswith("g")).all()
User.query.filter(User.name.startswith("w")).all()
User.query.filter(User.name.contains("n")).all()
User.query.filter(User.name.like("w%n%g")).all()  # 模糊查询

# 查询名字和邮箱都以li开头的所有用户[2种方式]
User.query.filter(User.name.startswith('li'), User.email.startswith('li')).all()
from sqlalchemy import and_
User.query.filter(and_(User.name.startswith('li'), User.email.startswith('li'))).all()

# 查询age是25 或者 `email`以`itheima.com`结尾的所有用户
from sqlalchemy import or_
User.query.filter(or_(User.age==25, User.email.endswith("itheima.com"))).all()

# 查询名字不等于wang的所有用户[2种方式]
from sqlalchemy import not_
User.query.filter(not_(User.name == 'wang')).all()
User.query.filter(User.name != 'wang').all()

# 查询id为[1, 3, 5, 7, 9]的用户
User.query.filter(User.id.in_([1, 3, 5, 7, 9])).all()

# 所有用户先按年龄从小到大, 再按id从大到小排序, 取前5个
User.query.order_by(User.age, User.id.desc()).limit(5).all()

# 查询年龄从小到大第2-5位的数据   2 3 4 5
User.query.order_by(User.age).offset(1).limit(4).all()

# 分页查询, 每页3个, 查询第2页的数据  paginate(页码, 每页条数)
pn = User.query.paginate(2, 3)
pn.pages 总页数  pn.page 当前页码 pn.items 当前页的数据  pn.total 总条数

# 查询每个年龄的人数    select age, count(name) from t_user group by age  分组聚合
from sqlalchemy import func
data = db.session.query(User.age, func.count(User.id).label("count")).group_by(User.age).all()
for item in data:
   # print(item[0], item[1])
   print(item.age, item.count)  # 建议通过label()方法给字段起别名, 以属性方式获取数据


# 只查询所有人的姓名和邮箱  优化查询   User.query.all()  # 相当于select *
from sqlalchemy.orm import load_only
data = User.query.options(load_only(User.name, User.email)).all()  # flask-sqlalchem的语法
for item in data:
   print(item.name, item.email)

data = db.session.query(User.name, User.email).all()  # sqlalchemy本体的语法
for item in data:
   print(item.name, item.email)

查询语法

  • 有两套查询语法可以使用:
    • flask-sqlalchemy扩展 封装的语法为 查询结果 = 模型类.query[.查询过滤器].查询执行器, 返回的查询结果中数据单元为对应的 模型对象
    • sqlalchemy本体 提供的语法为 组件对象.session.query(字段)[.查询过滤器].查询执行器, 返回的数据单元为 类元组对象, 该类型支持 索引属性名 以及 别名 三种取值方式
    • 查询过滤器非必须, 查询执行器必须设置
    • 除了特殊查询语句(联表/聚合等)需要使用 sqlalchemy本体的语法, 一般使用 flask-sqlalchemy扩展 封装的语法即可
      常用的SQLAlchemy查询执行器
  • 执行器的特点
    • j将整个查询语句转化为SQL语句并 执行查询
    • 在查询语句的末尾设置,每条查询语句 只能设置一个执行器
      flask-sqlalchemy扩展_第4张图片
  • 常用的SQLAlchemy查询过滤器
    • 过滤器的特点:
      • 只负责设置过滤条件, 不会执行查询(查询由执行 器来完成)
      • 允许续接其他过滤器执行器
        flask-sqlalchemy扩展_第5张图片
  • 注意点:
    • 查询过滤器返回的都是 BaseQuery类型对象, 该对象支持链式调用, 即可以续接其他过滤器 或 执行器
    • 如果考虑到性能优化, 应该避免 select *, 只查询需求字段 (select * 会导致数据库服务器去获取&解析&处理目标数据的每个字段, 对服务器资源造成浪费, 并且不必要的数据也会占用更多的 网络IO 和 数据库查询缓存)

2.3 修改/更新数据

1. 先查询,再更新
2. 基于过滤条件的更新
  • flask-sqlalchemy 提供了两种更新数据的方案:
    • 先查询, 再更新
      • 对应SQL中的 先 select, 再 update
    • 基于过滤条件的更新 (推荐方案)
      • 对应SQL中的 update xx where xx = xx (也称为 update子查询 )

1. 先查询,在更新

  • 这种方式简单易懂, 操作步骤如下:
    • 执行查询语句, 获取目标模型对象
    • 对模型对象的属性进行赋值 (更新数据)
    • 提交会话
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True

# 创建组件对象
db = SQLAlchemy(app)


# 构建模型类  商品表
class Goods(db.Model):
    __tablename__ = 't_good'  # 设置表名
    id = db.Column(db.Integer, primary_key=True)  # 设置主键
    name = db.Column(db.String(20), unique=True)  # 商品名称
    count = db.Column(db.Integer)  # 剩余数量


@app.route('/')
def purchase():
    """购买商品"""

    # 更新方式1: 先查询后更新
    # 缺点: 并发情况下, 容易出现更新丢失问题 (Lost Update)

    # 1.执行查询语句, 获取目标模型对象
    goods = Goods.query.filter(Goods.name == '方便面').first()
    # 2.对模型对象的属性进行赋值 (更新数据)
    goods.count = goods.count - 1
    # 3.提交会话
    db.session.commit()

    return "index"


if __name__ == '__main__':
    # 删除所有继承自db.Model的表
    db.drop_all()
    # 创建所有继承自db.Model的表
    db.create_all()

    # 添加一条测试数据
    goods = Goods(name='方便面', count=1)
    db.session.add(goods)
    db.session.commit()
    app.run(debug=True)
  • 这种方式的缺点:
    • 查询和更新分两条语句, 效率低
    • 如果并发更新, 可能出现更新丢失问题(Lost Update)

2.基于过滤条件的更新

  • 这种方式的优点:
    • 一条语句, 被网络IO影响程度低, 执行效率更高
  • 查询和更新在一条语句中完成, 单条SQL具有原子性, 不会出现更新丢失问题
  • 会对满足过滤条件的所有记录进行更新, 可以实现批量更新处理
  • 操作步骤如下:
    • 配合 查询过滤器filter() 和 更新执行器update() 进行数据更新
    • 提交会话
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True

# 创建组件对象
db = SQLAlchemy(app)


# 构建模型类  商品表
class Goods(db.Model):
    __tablename__ = 't_good'  
    id = db.Column(db.Integer, primary_key=True)  
    name = db.Column(db.String(20), unique=True)  
    count = db.Column(db.Integer) 


@app.route('/')
def purchase():
    """购买商品"""

    # 更新方式2: update子查询   可以避免更新丢失问题   
    # update t_good set count = count - 1 where name = '方便面';
    Goods.query.filter(Goods.name == '方便面').update({'count': Goods.count - 1})
    # 提交会话
    db.session.commit()
    return "index"


if __name__ == '__main__':
    # 重置数据库数据
    db.drop_all()
    db.create_all()

    # 添加一条测试数据
    goods = Goods(name='方便面', count=1)
    db.session.add(goods)
    db.session.commit()
    app.run(debug=True)

2.4 删除数据

1. 新查询,再删除
2. 基于过滤条件的删除
  • 类似更新数据, 也存在两种删除数据的方案:
    • 先查询, 再删除
      • 对应SQL中的 先select, 再delete
    • 基于过滤条件的删除 (推荐方案)
      • 对应SQL中的 delete xx where xx = xx (也称为 delete子查询 )

1. 先查询,后删除

  • 操作步骤如下:
    • 执行查询语句, 获取目标模型对象
    • 将模型对象从会话中删除
    • 提交会话
@app.route('/del')
def delete():
    """删除数据"""

    # 方式1: 先查后删除
    goods = Goods.query.filter(Goods.name == '方便面').first()
    # 删除数据
    db.session.delete(goods)
    # 提交会话 增删改都要提交会话
    db.session.commit()

    return "index"
  • 这种方式的缺点:
    • 查询和删除分两条语句,效率低

2. 基于过滤条件的删除

  • 这种方式的优点:
    • 一条语句, 被网络IO影响程度低, 执行效率更高
    • 会对满足过滤条件的所有记录进行删除, 可以实现批量删除处理
      操作步骤如下:
    • 配合 查询过滤器filter() 和 删除执行器delete() 进行数据删除
    • 提交会话
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True

# 创建组件对象
db = SQLAlchemy(app)


# 构建模型类  商品表
class Goods(db.Model):
    __tablename__ = 't_good'  
    id = db.Column(db.Integer, primary_key=True)  
    name = db.Column(db.String(20), unique=True)  
    count = db.Column(db.Integer) 


@app.route('/del')
def delete():
    """删除数据"""

    # 方式2: delete子查询
    Goods.query.filter(Goods.name == '方便面').delete()
    # 提交会话
    db.session.commit()

    return "index"


if __name__ == '__main__':
    # 重置数据库数据
    db.drop_all()
    db.create_all()

    # 添加一条测试数据
    goods = Goods(name='方便面', count=1)
    db.session.add(goods)
    db.session.commit()
    app.run(debug=True)
  • 注意点:
    • 增删改操作都需要提交会话 ,对应事务中进行数据库变化后提交事务

3. 高级机制

3.1 刷新数据

  • Session 被设计为数据操作的执行者, 会先将操作产生的数据保存到内存中
  • 在执行 flush刷新操作 后, 数据操作才会同步到数据库中
  • 有两种情况下会 隐式执行刷新操作 :
    • 提交会话
    • 执行查询操作 (包括 update 和 delete 子查询)
  • 开发者也可以 手动执行刷新操作 session.flush()
  • flask-sqlalchemy扩展_第6张图片
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)


# 构建模型类  
class Goods(db.Model):
    __tablename__ = 't_good'  
    id = db.Column(db.Integer, primary_key=True) 
    name = db.Column(db.String(20), unique=True) 
    count = db.Column(db.Integer)  


@app.route('/')
def purchase():

    goods = Goods(name='方便面', count=20)
    db.session.add(goods)
    # 主动执行flush操作, 立即执行SQL操作(数据库同步)
    db.session.flush()

    # Goods.query.count()  # 查询操作会自动执行flush操作
    db.session.commit()  # 提交会话会自动执行flush操作

    return "index"


if __name__ == '__main__':
    db.drop_all()
    db.create_all()

    app.run(debug=True)

3.2 多表查询

1. 外键关联查询

1. 数据关联
2. 关联查询
  • 数据库的表之间通过 外键进行关联
    *对于一对多关系, 外键在多的一方, 即 从表 中
  • sqalchemy 中也可以通过 外键字段 实现数据关联 及 关联查询
    flask-sqlalchemy扩展_第7张图片
1.1 数据关联
1. 从表模型类中定义外键字段
2. 从表模型对象的外键字段记录主表主键
  • 案例中包含两个模型类: User用户模型 和 Address地址模型, 并且一个用户可以有多个地址, 两张表之间存在一对多关系
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = False

# 创建组件对象
db = SQLAlchemy(app)


# 用户表  主表(一)   一个用户可以有多个地址
class User(db.Model):
    __tablename__ = 't_user'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))


# 地址表   从表(多)
class Address(db.Model):
    __tablename__ = 't_adr'
    id = db.Column(db.Integer, primary_key=True)
    detail = db.Column(db.String(20))
    user_id = db.Column(db.Integer)  # 定义外键


@app.route('/')
def index():
    """添加并关联数据"""
    user1 = User(name='张三')
    db.session.add(user1)
    db.session.flush()  # 需要手动执行flush操作, 让主表生成主键, 否则外键关联失败
    # db.session.commit()  # 有些场景下, 为了保证数据操作的原子性不能分成多个事务进行操作

    adr1 = Address(detail='中关村3号', user_id=user1.id)
    adr2 = Address(detail='华强北5号', user_id=user1.id)
    db.session.add_all([adr1, adr2])
    db.session.commit()

    return "index"



if __name__ == '__main__':
    db.drop_all()
    db.create_all()

    app.run(debug=True)
1.2 关联查询
1. 查询主表数据
2. 在通过外键字段查询关联的从表数据
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = False

# 创建组件对象
db = SQLAlchemy(app)


# 用户表  一   一个用户可以有多个地址
class User(db.Model):
    __tablename__ = 't_user'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))


# 地址表   多
class Address(db.Model):
    __tablename__ = 't_adr'
    id = db.Column(db.Integer, primary_key=True)
    detail = db.Column(db.String(20))
    user_id = db.Column(db.Integer)  # 定义外键


@app.route('/demo')
def demo():
    """查询多表数据  需求: 查询姓名为"张三"的所有地址信息"""

    # 1.先根据姓名查找到主表主键
    user1 = User.query.filter_by(name='张三').first()

    # 2.再根据主键到从表查询关联地址
    adrs = Address.query.filter_by(user_id=user1.id).all()
    for adr in adrs:
        print(adr.detail)

    return "demo"


@app.route('/')
def index():
    """添加并关联数据"""

    user1 = User(name='张三')
    db.session.add(user1)
    db.session.flush()  
    adr1 = Address(detail='中关村3号', user_id=user1.id)
    adr2 = Address(detail='华强北5号', user_id=user1.id)
    db.session.add_all([adr1, adr2])
    db.session.commit()

    return "index"


if __name__ == '__main__':
    db.drop_all()
    db.create_all()

    app.run(debug=True)
  • 注意点:
    • 普通字段就可以作为外键使用 ,只要记录了对应的主表主键就可以关联查询
    • 如果需要 主从表数据在同一事务 中添加并关联 ,则可能先手动调用session.flush 方法来执行插入操作并生成主键,否则无法关联

2. 关系属性/查询

  • 关系属性是 sqalchemy装的一套查询关联数据的语法, 其目的为 让开发者使用 面向对象的形式 方便快捷的获取关联数据
  • 关系属性的 本质仍是外键
  • 关系属性的使用步骤:
    • 定义关系属性
      关系属性名 = db.relationship(‘关联数据所在的模型类’)
    • 外键字段设置外键参数
      外键字段 = db.Column(字段类型, db.ForeignKey(主表名.主键名))
    • 通过关系属性获取关联数据
      模型对象.关系属性
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = False

# 创建组件对象
db = SQLAlchemy(app)


# 用户表  一   一个用户可以有多个地址
class User(db.Model):
    __tablename__ = 't_user'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))
    addresses = db.relationship('Address')  # 1.定义关系属性 relationship("关联数据所在的模型类")


# 地址表   多
class Address(db.Model):
    __tablename__ = 't_adr'
    id = db.Column(db.Integer, primary_key=True)
    detail = db.Column(db.String(20))
    # 2. 外键字段设置外键参数  db.ForeignKey('主表名.主键')
    user_id = db.Column(db.Integer, db.ForeignKey('t_user.id'))  


@app.route('/')
def index():
    """添加数据"""
    user1 = User(name='张三')
    db.session.add(user1)
    db.session.flush()
    adr1 = Address(detail='中关村3号', user_id=user1.id)
    adr2 = Address(detail='华强北5号', user_id=user1.id)
    db.session.add_all([adr1, adr2])
    db.session.commit()

    """查询多表数据  需求: 查询姓名为"张三"的所有地址信息"""
    # 先根据姓名查找用户主键
    user1 = User.query.filter_by(name='张三').first()

    # 3.使用关系属性获取关系数据
    for address in user1.addresses:
        print(address.detail)

    return "index"



if __name__ == '__main__':
    # 重置所有继承自db.Model的表
    db.drop_all()
    db.create_all()

    app.run(debug=True)
  • 注意点:
    • 关系属性的本质还是外键 , 所以数据关联还是通过外键来完成
    • 设置了外键参数的外键字段, 在创建表时会自动生成 外键约束
    • 对多关系属性返回值为列表, 元素为关联的模型对象
    • 关系属性和外键关联都可以查询关联数据, 但是 关系属性会取出关联表中的所有字段(效率较低) , 所以实际开发中 推荐使用外键关联查询

3. 连接属性/查询

  • 开发中有 联表查询需求 时, 一般会使用 join连接查询
  • sqlalchemy 也提供了对应的查询语法
  • db.session.query(主表模型字段1, 主表模型字段2, 从表模型字段1, xx… ).join(从表模型类, 主表模型类.主键 == 从表模型类.外键)
  • ***join***语句 属于查询过滤器, 返回值也是 BaseQuery 类型对象
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test31'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = False

# 创建组件对象
db = SQLAlchemy(app)


# 用户表  一   
class User(db.Model):
    __tablename__ = 't_user'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))


# 地址表   多
class Address(db.Model):
    __tablename__ = 't_adr'
    id = db.Column(db.Integer, primary_key=True)
    detail = db.Column(db.String(20))
    user_id = db.Column(db.Integer)  # 定义外键


@app.route('/demo')
def demo():
    """查询多表数据  需求: 查询姓名为"张三"的用户id和地址信息"""

    # sqlalchemy的join查询
    data = db.session.query(User.id, Address.detail).join(Address, User.id == Address.user_id).filter(User.name == '张三').all()
    for item in data:
        print(item.detail, item.id)

    return "demo"


@app.route('/')
def index():
    """添加数据"""
    user1 = User(name='张三')
    db.session.add(user1)
    db.session.flush()
    adr1 = Address(detail='中关村3号', user_id=user1.id)
    adr2 = Address(detail='华强北5号', user_id=user1.id)
    db.session.add_all([adr1, adr2, user1])
    db.session.commit()

    return 'index'


if __name__ == '__main__':
    db.drop_all()
    db.create_all()

    app.run(debug=True)
  • 关联查询的性能优化:
    • 无论使用 外键 还是 关系属性 查询关联数据, 都需要 查询两次, 一次查询用户数据, 一次查询地址数据
    • 两次查询就需要发送两次请求给数据库服务器, 如果数据库和web应用不在一台服务器中, 则 网络IO会对查询效率产生一定影响
    • 可以考虑使用 连接查询 join 使用一条语句就完成关联数据的查询
# 使用join语句优化关联查询
adrs = Address.query.join(User, Address.user_id == User.id).filter(User.name == '张三').all()  # 列表中包含地址模型对象
  • 关于拆分join语句:
    • 使用 JOIN连接查询 还是 拆分join为多条简单语句 并不是一个可以一概而论的问题, 查询性能受 联表数量、索引设计、缓存处理、数据库拆分、SQL优化器等多方面因素影响, 每个场景不尽相同
    • 一般可以先考虑使用 JOIN连接查询, 在发现性能问题后再考虑进一步的优化尝试

3.3 Session机制

1. 生命周期

  • flask-sqlalchemy 对于 ***sqlalchemy***本体 的 Session 进行了一定的封装:
  • Session的生命周期和请求相近:
    • 请求中的首次数据操作会创建Session
    • 整个请求过程中使用的Session为同一个, 并且线程隔离
    • 请求结束时会自动销毁Session(释放内存)
      flask-sqlalchemy扩展_第8张图片

2. Session和事务

  • Session中可以包含多个事务, 提交事务失败后, 会自动执行SQL的回滚操作
  • 同一个请求中, 想要在在前一个事务失败的情况下创建新的事务, 必须先手动回滚事务 Session.rollback
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/toutiao'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)


# 构建模型类 
class User(db.Model):
    __tablename__ = 't_user'  
    id = db.Column(db.Integer, primary_key=True)  
    name = db.Column('username', db.String(20), unique=True) 
    age = db.Column(db.Integer, default=0, index=True)  


@app.route('/')
def index():

    """事务1"""
    try:
        user1 = User(name='zs', age=20)
        db.session.add(user1)
        db.session.commit()
    except BaseException:
        # 手动回滚   同一个session中, 前一个事务如果失败, 必须手动回滚, 否则无法创建新的事务
        db.session.rollback()

    """事务2"""
    user1 = User(name='lisi', age=30)
    db.session.add(user1)
    db.session.commit()

    return "index"


if __name__ == '__main__':
    """为了进行测试, 首次运行 建表并添加一条测试数据后, 注释下方代码, 并重新运行测试"""

    # 重置所有继承自db.Model的表
    # db.drop_all()
    # db.create_all()

    # 添加一条测试数据
    # user1 = User(name='zs', age=20)
    # db.session.add(user1)
    # db.session.commit()

    app.run(debug=True)

3.4 数据迁移

  • flask-migrate 组件 为 flask-sqlalchemy提供了数据迁移功能, 以便进行数据库升级, 如增加字段、修改字段类型等
  • 安装组件 pip install flask-migrate
# hm_数据迁移.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate


app = Flask(__name__)

# 相关配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test32'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False


# SQlalchemy组件初始化
db = SQLAlchemy(app)

# 迁移组件初始化
Migrate(app, db)


# 构建模型类
class User(db.Model):
    __tablename__ = 't_user'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column('username', db.String(20), unique=True)
    age= db.Column(db.Integer, default=10, index=True)


@app.route('/')
def index():

    return "index"


if __name__ == '__main__':
    app.run(debug=True)
  • 执行迁移命令
1. export FLASK_APP=hm_数据迁移.py  # 设置环境变量指定启动文件
2. flask db init  # 生成迁移文件夹
3. flask db migrate  # ⽣成迁移版本, 保存到迁移文件夹中
4. flask db upgrade  # 执行迁移
  • 注意点:
    • 执行迁移命令前需要先设置环境变量指定启动文件

你可能感兴趣的:(#,Python,框架,flask,python,数据库,orm,软件框架)