编辑
2022-11-11
🧰语言-python
00
请注意,本文编写于 951 天前,最后修改于 225 天前,其中某些信息可能已经过时。

目录

1.据库的分类
2.ORM魔法
3.使用flask-sqlalchemy管理数据库
3.1实例化
3.2 链接数据库服务
3.3 定义数据库模型
3.4 创建数据库和表
4.数据库操作
5.定义关系
5.1 一对多
5.2 多对一
5.3 一对一
6.更新数据库表
6.1 重新生成表
6.2 使用flask-Migrate迁移数据库
6.3 开发时是否需要迁移?
7.数据库进阶实践
7.1 级联操作
7.2 事件监听

1.据库的分类

分为SQL和No Sql

NoSQL数据库不需要定义表和列等结果,也不限定存储的数据格式,在存储方式上比较灵活,在特定的场景下效率更高。

SQL数据库稍显复杂,但不容易出错,能够适应大部分的应用场景。

大型项目通常会同时需要多种数据库,比如:

使用MySQL作为主数据库存储用户资料和文章

使用Redis缓存数据

使用MongoDB存储实时消息

2.ORM魔法

问题:

  • 手动编写SQL语句比较乏味,而且视图函数中加入太多SQL语句会降低代码的易读性,另外还会容易出现安全的问题,比如SQL注入
  • 常见的开发模式是在开发时使用简单的SQLite,而在部署时切换到MySQL等更健壮的DBMS。但是对于不同的DBMS,我们需要使用不同的Python接口库,这让DBMS的切换变得不太容易。

数据库管理系统(Database Management System)是一种操纵和管理数据库的大型软件,用于建立、使用和维护数据库,简称DBMS。

ORM实现了三层映射关系:

  • 表 ——>Python类
  • 字段(列)——>类属性
  • 记录(行)——>类实例

3.使用flask-sqlalchemy管理数据库

3.1实例化

from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) db = SQLAlchemy(app)

3.2 链接数据库服务

常用的数据库库URI格式

DBMSURI
PostgreSQLpostgresql://username
@host/databasename
MySQLmysql://username
@host/databasename
Oracleoracle://username
@host
/sidname
SQLite(UNIX)sqlite:////absolute/path/to/foo.db
SQLite(Windows)sqlite:///absolute\path\to\foo.db
SQlite(内存型)sqlite:///或sqlite:///:memory:

配置数据库URI

import os app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL','sqlite:///'+os.path.join(app.root_path,'data.db'))

3.3 定义数据库模型

定义Note模型

class Note(db.Model): id = db.Column(db.Interger,primary_key=True) body = db.Column(db.Text)

常用的字段类型

字段说明
Integer整数
String字符串,可选参数length可以用来设置最大长度
Text较长的Unicode文本
Date日期,存储Python的datetime.date对象
Time日期,存储Python的datetime.time对象
DateTime时间和日期,存储Python的datetime对象
Interval时间间隔,存储Python的datetime.timedelta对象
Float浮点数
Boolean布尔值
PickleType存储Pickle列化的Python对象
LargeBinary存储任意二进制数据

常用的SQLalchemy字段参数

参数名说明
primary_key如果设为True,该字段为主键
unique如果设为True,该字段不允许出现重复值
index如果设为True,为该字段创建索引,以提高查询效率
nullable确定字段值可否为空,值为True或False
default为字段设置默认值

3.4 创建数据库和表

import click @app.cli.command() def initdb(): db.create_all() click.echo('Initialized database.') >>> flask initdb

4.数据库操作

数据库操作

Read

<模型类>.query.<过滤方法>.<查询方法>

常用的查询方法:

查询方法说明
all()返回包含所有查询记录的列表
first()返回查询的第一条记录,如果未找到,则返回None
one()返回第一条记录,且仅允许有一条记录。如果记录数量大于1或小于1,则抛出错误。
get(indent)传入主键值作为参数,返回指定主键值的记录,如果未找到,则返回None
count()返回查询结果的数量
one_or_none()类似one(),如果结果数量不为1,返回None
first_or_404()返回查询的第一条记录,如果未找到,则返回404错误响应
get_or_404()传入主键值作为参数,返回指定主键值的记录,如果未找到,则返回404错误响应
paginate返回一个Pagination对象,可以对记录进行分页处理
with_parent(instance)传入模型类实例作为参数,返回和这个实例相关联的对象

常用的过滤方法

查询过滤器名称说明
filter()使用指定的规则过滤记录,返回新产生的查询对象
filter_by()使用指定规则记录(以关键字表达式的形式),返回新产生的查询对象
order_by()根据指定条件对记录进行排序,返回新产生的查询对象
limit(limit)使用指定的值限制原查询返回的记录数量,返回新产生的查询对象
group_by()根据指定条件对记录进行分组,返回新产生的查询对象
offset(offset)使用指定的值偏移原查询的结果,返回新产生的查询对象

