为什么不使用SQL语句,而使用ORM框架管理数据库?首先,在python程序中嵌入原生SQL语句,不方便维护,ORM框架使用面向对象思想,使用较方便;第二,如果更换底层数据库引擎,ORM框架不需要修改代码。ORM框架也有其弊端,如有一定的性能损耗,且高级查询编写复杂,有一定的学习成本。
1、配置数据库链接
Flask-SQLAlchemy 中,数据库使用 URL 指定。一般开发环境使用SQLite,生产环境使用MySQL,使用MySQL时,还需要再安装相应的依赖包。
几种最流行的数据库引擎使用的 URL 格式:
数据库引擎 | URL |
---|---|
MySQL | mysql://username:password@hostname/database |
SQLite(Linux,macOS) | sqlite:////absolute/path/to/database |
SQLite(Windows) | sqlite:///c:/absolute/path/to/database |
配置数据库:
app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# SQLALCHEMY_TRACK_MODIFICATIONS 参数决定是否追踪对象的修改,SQLAlchemy建议配置此变量,不然会有警告。
2、定义模型
模型这个术语表示应用使用的持久化实体。在ORM中,模型一般是一个表示数据表的类,类的实例对应一条记录,类中的属性对应于数据库表中的列。
所以模型定义,类似原生SQL的CREATE TABLE语句:
class Role(db.Model):
# 定义表名
__tablename__ = 'roles'
# 定义字段
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
def __repr__(self):
return '<Role %r>' % self.name
2.1 常用列类型:
类型名 | Python类型 | 说明 |
---|---|---|
Integer |
int |
普通整数,通常是 32 位 |
SmallInteger |
int |
取值范围小的整数,通常是 16 位 |
BigInteger |
int 或 long |
不限制精度的整数 |
Float |
float |
浮点数 |
Numeric |
decimal.Decimal |
定点数 |
String |
str |
变长字符串 |
Text |
str |
变长字符串,对较长或不限长度的字符串做了优化 |
Unicode |
unicode |
变长 Unicode 字符串 |
UnicodeText |
unicode |
变长 Unicode 字符串,对较长或不限长度的字符串做了优化 |
Boolean |
bool |
布尔值 |
Date |
datetime.date |
日期 |
Time |
datetime.time |
时间 |
DateTime |
datetime.datetime |
日期和时间 |
Interval |
datetime.timedelta |
时间间隔 |
Enum |
str |
一组字符串 |
PickleType |
任何 Python 对象 | 自动使用 Pickle 序列化 |
LargeBinary |
str |
二进制 blob |
2.2 常用列选项
选项名 | 说明 |
---|---|
primary_key |
如果设为 True ,列为表的主键 |
uniquey |
如果设为 True ,列不允许出现重复的值 |
indexy |
如果设为 True ,为列创建索引,提升查询效率 |
nullabley |
如果设为 True ,列允许使用空值;如果设为 False ,列不允许使用空值 |
defaulty |
为列定义默认值 |
3、关系
关系型数据库的表和表之间需要建立“一对多”,“多对一”和“一对一”的关系,这样才能够按照应用程序的逻辑来组织和存储数据。在关系型数据库中,关系是通过主键和外键来维护,其中外键既可以通过数据库来约束,也可以不设置约束,仅依靠应用程序的逻辑来保证,这里讨论使用数据库来约束的情况。
3.1 一对多
例如角色和用户是一对多关系,一个用户只对用一个角色,而一个角色可以对应多个用户:
class Role(db.Model):
# ...
users = db.relationship('User', backref='role', lazy='dynamic')
class User(db.Model):
# ...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
- 添加到
User
模型中的role_id
列被定义为外键,就是这个外键建立起了关系。传给db.ForeignKey()
的参数'roles.id'
表明,这列的值是roles
表中相应行的id
值。
- 添加到
Role
模型中的users
属性代表这个关系的面向对象视角,对于一个Role
类的实例,其users
属性将返回与角色相关联的用户组成的列表。-
db.relationship()
的第一个参数表明这个关系的另一端是哪个模型 -
backref
参数向User
模型中添加一个role
属性,从而定义反向关系。通过User
实例的这个属性可以获取对应的Role
模型对象,而不用再通过role_id
外键获取。 -
lazy
参数会告诉SQLAlchemy如何去加载我们指定的关联对象。如果设为子查询方式 (subquery),则会在加载完Post对象的时候,就立即加载与其关联的对象。这样会让总查询数量减少,但如果返回的条目数量很多,就会比较慢。另外,也可以设置为动态方式 (dynamic),这样关联对象会在被使用的时候再进行加载,并且在返回前进行过滤。如果返回的对象数很多,或者未来会变得很多,那最好采用这种方式
-
3.2 一对一
一对一关系可以用前面介绍的一对多关系表示,但调用 db.relationship()
时要把 uselist
属性设为 False
,把“多”变成“一”。
3.3 多对一
多对一关系从“多”这一侧看,就是一对多关系,对调两个表即可。
3.4 多对多
上述关系至少都有一侧是单个实体,所以记录之间的联系通过外键实现,让外键指向那个实体。但是,可能两侧都是“多”的关系,如有学生表和课程表,学生可能选多个课程,课程也可能被多个学生选。这种时候一般使用第三张表(即关联表)来表示关系:
class Student(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
classes = db.relationship('Class',
secondary=registrations,
backref=db.backref('students', lazy='dynamic'),
lazy='dynamic')
class Class(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
registrations = db.Table('registrations',
db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
db.Column('class_id', db.Integer, db.ForeignKey('classes.id'))
)
多对多关系仍使用定义一对多关系的 db.relationship()
方法定义,但在多对多关系中,必须把 secondary
参数设为关联表。多对多关系可以在任何一个类中定义,backref
参数会处理好关系的另一侧。关联表就是一个简单的表,不是模型,SQLAlchemy 会自动接管这个表。
Student
中classes
关系使用列表语义,这样处理多对多关系特别简单:
# 查看Student实例所选课程、Class实例被选情况
>>> s.classes.all()
>>> c.students.all()
# 增加课程
>>> s.classes.append(c)
>>> db.session.add(s)
>>> db.session.commit()
# 取消选课
>>> s.classes.remove(c)
>>> db.session.add(c)
>>> db.session.commit()
3.5 自引用
多对多有种特殊情况,即关系中的两侧都在同一个表中,这种关系称为自引用关系。如用户表之间的关注关系,
4、数据库操作
对数据库的改动通过数据库会话管理,在 Flask-SQLAlchemy 中,数据库会话由 db.session 表示。调用 db.session.commit()提交事务,db.session.rollback() 回滚事务。
4.1 创建表、删除表
db.create_all()
db.drop_all()
4.2 CREATE
admin_role = Role(name='Admin')
db.session.add(admin_role)
db.session.add_all([admin_role, mod_role])
db.session.commit()
4.3 UPDATE
# 方法一
admin_role.name = 'Administrator'
db.session.add(admin_role)
# 方法二
User.query.filter_by(username='Alice').update({'password': 'test123'})
# 修改后commit
db.session.commit()
4.4 DELETE
db.session.delete(mod_role)
db.session.commit()
4.5 READ
<模型类>.query.<过滤方法>.<查询方法>
基本查询:
#------------------基础查询-----------------------------
# 返回所有数据集合
>>> User.query.all()
# 返回前十条数据集合
>>> users = User.query.limit(10).all()
# 返回查到的第一条记录
>>> users = User.query.first()
# 返回主键为1的记录
>>> user = User.query.get(1)
# 对查询结果排序
>>> users = User.query.order_by(User.username).all()
# 逆序
>>> users = User.query.order_by(User.username.desc()).all()
# 链式调用
>>> users = User.query.order_by(
User.username.desc()
).limit(10).first()
#------------------条件查询:filter_by(精确条件)-----------------------------
# 单条件
>>> User.query.filter_by(role=user_role).all()
>>> User.query.filter_by(role=user_role).first()
# 多条件
>>> User.query.filter_by(password='123',username='test0').all()
>>> User.query.filter_by(password='123').filter_by(username='test0').all()
# 带条件的链式调用
>>> users = User.query.order_by(User.username.desc())
.filter_by(username='fake_name')
.limit(2)
.all()
#------------------条件查询:filter(模糊条件>、<、==...)-----------------------------
>>> user = User.query.filter(User.id > 1).all()
#------------------分页查询:paginate-----------------------------
# 获取前十个虚构的User对象
>>> page = User.query.paginate(1, 10)
# 返回这一页包含的数据对象
>>> page.items
# 返回这一页的页数
>>> page.page
# 返回总页数
>>> pages
# 上一页和下一页是否有对象可以显示
>>> page.has_prev, page.has_next
# 返回上一页和下一页的 pagination 对象如果不存在的话则返回当前页
>>> page.prev(), page.next()
一些复杂的SQL查询可以转为用SQLAlchemy的函数来表示。例如,可以像下面这样实现SQL中IN、OR和NOT的比较操作:
>>> from sqlalchemy.sql.expression import not_, or_
>>> user = User.query.filter(
User.username.in_(['fake_name']),
User.password == None
).first()
# 找出拥有密码的用户
>>> user = User.query.filter(
not_(User.password == None)
).first()
# 这些方法都可以被组合起来
>>> user = User.query.filter(
or_(not_(User.password == None), User.id >= 1)
).first()
5、数据库迁移
配置flask-migrate:
# ...
from flask_migrate import Migrate, MigrateCommand
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
# ...
开始跟踪数据库变更,使用init命令:
$ python manage.py db init
这会在项目目录中创建一个叫作migrations的文件夹,所有的记录文件会被保存在里面。现在可以开始进行首次迁移:
$ python manage.py db migrate -m "initial migration"
执行迁移脚本
$ python manage.py db upgrade
要返回以前的版本,则可以根据history命令找到版本号,然后传给downgrade命令:
$ python manage.py db history
<base> -> 7ded34bc4fb (head), initial migration
$ python manage.py db downgrade 7ded34bc4fb
6、集成到flask shell
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
参考
sqlalchemy中文文档:https://www.osgeo.cn/sqlalchemy/
sqlalchemy英文文档:https://docs.sqlalchemy.org/
flask-sqlalchemy文档:http://www.pythondoc.com/flask-sqlalchemy/index.html