1 学习目标
- 能够理解 ORM 工作原理以及其优缺点
- 能够写出在 Flask 中连接 MySQL 的配置项格式(IP,端口,数据库)
- 能够使用 SQLAlchemy 定义出关系为一对多模型类
- 能够使用 SQLAlchemy 的相关函数创建表及删除表
- 能够写出的指定模型数据对数据库的增删改代码
- 能够写出模型数据按照条件查询的功能逻辑
- 能够写出模型数据按照指定数量分页的功能逻辑
- 能够写出模型数据按照指定条件排序的功能逻辑
2 ORM
ORM
全拼 Object-Relation Mapping,文意为
主要实现模型对象到关系数据库数据的映射,比如:把数据库表中每条记录映射为一个模型对象。对象-关系映射,
2.1 ORM图解
优点 :
- 只需要面向对象编程,不需要面向数据库编写代码
- 对数据库的操作都转化成对类属性和方法的操作
- 不用编写各种数据库的
sql语句
- 实现了数据模型与数据库的解耦,屏蔽了不同数据库操作上的差异
- 不在关注用的是
mysql
、oracle
...等 - 通过简单的配置就可以轻松更换数据库,而不需要修改代码
- 不在关注用的是
缺点 :
- 相比较直接使用SQL语句操作数据库,有性能损失
- 根据对象的操作转换成SQL语句,根据查询的结果转化成对象,在映射过程中有性能损失
3 Flask-SQLAlchemy安装及设置
- SQLALchemy 实际上是对数据库的抽象,让开发者不用直接和 SQL 语句打交道,而是通过 Python 对象来操作数据库,在舍弃一些性能开销的同时,换来的是开发效率的较大提升
- SQLAlchemy是一个关系型数据库框架,它提供了高层的 ORM 和底层的原生数据库的操作。flask-sqlalchemy 是一个简化了 SQLAlchemy 操作的flask扩展。
- 文档地址:http://docs.jinkan.org/docs/flask-sqlalchemy
3.1 安装
- 安装 flask-sqlalchemy
pip install flask-sqlalchemy
- 如果连接的是 mysql 数据库,需要安装 mysqldb
pip install flask-mysqldb
- 在安装flask-mysqldb的时候可能会报错:mysql_config not found
- 解决如下,输入以下代码 :
sudo aptitude install libmysqlclient-dev
3.2 数据库连接设置
3.2.1 连接 MySQL 数据库
完整连接 URI 列表请跳转到 SQLAlchemy 下面的文档 (Supported Databases) 。这里给出一些 常见的连接字符串。
- Postgres:
postgresql://scott:tiger@localhost/mydatabase
- MySQL:
mysql://scott:tiger@localhost/mydatabase
- Oracle:
- oracle://scott:tiger@127.0.0.1:1521/sidname
- SQLite (注意开头的四个斜线):
sqlite:absolute/path/to/foo.db
- 在 Flask-SQLAlchemy 中,数据库使用URL指定,而且程序使用的数据库必须保存到Flask配置对象的 SQLALCHEMY_DATABASE_URI 键中
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/test'
数据库连接url的说明:
mysql://:是协议
root:用户名
mysql:密码
127.0.0.1:数据库所在电脑
3306:数据库端口号
test:数据库名
- 其他设置:
# 动态追踪修改设置,如未设置只会提示警告
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
#查询时会显示原始SQL语句
app.config['SQLALCHEMY_ECHO'] = True
- 配置完成需要去 MySQL 中创建项目所使用的数据库
mysql -uroot -pmysql
create database test charset utf8;
- 其他配置
名字 | 备注 |
---|---|
SQLALCHEMY_DATABASE_URI | 用于连接的数据库 URI 。例如:sqlite:tmp/test.dbmysql://username:password@server/db |
SQLALCHEMY_BINDS | 一个映射 binds 到连接 URI 的字典。更多 binds 的信息见用 Binds 操作多个数据库。 |
SQLALCHEMY_ECHO | 如果设置为Ture, SQLAlchemy 会记录所有 发给 stderr 的语句,这对调试有用。(打印sql语句) |
SQLALCHEMY_RECORD_QUERIES | 可以用于显式地禁用或启用查询记录。查询记录 在调试或测试模式自动启用。更多信息见get_debug_queries()。 |
SQLALCHEMY_NATIVE_UNICODE | 可以用于显式禁用原生 unicode 支持。当使用 不合适的指定无编码的数据库默认值时,这对于 一些数据库适配器是必须的(比如 Ubuntu 上 某些版本的 PostgreSQL )。 |
SQLALCHEMY_POOL_SIZE | 数据库连接池的大小。默认是引擎默认值(通常 是 5 ) |
SQLALCHEMY_POOL_TIMEOUT | 设定连接池的连接超时时间。默认是 10 。 |
SQLALCHEMY_POOL_RECYCLE | 多少秒后自动回收连接。这对 MySQL 是必要的, 它默认移除闲置多于 8 小时的连接。注意如果 使用了 MySQL , Flask-SQLALchemy 自动设定 这个值为 2 小时。 |
3.2.2 连接其他数据库
完整连接 URI 列表请跳转到 SQLAlchemy 下面的文档 (Supported Databases) 。这里给出一些 常见的连接字符串。
- Postgres:
postgresql://scott:tiger@localhost/mydatabase
- MySQL:
mysql://scott:tiger@localhost/mydatabase
- Oracle:
- oracle://scott:tiger@127.0.0.1:1521/sidname
- SQLite (注意开头的四个斜线):
sqlite:absolute/path/to/foo.db
3.2.3 常用的SQLAlchemy字段类型
类型名 | python中类型 | 说明 |
---|---|---|
Integer | int | 普通整数,一般是32位 |
SmallInteger | int | 取值范围小的整数,一般是16位 |
BigInteger | int或long | 不限制精度的整数 |
Float | float | 浮点数 |
Numeric | decimal.Decimal | 普通整数,一般是32位 |
String | str | 变长字符串 |
Text | str | 变长字符串,对较长或不限长度的字符串做了优化 |
Unicode | unicode | 变长Unicode字符串 |
UnicodeText | unicode | 变长Unicode字符串,对较长或不限长度的字符串做了优化 |
Boolean | bool | 布尔值 |
Date | datetime.date | 时间 |
Time | datetime.datetime | 日期和时间 |
LargeBinary | str | 二进制文件 |
3.2.4 常用的SQLAlchemy列选项
选项名 | 说明 |
---|---|
primary_key | 如果为True,代表表的主键 |
unique | 如果为True,代表这列不允许出现重复的值 |
index | 如果为True,为这列创建索引,提高查询效率 |
nullable | 如果为True,允许有空值,如果为False,不允许有空值 |
default | 为这列定义默认值 |
3.2.5 常用的SQLAlchemy关系选项
选项名 | 说明 |
---|---|
backref | 在关系的另一模型中添加反向引用 |
primary join | 明确指定两个模型之间使用的联结条件 |
uselist | 如果为False,不使用列表,而使用标量值 |
order_by | 指定关系中记录的排序方式 |
secondary | 指定多对多关系中关系表的名字 |
secondary join | 在SQLAlchemy中无法自行决定时,指定多对多关系中的二级联结条件 |
3.2.6 SQLAlchemy使用操作
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
# 配置数据库连接地址
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://root:mysql@127.0.0.1:3306/test_27"
# 是否追踪数据库的修改
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
@app.route("/")
def index():
return "index"
if __name__ == '__main__':
app.run(debug=True)
SQLAlchemy常用配置就这两个:uri 和 track_modifications
4 数据库基本操作
在Flask-SQLAlchemy中,插入、修改、删除操作,均由数据库会话管理。
- 会话用 db.session 表示。在准备把数据写入数据库前,要先将数据添加到会话中然后调用 commit() 方法提交会话。
在 Flask-SQLAlchemy 中,查询操作是通过 query 对象操作数据。
- 最基本的查询是返回表中所有数据,可以通过过滤器进行更精确的数据库查询。
4.1 添加模型&增删改
4.1.1 准备工作
通过对象操作数据库,那么首先需要创建数据库 test:
create database test charset utf8;
进入看一眼,目前还没有表:
4.1.2 添加模型
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
# 配置数据库连接地址
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://root:mysql@127.0.0.1:3306/test"
# 是否追踪数据库的修改
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
class Role(db.Model):
# 指定该模型对应数据库中的表名,如果不指定,表名就为类名小写,即“role”
__tablename__ = "roles"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(64), unique=True)
def __repr__(self):
return "Role: %s %s" % (self.id, self.name)
@app.route("/")
def index():
return "index"
if __name__ == '__main__':
db.create_all()
app.run(debug=True)
代码说明:
__repr__ :类似于__str__,打印Role实例对象的时候会调用__repr__
db.create_all():创建所有继承 db.Model 的模型对应的数据库表
接下来运行此程序,然后查看表:
4.1.3 增加
通过 pycharm 的 terminal 进入 ipython3,输入如下代码:
增加数据,需要新建Role实例对象,并且添加到db.session中,最终还要commit提交才可以
注意:
这里的session(会话)跟视图中操作的session不一样,这里他是自己做了一个会话,可以将多个数据库操作一次性添加到会话中,最终commit一次即可
commit之后,role.id竟然有值了,这是因为插入成功之后,数据库有了id值,然后SQLAlchemy给数据库的改变反馈到了实例对象role上了,所以role.id有值了
插入成功之后,查询数据库:(插入之前没有数据,插入之后有一条数据)
4.1.4 修改
注意:
这里的修改操作也应该通过db.session.add(role)来处理,但是因为上边增加操作已经将role 添加到db.session中了。所以这里可以省略第二步。(其实建议再写一遍,这样更规范)
4.1.5 删除
注意:删除操作是通过db.session.delete()来处理的
4.2 数据库一对多的关系定义
4.2.1 准备工作
接下来我们讨论一下多表的关系:一对多关系
那么就需要增加一个模型类User,Role 与 User 的关系是一对多。
增加User如下:
增加测试数据:
if __name__ == '__main__':
db.drop_all()
db.create_all()
# 插入一条数据
ro1 = Role(name='admin')
db.session.add(ro1)
db.session.commit()
# 再次插入一条数据
ro2 = Role(name='user')
db.session.add(ro2)
db.session.commit()
# 一次插入多条数据
us1 = User(name='wang', email='wang@163.com', password='123456', role_id=ro1.id)
us2 = User(name='zhang', email='zhang@189.com', password='201512', role_id=ro2.id)
us3 = User(name='chen', email='chen@126.com', password='987654', role_id=ro2.id)
us4 = User(name='zhou', email='zhou@163.com', password='456789', role_id=ro1.id)
us5 = User(name='tang', email='tang@itheima.com', password='158104', role_id=ro2.id)
us6 = User(name='wu', email='wu@gmail.com', password='5623514', role_id=ro2.id)
us7 = User(name='qian', email='qian@gmail.com', password='1543567', role_id=ro1.id)
us8 = User(name='liu', email='liu@itheima.com', password='867322', role_id=ro1.id)
us9 = User(name='li', email='li@163.com', password='4526342', role_id=ro2.id)
us10 = User(name='sun', email='sun@163.com', password='235523', role_id=ro2.id)
db.session.add_all([us1, us2, us3, us4, us5, us6, us7, us8, us9, us10])
db.session.commit()
app.run(debug=True)
问题:
- 为啥create_all()之前要先drop_all()?
因为执行已经生成过Role表了,现在如果重新创建,肯定会报错,所以先删除所有表,在重新创建
- 为啥ro1和ro2添加到db.session之后,要立马commit,不能和user一起commit么?
运行,生成表和数据:
4.2.2 关联查询
进入ipython3,我们来看一下一对多查询:
说明:
all 返回两个用户的列表,列表中的每一项是一个role对象;
为啥是Role 1 admin,这是因为__repr__方法就是这样返回的
我们先执行如下代码:
然后获取id3的用户的角色信息:
你会发现这样查询很麻烦,需要先通过id3查询到user,然后在查询user身上的role_id角色id,在通过角色id查询Role表才可以知道。那我们畅想一下:
如何实现畅想呢?给Role添加一个关系属性users:
上图代码说明:users属性关联着User模型类
接下来查询:
那么想查询用户所属角色怎么办?在后边加上一句:backref("role")
users = db.relationship("User", backref="role")
查询:
4.3 数据库的简单查询
4.3.1 常用的SQLAlchemy查询过滤器
过滤器 | 说明 |
---|---|
filter() | 把过滤器添加到原查询上,返回一个新查询 |
filter_by() | 把等值过滤器添加到原查询上,返回一个新查询 |
limit | 使用指定的值限定原查询返回的结果 |
offset() | 偏移原查询返回的结果,返回一个新查询 |
order_by() | 根据指定条件对原查询结果进行排序,返回一个新查询 |
group_by() | 根据指定条件对原查询结果进行分组,返回一个新查询 |
4.3.2 常用的SQLAlchemy查询执行器
方法 | 说明 |
---|---|
all() | 以列表形式返回查询的所有结果 |
first() | 返回查询的第一个结果,如果未查到,返回None |
first_or_404() | 返回查询的第一个结果,如果未查到,返回404 |
get() | 返回指定主键对应的行,如不存在,返回None |
get_or_404() | 返回指定主键对应的行,如不存在,返回404 |
count() | 返回查询结果的数量 |
paginate() | 返回一个Paginate对象,它包含指定范围内的结果 |
4.3.3 牛刀小试
接下来我们给下边的所有题目做完即可:
查询所有用户数据
查询有多少个用户
查询第1个用户
查询id为4的用户[3种方式]
查询名字结尾字符为g的所有数据[开始/包含]
查询名字不等于wang的所有数据[2种方式]
查询名字和邮箱都以 li 开头的所有数据[2种方式]
查询password是 `123456` 或者 `email` 以 `itheima.com` 结尾的所有数据
查询id为 [1, 3, 5, 7, 9] 的用户列表
查询name为liu的角色数据
查询所有用户数据,并以邮箱排序
每页3个,查询第2页的数据
答案如下:
In [1]: from test import *
In [2]: User.query.all()
Out[2]:
[User: 1 wang,
User: 2 zhang,
User: 3 chen,
User: 4 zhou,
User: 5 tang,
User: 6 wu,
User: 7 qian,
User: 8 liu,
User: 9 li,
User: 10 sun]
In [3]: User.query.count()
Out[3]: 10
In [4]: User.query.first()
Out[4]: User: 1 wang
In [5]: User.query.get(4)
Out[5]: User: 4 zhou
In [6]: User.query.filter_by(id=4).first()
Out[6]: User: 4 zhou
In [7]: User.query.filter(User.id==4).first()
Out[7]: User: 4 zhou
In [10]: User.query.filter(User.name.endswith("g")).all()
Out[10]: [User: 1 wang, User: 2 zhang, User: 5 tang]
In [11]: User.query.filter(User.name.endswith("g")).all()
Out[11]: [User: 1 wang, User: 2 zhang, User: 5 tang]
In [12]: User.query.filter(User.name.startswith("g")).all()
Out[12]: []
In [13]: User.query.filter(User.name.contains("g")).all()
Out[13]: [User: 1 wang, User: 2 zhang, User: 5 tang]
In [14]: User.query.filter(User.name != "wang").all()
Out[14]:
[User: 2 zhang,
User: 3 chen,
User: 4 zhou,
User: 5 tang,
User: 6 wu,
User: 7 qian,
User: 8 liu,
User: 9 li,
User: 10 sun]
In [17]: from sqlalchemy import not_
In [18]: User.query.filter(User.name != "wang").all()
Out[18]:
[User: 2 zhang,
User: 3 chen,
User: 4 zhou,
User: 5 tang,
User: 6 wu,
User: 7 qian,
User: 8 liu,
User: 9 li,
User: 10 sun]
In [19]: from sqlalchemy import not_
In [20]: User.query.filter(not_(User.name == "wang")).all()
Out[20]:
[User: 2 zhang,
User: 3 chen,
User: 4 zhou,
User: 5 tang,
User: 6 wu,
User: 7 qian,
User: 8 liu,
User: 9 li,
User: 10 sun]
In [21]: User.query.filter(User.name.startswith("li"), User.email.startswith("li")).all()
Out[21]: [User: 9 li, User: 8 liu]
In [22]: from sqlalchemy import and_
In [23]: User.query.filter(and_(User.name.startswith("li"), User.email.startswith("li"))).all()
Out[23]: [User: 9 li, User: 8 liu]
In [24]: from sqlalchemy import or_
In [25]: User.query.filter(or_(User.password == "123456", User.email.endswith("itheima.com"))).all
...: ()
Out[25]: [User: 1 wang, User: 5 tang, User: 8 liu]
In [26]: User.query.filter(User.id.in_([1, 3, 5, 7, 9])).all()
Out[26]: [User: 1 wang, User: 3 chen, User: 5 tang, User: 7 qian, User: 9 li]
In [28]: User.query.filter(User.name == "liu").first().role
Out[28]: Role: 1 admin
In [29]: User.query.order_by(User.email).all()
Out[29]:
[User: 3 chen,
User: 9 li,
User: 8 liu,
User: 7 qian,
User: 10 sun,
User: 5 tang,
User: 1 wang,
User: 6 wu,
User: 2 zhang,
User: 4 zhou]
In [30]: User.query.order_by(User.email.desc()).all()
Out[30]:
[User: 4 zhou,
User: 2 zhang,
User: 6 wu,
User: 1 wang,
User: 5 tang,
User: 10 sun,
User: 7 qian,
User: 8 liu,
User: 9 li,
User: 3 chen]
In [31]: User.query.order_by(User.email.asc()).all()
Out[31]:
[User: 3 chen,
User: 9 li,
User: 8 liu,
User: 7 qian,
User: 10 sun,
User: 5 tang,
User: 1 wang,
User: 6 wu,
User: 2 zhang,
User: 4 zhou]
In [34]: paginate = User.query.paginate(2, 3)
In [35]: paginate.items
Out[35]: [User: 4 zhou, User: 5 tang, User: 6 wu]
In [36]: paginate.pages
Out[36]: 4
In [37]: paginate.page
Out[37]: 2