filter的相关操作

LIKE:

filter(Note.body.like('%foo%'))

IN:

filter(Note.body.in_(['foo','bar','baz']))

NOT IN:

filter(~Note.body.in_(['foo','bar','baz']))

AND:

# 使用 and_() from sqlalchemy import and_ filter(and_(Note.body=='foo',Note.title =='FooBar')) # 或在filter()中加入多个表达式,使用逗号分隔 filter(Note.body=='foo',Note.title=='FooBar') # 或叠加调用多个filter()/filter_by()方法 filter(Note.body=='foo').filter(Note.title=='FooBar')

OR:

from sqlachemy import or_ filter(or_(Note.body=='foo',Note.body=='bar'))

Update

note = Note.query.get(2) note.body = 'SHAVE LEFT THIGH' db.session.commit()

Delete

note = Note.query.get(2) db.session.delete(note) db.session.commit()

5.定义关系

5.1 一对多

定义外键

定义关系的第一步是创建外键。

外键是用来在A表存储B表的主键值以便和B表建立联系的关系字段。

因为外键只能存储单一数据(标量),所以外键总是在“多”这一侧定义。

class Article(db.Model): author_id = db.Column(db.Interger,db.ForeignKey('author.id'))

定义关系属性

定义关系的第二步是使用关系函数定义关系属性。关系属性在关系的出发侧定义,即一对多关系的“一”这一侧

class Author(db.Model): articles = db.relationship('Article')

建立关系

spam.author_id =1 db.session.commit()

常用的SQLalchemy关系函数参数

参数名说明
back_populates定义反向引用,用于建立双向关系,在关系的另一侧也必须显示定义关系属性
backref添加反向引用,自动在另一侧建立关系属性,是back_populates的简化版
lazy指定如何加载相关记录
uselist指定是否使用列表的形式加载记录,设为False则使用标量scalar
cascade设置级联操作
order_by指定加载相关记录时的排序方式
secondary在多对多关系中指定关联表
primaryjoin指定多对多关系中的一级联结条件
secondaryjoin指定多对多关系中的二级联结条件

常用的sqlalchemy关系记录加载方式(lazy参数可选值)

关系加载方式说明
select在必要时一次性加载记录,返回包含记录的列表,等同于lazy=True
joined在父查询一样加载记录,但使用联结,等同于lazy=True
immediate一旦父查询加载就加载
dynamic不直接加载记录,而是返回一个包含相关记录的query对象,以便再继续附加查询函数对结果进行过滤
subquery类似joined,不过将使用子查询
dynamic选项仅用于集合关系属性,不可用于多对一、一对一或是在关系函数中将uselist参数设为False的情况。

建立双向关系

基于一对多关系的双向关系 class Writer(db.Model): id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(70),unique=True) books = db.relationship('Book',back_populates='wirter') class Book(db.Model): id = db.Column(db.Integer,primary_key=True) title = db.Column(db.String(50),index=True) writer_id = db.Column(db.Integer,db.ForeignKey('writer.id')) writer = db.relationship('Writer',back_populates='books')

使用backref简化关系定义

使用关系函数中的backref参数可以简化双向关系的定义。

以一对多关系为例,backref参数用来自动为关系另一侧添加关系属性,作为反向引用,赋予的值会作为关系另一侧的关系属性名称。

比如,我们在Author一侧的关系函数中将backref参数设为author,SQLAlchemy会自动为Article类添加一个author属性。

为了避免和前面的示例命名冲突,我们使用歌手和歌曲的一对多关系作为演示:

使用backref建立双向关系

class Singer(db.Model): id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(70),unique=True) songs = db.relationship('Song',backref='singer') class Song(db.Model): id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(50),index=True) singer_id = db.Column(db.Integer,db.ForeignKey('singer.id'))

在定义集合属性songs的关系函数中,我们将backref参数设为singer,这会同时在Song类中添加了一个singer标量属性。

这时我们仅需要定义一个关系函数,虽然singer是一个”看不见的关系属性“,但在使用和定义两个关系函数并使用back_populates参数的效果完全相同。

需要注意的是,使用backref允许我们仅在关系一侧定义另一侧的关系属性,但是在某些情况下,我们希望可以对在关系另一侧的关系属性进行设置,这时就需要使用backref函数。

backref()函数接受第一个参数作为在关系另一侧添加的关系属性名,其他关键字参数会作为另一侧关系函数的参数传入。

比如,我们要在关系另一侧”看不见的relationship()函数“中将uselist参数设为False,可以这样实现:

class Singer(db.Model): songs = relationship('Song',backref=backref('singer',userlist=False))

尽管使用backref非常方便,但通常来说”显式好过于隐式“,所以我们应该尽量使用back_populates定义双向关系。为了便于理解,在本书的示例程序中都将使用back_populates来建立双向关系。

5.2 多对一

建立多对一关系

citizen表示居民,city类表示城市,表示多个居民住在同一个城市

class Citizen(db.Model): id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(70),unique=True) city_id = db.Column(db.Integer,db.ForeignKey('city.id')) city = db.relationship('City') class City(db.Model): id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(30),unique=True)

这时定义的city关系属性是一个标量属性。

当Citizen.city被调用时,SQLAlchemy会根据外键字段city_id存储的值查找对应的City对象并返回,即居民记录对应的城市记录。

当建立双向关系时,如果不使用backref,那么一对多和多对一关系模式在定义上完全相同,这时可以将一对多和多对一视为同一种关系模式。

5.3 一对一

一对一关系实际上是通过建立双向关系的一对多关系的基础上转换而来。

我们要确保关系两侧的关系属性都是标量属性,都只返回单个值,所以要在定义集合属性的关系函数中将uselist参数设为False,这时一对多关系将被转换为一对一关系。

建立一对一关系

class Country(db.Model): id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(30),unique=True) capital = db.relationship('Capital',uselist=False) class Capital(db.Model): id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(30),unique=True) country_id = db.Column(db.Integer,db.ForeignKey('country.id')) country = db.relationship('Country')

”多“这一侧本身就是标量属性,不用做任何活动。

而”一“这一侧的集合关系属性,通过将uselist设为False后,将仅返回对应的单个记录,而且无法使用列表语义操作。

多对多

使用学生和老师来演示多对多关系:每个学生有多个老师,而每个老师有多个学生。

在一对多关系中,我们可以在”多“这一侧添加外键指向”一“这一侧,外键只能存储一个记录,但是在多对多关系中,每一个记录都可以与关系另一侧的多个记录建立关系,关系两侧的模型都需要存储一组外键。

在SQLALchemy中,要想表示多对多关系,除了关系两侧的模型外,还需要创建一个关联表(association table)。关联表不存储数据,只用来存储关系两侧模型的外键对应关系。

association_table = db.Table('association',db.Column('student_id',db.Integer,db.ForeignKey('student.id')),db.Column('teacher_id',db.Integer,db.ForeignKey('teacher.id'))) class Student(db.Model): id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(70),unique=True) grade = db.Column(db.String(20)) teachers = db.relationship('Teacher',secondary=association_table,back_populates='students') class Teacher(db.Model): id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(70),unique=True) office = db.Column(db.String(70)) students = db.relationship('Student',secondary=association_table,back_populates='teachers')

关联表使用db.Table类定义,传入的第一个参数是关联表名称。

我们在关联表中定义了两个外键字段:teacher_id字段存储Teacher类的主键,student_id存储Student类的主键。

借助关联表这个中间人存储的外键对,我们可以把多对多关系分化为两个一对多关系。

6.更新数据库表

6.1 重新生成表

db.drop_all() db.create_all()

为了方便开发,我们修改Initdb命令函数的内容,为其增加一个 --drop选项来支持删除表和数据库后进行重建:

@app.cli.command() @click.option('--drop',is_flag=True,help='Create after drop.') def initdb(drop): if drop: click.config('This opration will delete the database, do you want to continue?',about=True) db.drop_all() click.echo('Drop tables.') db.create_all() click.ehco('Initialized database.')

6.2 使用flask-Migrate迁移数据库

在开发时,以删除表再重建的方式更新数据库简单直接,但明显的缺陷是会丢掉数据库中的所有数据。

在生产环境下,你绝对不会想让数据库里的数据被删除掉,这时需要使用数据库迁移工具来完成这个工作。

6.3 开发时是否需要迁移?

在生产环境下,当对数据库结构进行修改后,进行数据库迁移是必要的。因为你不想损坏任何数据,毕竟数据是物价的。

在生成自动迁移脚本后,执行更新之前,对迁移脚本进行检查,甚至是使用备份的数据库进行迁移测试,都是有必要的。

而在开发环境中,你可以按需要选择是否进行数据迁移。

对于大多数程序来说,我们可以在开发时使用虚拟数据生成工具来生成虚拟数据,从而避免手动创建记录进行测试。

这样每次更改表结构时,可以直接清除后重新生成。

然后生成测试数据,这要比执行一次迁移简单很多,除非生成虚拟数据耗费的时间过长。

另外,当SQLite数据库表的字段删除或修改后,我们没法直接使用迁移工具进行更新,你需要手动添加迁移代码来进行迁移。

在开发中,修改和删除列是很常见的行为,手动操作迁移会花费太多的时间。

7.数据库进阶实践

7.1 级联操作

Cascade意为:“级联操作”,就是在操作一个对象的同时,对相关的对象也执行某些操作。

我们通过Post模型和Comment模型来演示级联操作:

class Post(db.Model): id = db.Column(db.Integer,primary_key=True) title = db.Column(db.String(50),unique=True) body = db.Column(db.Text) comments = db.relationship('Comment',back_populates='post') class Comment(db.Model): id = db.Column(db.Integer,primary_key=True) body = db.Column(db.Text) post_id = db.Column(db.Integer,db.ForeignKey('post.id')) post = db.relationship('Post',back_populates='comments')

级联行为通过关系函数relationship()的cascade参数设置。

希望在操作Post对象时处于附属地位的Comment对象也被相应执行某些操作,这时应该在post类的关系函数中定义级联参数。

设置了cascade参数的一侧将被视为父对象,相关的对象则被视为子对象。

cascade通常使用多个组合值,级联值之间使用逗号分隔,比如:

class Post(db.Model): comments = relationship('Comment',cascade='save-update,merge,delete')

当没有设置cascade参数时,会使用默认值save-update、merge,all等同于除了delete-orphan以外所有可用值的组合,即save-update、merge、refresh-expire、expunge、delete。

save-update

save-update是默认的级联行为,当cascade参数设为save-update时,如果使用db.session.add()方法将post对象添加到数据库会话时,那么与Post相关联的Comment对象也将被添加到数据库会话,

post1 = Post() comment1 = Comment() comment2 = Commnet() # 将post1添加到数据库会话时,只有post1在数据库会话中 db.session.add(post1) >>>post1 in db.session True >>>comment1 in db.session False # 如果我们让post1与这两个Comment对象建立关系,那么这两个Comment对象也会被自动被添加到数据库会话中。 >>> post1.comments.append(comment1) >>>post1.comments.append(comment2) >>> comment1 in db.session True

当调用db.session.commit()提交数据库会话时,这3个对象都会被提交到数据库中。

delete

如果某个post对象被删除,那么按照默认的行为,该Post对象相关联的所有Comment对象都将与这个Post对象取消关联,外键字段的值会被清空,如果post类的关系函数中的cascade参数设为delete,这些相关的Comment会在关联的Post对象删除时被一并删除。当需要设置delete级联时,我们会将级联值设为all或save-update、merge、delete。比如:

class Post(db.Model): comments = relationship('Comment',cascade='all') post2 = Post() comment3 = Comment() comment4 = Comment() post2.comments.append(comment3) post2.comments.append(comment4) db.session.add(post2) db.session.commit()

现在共有2条Post记录,和4条comment记录

>>>Post.query.all() [<Post 1>,<Post 2>] >>>Comment.query.all() [<Comment 1>,<Comment 2>,<Comment 3>,<Comment 4>]

如果删除文章对象post2,那么对应的2个评论对象也会一并被删除。

delete-orphan

orphan是孤儿的意思

这个模式是基于delete级联的,必须和delete级联一起使用,通常设为all、delete-orphan。

当某个Post对象被删除时,所有相关的Comment对象都将被删除(delete级联)。

除此之外,当某个Post对象(父对象)与某个Comment对象(子对象)解除关系时,也会删除该comment对象,这个解除关系的对象被称为孤立对象

class Post(db.Model): comments = relationship('Comment',cascade='all,delete-orphan') post3 = Post() comment5 = Comment() comment6 = Comment() post3.comments.append(comment5) post3.comments.append(comment6) db.session.add(post3) db.session.commit()

下面将comment5和comment6与post3解除关系并提交数据库会话

post3.comments.remove(comment5) post3.comments.remove(comment6) db.session.commit()

默认情况下,相关评论对象的外键会被设为空值,因为我们设置了delete-orphan级联,所以现在你会发现解除关系的两条记录被删除了

>>>Comment.query.all() [<Comment 1>,<Comment 2>]

7.2 事件监听

SQLAlchemy提供了一个listen_for()装饰器,它可以用来注册事件回调函数。

listen_for()装饰器主要接受2个参数,target表示监听的对象,这个对象可以是模型类、类实例、类属性等。

identifier参数表示被监听时间的标识符,比如,用于监听属性的事件标识符有set、append、remove、init_scalar、init_collection等。

为了演示事件监听,创建了一个Draft模型类表示草稿,其中包含字段和edit_time字段,分别存储草稿正文和被修改的次数,其中edit_time字段的默认值为0

class Draft(db.Model): id = db.Column(db.Integer,priamary_key=True) body = db.Column(db.Text) edit_time = db.Column(db.Integer,default=0)

set事件监听函数

@db.event.listens_for(Draft.body,'set') def incrment_edit_time(target,value,oldvalue,initiator): if target.edit_time is not None: target.edit_time = 1

自此之后,更改一次,edit_time 就会加1

本文作者:Eric

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!