Flask - 数据库 - 2

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/apollo_miracle/article/details/83042541

1 学习目标

  1. 能够按照步骤实现综合图书管理的相关案例
  2. 能够使用 Flask-Migrate 扩展对数据库进行迁移

2 综合案例-图书管理

2.1 pycharm连接数据库

新建项目,创建demo1_bookDemo.py文件

一般通过终端连接数据库,其实也可以通过pycharm连接数据库,pycharm最右侧找到Database,然后操作如下:

然后做如下配置:(第一次需要下载Driver驱动)

连接上去之后的效果:

接下来在pycharm中打开执行sql的命令行窗口,然后创建数据库:

创建之后的结果:(如果没有出现,点击刷新按钮:)

双击“booktest”数据库就相当于“use booktest;”命令

2.2 创建模型

模型表示程序使用的数据实体,在Flask-SQLAlchemy中,模型一般是Python类,继承自db.Model,db是SQLAlchemy类的实例,代表程序使用的数据库。

类中的属性对应数据库表中的列。id为主键,是由Flask-SQLAlchemy管理。db.Column类构造函数的第一个参数是数据库列和模型属性类型。

注:如果没有在创建数据库的时候指定编码的话,向数据库中插入中文后,会报错,那么需要修改数据库的编码集:

alter database 数据库名 CHARACTER SET utf8

如下示例:定义了两个模型类,作者和书名。

from flask import Flask, render_template, redirect, url_for
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 设置连接数据
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test2'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

# 实例化SQLAlchemy对象
db = SQLAlchemy(app)


# 定义模型类-作者
class Author(db.Model):
    """作者模型:1的一方"""
    __tablename__ = 'authors'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(32), unique=True)

    # 定义属性,以便作者模型可以通过该属性访问其多的一方的数据(书的数据)
    # backref 给 Book 也添加了一个 author 的属性,可以通过  book.author 获取 book 所对应的作者信息
    books = db.relationship("Book", backref="author")


# 定义模型类-书名
class Book(db.Model):
    """书的模型:多的一方"""
    __tablename__ = 'books'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    # 记录1的一方的 id 作为外键
    au_book = db.Column(db.Integer, db.ForeignKey('author.id'))

添加测试数据:

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

    # 生成数据
    au1 = Author(name='老王')
    au2 = Author(name='老尹')
    au3 = Author(name='老刘')
    # 把数据提交给用户会话
    db.session.add_all([au1, au2, au3])
    # 提交会话
    db.session.commit()
    bk1 = Book(name='老王回忆录', author_id=au1.id)
    bk2 = Book(name='我读书少,你别骗我', author_id=au1.id)
    bk3 = Book(name='如何才能让自己更骚', author_id=au2.id)
    bk4 = Book(name='怎样征服美丽少女', author_id=au3.id)
    bk5 = Book(name='如何征服英俊少男', author_id=au3.id)
    # 把数据提交给用户会话
    db.session.add_all([bk1, bk2, bk3, bk4, bk5])
    # 提交会话
    db.session.commit()

    app.run(debug=True)

运行之后,查看结果:(双击booktest,展开所有表)

双击books表,查看数据:

查看authors表数据:

2.3 作者列表

新建templates模板文件夹,变为模板文件夹,并且设置模板语言:

新建模板文件:demo1_bookDemo.html

新建视图函数:

展示作者列表:

运行:

作者下边还应该显示对应图书:

效果如下:

2.3 增加图书的表单

创建表单对应的类:

实例化并且传递到模板:

模板中使用form来处理表单:

展示效果如下:

2.4 添加数据到数据库

因为点击添加,还是提交到当前url

所以增加逻辑如下:

@app.route("/", methods=["GET", "POST"])
def index():
    """返回首页"""

    book_form = AddBookForm()

    # 如果数据可以提交(所有数据都已填好)
    if book_form.validate_on_submit():
        # 1. 提取表单中的数据
        # WTF 表单专用
        # author_name = book_form.author.data
        # book_name = book_form.book.data
        # 通用(推荐使用)
        # author_name = request.form.get("author")
        # book_name = request.form.get("book")
        # 2. 做具体业务逻辑实现代码
        # 2.1 查询指定名字的作者
        author = Author.query.filter(Author.name == author_name).first()
        # if 指定名字的作者不存在:
        if not author:
            # 添加作者信息到数据库
            # 初始化作者的模型对象
            author = Author(name=author_name)
            db.session.add(author)
            db.session.commit()

            # 添加书籍信息到数据库(指定其作者)
            book = Book(name=book_name, author_id=author.id)
            db.session.add(book)
            db.session.commit()
        else:
            book = Book.query.filter(Book.name == book_name).first()
            if not book:
                # 添加书籍信息到数据库(指定其作者)
                book = Book(name=book_name, author_id=author.id)
                db.session.add(book)
                db.session.commit()
            else:
                flash("已存在")

    else:
        if request.method == "POST":
            flash("参数错误")

    # 1.查询数据
    authors = Author.query.all()
    # 2.将数据返回到模板中进行渲染返回
    return render_template("demo1_bookDemo.html", authors=authors, form=book_form)

如果出错,设置了闪现消息,所以模板中需要显示闪现消息:

2.5 增加 try

因为数据库操作可能会失败,所以增加try逻辑:

2.6 删除作者及书籍

2.6.1 删除分析

删除其实有俩逻辑:

  1. 删除图书

  2. 删除作者,同时删除作者下所有图书

2.6.2 删除图书

增加删除图书逻辑如下:

@app.route("/delete_book/<book_id>")
def delete_book(book_id):
    """删除书籍"""
    try:
        book = Book.query.get(book_id)
    except Exception as e:
        print(e)
        return "查询错误"
    
    if not book:
        return "书籍不存在"
    
    try:
        db.session.delete(book)
        db.session.commit()
    except Exception as e:
        print(e)
        db.session.rollback()
        flash("删除失败")
        
    return redirect(url_for("index"))

模板中增加删除链接:

2.6.3 删除作者

增加删除作者逻辑如下:

@app.route("/delete_author/<author_id>")
def delete_author(author_id):
    """删除作者以及作者所有的书籍"""
    try:
        author = Author.query.get(author_id)
    except Exception as e:
        print(e)
        return "查询错误"

    if not author:
        return "没有此作者"

    # 删除作者及其所有书籍
    try:
        # 先删除书籍
        Book.query.filter(Book.author_id==author.id).delete()
        # 再删除指定作者
        db.session.delete(author)
        db.session.commit()
    except Exception as e:
        print(e)
        flash("删除错误")

    return redirect(url_for("index"))

代码说明:

Book.query.filter().delete():先拿到一个查询结果,然后直接delete,是对查询结果做整体删除

模板增加删除超链接:

2.7 全部代码实现

# demo1_bookDemo.py
from flask import Flask, render_template, request, flash, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import InputRequired

app = Flask(__name__)

# 设置连接数据
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/booktest'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

# 实例化SQLAlchemy对象
db = SQLAlchemy(app)
app.secret_key = "apollo_miracle"


# 定义模型类-作者
class Author(db.Model):
    """作者模型:1的一方"""
    __tablename__ = 'authors'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)

    # 定义属性,以便作者模型可以通过该属性访问其多的一方的数据(书的数据)
    # backref 给 Book 也添加了一个 author 的属性,可以通过  book.author 获取 book 所对应的作者信息
    books = db.relationship("Book", backref="author")


# 定义模型类-书名
class Book(db.Model):
    """书的模型:多的一方"""
    __tablename__ = 'books'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    # 记录1的一方的 id 作为外键
    author_id = db.Column(db.Integer, db.ForeignKey(Author.id))


class AddBookForm(FlaskForm):
    """自定义添加书籍的表单"""
    author = StringField("作者:", validators=[InputRequired("请输入作者姓名")])
    book = StringField("书名:", validators=[InputRequired("请输入书名")])
    submit = SubmitField("添加")


@app.route("/delete_author/<author_id>")
def delete_author(author_id):
    """删除作者以及作者所有的书籍"""
    try:
        author = Author.query.get(author_id)
    except Exception as e:
        print(e)
        return "查询错误"

    if not author:
        return "没有此作者"

    # 删除作者及其所有书籍
    try:
        # 先删除书籍
        Book.query.filter(Book.author_id == author.id).delete()
        # 再删除指定作者
        db.session.delete(author)
        db.session.commit()
    except Exception as e:
        print(e)
        flash("删除错误")

    return redirect(url_for("index"))


@app.route("/delete_book/<book_id>")
def delete_book(book_id):
    """删除书籍"""
    try:
        book = Book.query.get(book_id)
    except Exception as e:
        print(e)
        return "查询错误"

    if not book:
        return "书籍不存在"

    try:
        db.session.delete(book)
        db.session.commit()
    except Exception as e:
        print(e)
        db.session.rollback()
        flash("删除失败")

    return redirect(url_for("index"))


@app.route("/", methods=["GET", "POST"])
def index():
    """返回首页"""

    book_form = AddBookForm()

    # 如果数据可以提交(所有数据都已填好)
    if book_form.validate_on_submit():
        # 1. 提取表单中的数据
        # WTF 表单专用
        author_name = book_form.author.data
        book_name = book_form.book.data
        # 通用(推荐使用)
        # author_name = request.form.get("author")
        # book_name = request.form.get("book")
        # 2. 做具体业务逻辑实现代码
        # 2.1 查询指定名字的作者
        author = Author.query.filter(Author.name == author_name).first()
        # if 指定名字的作者不存在:
        if not author:
            try:
                # 添加作者信息到数据库
                # 初始化作者的模型对象
                author = Author(name=author_name)
                db.session.add(author)
                db.session.commit()

                # 添加书籍信息到数据库(指定其作者)
                book = Book(name=book_name, author_id=author.id)
                db.session.add(book)
                db.session.commit()
            except Exception as error:
                db.session.rollback()
                print(error)
                flash("添加失败")
        else:
            book = Book.query.filter(Book.name == book_name).first()
            if not book:
                try:
                    # 添加书籍信息到数据库(指定其作者)
                    book = Book(name=book_name, author_id=author.id)
                    db.session.add(book)
                    db.session.commit()
                except Exception as error:
                    db.session.rollback()
                    print(error)
                    flash("添加失败")
            else:
                flash("已存在")

    else:
        if request.method == "POST":
            flash("参数错误")

    # 1.查询数据
    authors = Author.query.all()
    # 2.将数据返回到模板中进行渲染返回
    return render_template("demo1_bookDemo.html", authors=authors, form=book_form)


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

    # 生成数据
    au1 = Author(name='老王')
    au2 = Author(name='老尹')
    au3 = Author(name='老刘')
    # 把数据提交给用户会话
    db.session.add_all([au1, au2, au3])
    # 提交会话
    db.session.commit()

    # 生成数据
    bk1 = Book(name='老王回忆录', author_id=au1.id)
    bk2 = Book(name='我读书少,你别骗我', author_id=au1.id)
    bk3 = Book(name='如何才能让自己更骚', author_id=au2.id)
    bk4 = Book(name='怎样征服美丽少女', author_id=au3.id)
    bk5 = Book(name='如何征服英俊少男', author_id=au3.id)
    # 把数据提交给用户会话
    db.session.add_all([bk1, bk2, bk3, bk4, bk5])
    # 提交会话
    db.session.commit()

    app.run(debug=True)
{# demo1_bookDemo.html #}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>图书管理</h1>

<form method="post">
    {{ form.csrf_token }}<br>
    {{ form.author.label }}{{ form.author }}<br>
    {{ form.book.label }}{{ form.book }}<br>
    {{ form.submit }}<br>

    {% for message in get_flashed_messages() %}
        {{ message }}
    {% endfor %}
</form>

<hr>

<ul>
    {% for author in authors %}
        <li>{{ author.name }}<a href="/delete_author/{{ author.id }}">删除</a></li>
        <ul>
            {% for book in author.books %}
                <li>{{ book.name }}<a href="/delete_book/{{ book.id }}">删除</a></li>
            {% endfor %}
        </ul>
    {% endfor %}
</ul>

</body>
</html>

3 多对多演练

在项目开发过程中,会遇到很多数据之间多对多关系的情况,比如:

  • 学生网上选课(学生和课程)
  • 老师与其授课的班级(老师和班级)
  • 用户与其收藏的新闻(用户和新闻)
  • 等等...

所以在开发过程中需要使用 ORM 模型将表与表的多对多关联关系使用代码描述出来。多对多关系描述有一个唯一的点就是:需要添加一张单独的表去记录两张表之间的对应关系

3.1 场景示例

3.1.1 需求分析

  • 学生可以网上选课,学生有多个,课程也有多个
  • 学生有:张三、李四、王五
  • 课程有:物理、化学、生物
  • 选修关系有:
    • 张三选修了化学和生物
    • 李四选修了化学
    • 王五选修了物理、化学和生物
  • 需求:
    1. 查询某个学生选修了哪些课程
    2. 查询某个课程都有哪些学生选择

3.1.2 思路分析

  • 可以通过分析得出
    • 用一张表来保存所有的学生数据
    • 用一张表来保存所有的课程数据
  • 具体表及测试数据可以如下:

学生表(Student)

主键(id) 学生名(name)
1 张三
2 李四
3 王五

选修课表(Course)

主键(id) 课程名(name)
1 物理
2 化学
3 生物

数据关联关系表(Student_Course)

主键(student.id) 主键(course.id)
1 2
1 3
2 2
3 1
3 2
3 3

3.1.3 结果

  • 查询某个学生选修了哪些课程,例如:查询王五选修了哪些课程

    • 取出王五的 id 去 Student_Course 表中查询 student.id 值为 3 的所有数据
    • 查询出来有3条数据,然后将这3条数据里面的 course.id 取值并查询 Course 表即可获得结果
  • 查询某个课程都有哪些学生选择,例如:查询生物课程都有哪些学生选修

    • 取出生物课程的 id 去 Student_Course 表中查询 course.id 值为 3 的所有数据
    • 查询出来有2条数据,然后将这2条数据里面的 student.id 取值并查询 Student 表即可获得结果

3.1.4 代码演练

  • 定义模型及表
tb_student_course = db.Table('tb_student_course',
                             db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
                             db.Column('course_id', db.Integer, db.ForeignKey('courses.id'))
                             )


class Student(db.Model):
    __tablename__ = "students"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)

    courses = db.relationship('Course', secondary=tb_student_course,
                              backref='student',
                              lazy='dynamic')


class Course(db.Model):
    __tablename__ = "courses"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
  • 添加测试数据
if __name__ == '__main__':
    db.drop_all()
    db.create_all()

    # 添加测试数据

    stu1 = Student(name='张三')
    stu2 = Student(name='李四')
    stu3 = Student(name='王五')

    cou1 = Course(name='物理')
    cou2 = Course(name='化学')
    cou3 = Course(name='生物')

    stu1.courses = [cou2, cou3]
    stu2.courses = [cou2]
    stu3.courses = [cou1, cou2, cou3]

    db.session.add_all([stu1, stu2, stu2])
    db.session.add_all([cou1, cou2, cou3])

    db.session.commit()

    app.run(debug=True)

3.2 具体操作

  • 新建数据库:manytomany

  • 新建demo2_manytomany.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 设置连接数据
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/manytomany'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

# 实例化SQLAlchemy对象
db = SQLAlchemy(app)
app.secret_key = "apollo_miracle"

tb_student_course = db.Table('tb_student_course',
                             db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
                             db.Column('course_id', db.Integer, db.ForeignKey('courses.id'))
                             )


class Student(db.Model):
    __tablename__ = "students"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)

    courses = db.relationship('Course', secondary=tb_student_course,
                              backref='student',
                              lazy='dynamic')


class Course(db.Model):
    __tablename__ = "courses"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)


@app.route("/")
def index():
    return "index"


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

    # 添加测试数据
    stu1 = Student(name='张三')
    stu2 = Student(name='李四')
    stu3 = Student(name='王五')

    cou1 = Course(name='物理')
    cou2 = Course(name='化学')
    cou3 = Course(name='生物')

    stu1.courses = [cou2, cou3]
    stu2.courses = [cou2]
    stu3.courses = [cou1, cou2, cou3]

    db.session.add_all([stu1, stu2, stu2])
    db.session.add_all([cou1, cou2, cou3])

    db.session.commit()

    app.run(debug=True)

realtionship描述了Role和User的关系:

第一个参数为对应参照的类"User"

第二个参数backref为类User申明新属性的方法 

第三个参数为二次查询

注意:

tb_Student_Course就是第三张表,记录关系的表,它不是一个Model,只是一个db.Table,也就是一张数据库表。我们在程序中是不用操作这张表的,但是在数据库中必须存在,所以写法跟模型类不一样

二次查询:secondary参数

注意修改数据库名称

3.3 二次查询

什么叫二次查询呢?

比如我要查询这个学生都选修了哪些课程,通过python代码即可实现:

stu = Student.query.filter(xx).first()

stu.courses

但是内部处理的sql语句可没那么简单:

  • 首先先查询stu : select * from student where xx;

假设查询出来的id为1

  • 然后要查询这个学生选修的课程:

  • 第一次查询:select course_id from student_course where student_id = 1

假设查询结果为 1,3,5 (代表选修课程id)

  • 第二次查询:select * from course where id in (1,3,5)
  • 查询

查询这个学生选修的课程 需要经过两次查询, 所以叫做二次查询

3.4 lazy 指定

3.4.1 学生查询课程

我们先来看一个现象:

  • 断点运行程序:

  • 然后观察表达式:

  • 如果添加lazy:

  • 同样断点调试:

  • 只有在调用了all之后才可以看到具体列表结果:

  • 分析lazy:

lazy介绍如下:

参数lazy决定了什么时候SQLALchemy从数据库中加载数据

  • 如果设置为子查询方式(subquery),则会在加载完Role对象后,就立即加载与其关联的对象,这样会让总查询数量减少,但如果返回的条目数量很多,就会比较慢

设置为 subquery 的话,role.users 返回所有数据列表

  • 另外,也可以设置为动态方式(dynamic),这样关联对象会在被使用的时候再进行加载,并且在返回前进行过滤,如果返回的对象数很多,或者未来会变得很多,那最好采用这种方式

设置为 dynamic 的话,role.users 返回查询对象,并没有做到真正的查询,可以利用查询对象做其他逻辑,比如:先排序再返回结果

  • 总结如下:lazy = "dynamic"
  1. 如果不指定该值,那么当 student 查询数据之后,courses 就已经有值(已经从Course表里面把数据查询出来了)

  2. 如果指定该值,那么当 student 查询数据之后,courses 并没有具体的值,而只是查询对象

  3. 如果只是查询对象,那么就可以在用的时候再去数据库查询,避免不必要的查询操作,影响性能

3.4.2 课程查询学生

  • 我们来反过来查询一下,发现直接得到列表, 这个能不能也懒查询呢?

  • 如下操作即可:

  • 再次查看:

4 常见关系模板代码

以下罗列了使用关系型数据库中常见关系定义模板代码

4.1 一对多

  • 示例场景:
    • 用户与其发布的帖子(用户表与帖子表)
    • 角色与所属于该角色的用户(角色表与多用户表)
  • 示例代码
class Role(db.Model):
    """角色表"""
    __tablename__ = 'roles'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    users = db.relationship('User', backref='role', lazy='dynamic')

class User(db.Model):
    """用户表"""
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True, index=True)

4.2 多对多

  • 示例场景
    • 讲师与其上课的班级(讲师表与班级表)
    • 用户与其收藏的新闻(用户表与新闻表)
    • 学生与其选修的课程(学生表与选修课程表)
  • 示例代码
tb_student_course = db.Table('tb_student_course',
                             db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
                             db.Column('course_id', db.Integer, db.ForeignKey('courses.id'))
                             )

class Student(db.Model):
    __tablename__ = "students"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)

    courses = db.relationship('Course', secondary=tb_student_course,
                              backref=db.backref('students', lazy='dynamic'),
                              lazy='dynamic')

class Course(db.Model):
    __tablename__ = "courses"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)

4.3 自关联一对多

  • 示例场景
    • 评论与该评论的子评论(评论表)
    • 参考网易新闻
  • 示例代码
class Comment(db.Model):
    """评论"""
    __tablename__ = "comments"

    id = db.Column(db.Integer, primary_key=True)
    # 评论内容
    content = db.Column(db.Text, nullable=False)
    # 父评论id
    parent_id = db.Column(db.Integer, db.ForeignKey("comments.id"))
    # 父评论(也是评论模型)
    parent = db.relationship("Comment", remote_side=[id],
                             backref=db.backref('childs', lazy='dynamic'))

# 测试代码
if __name__ == '__main__':
    db.drop_all()
    db.create_all()

    com1 = Comment(content='我是主评论1')
    com2 = Comment(content='我是主评论2')
    com11 = Comment(content='我是回复主评论1的子评论1')
    com11.parent = com1
    com12 = Comment(content='我是回复主评论1的子评论2')
    com12.parent = com1

    db.session.add_all([com1, com2, com11, com12])
    db.session.commit()
    app.run(debug=True)

网易新闻的评论:

一个主评论可能会有多个子评论 (也就是对于这个评论的回复评论)

4.4 自关联多对多

  • 示例场景

    • 用户关注其他用户(用户表,中间表)
  • 示例代码


tb_user_follows = db.Table(
    "tb_user_follows",
    db.Column('follower_id', db.Integer, db.ForeignKey('info_user.id'), primary_key=True),  # 粉丝id
    db.Column('followed_id', db.Integer, db.ForeignKey('info_user.id'), primary_key=True)  # 被关注人的id
)

class User(db.Model):
    """用户表"""
    __tablename__ = "info_user"

    id = db.Column(db.Integer, primary_key=True)  
    name = db.Column(db.String(32), unique=True, nullable=False)

    # 用户所有的粉丝,添加了反向引用followed,代表用户都关注了哪些人
    followers = db.relationship('User',
                                secondary=tb_user_follows,
                                primaryjoin=id == tb_user_follows.c.followed_id,
                                secondaryjoin=id == tb_user_follows.c.follower_id,
                                backref=db.backref('followed', lazy='dynamic'),
                                lazy='dynamic')

5 数据库迁移

5.1 简介

  • 在开发过程中,需要修改数据库模型,而且还要在修改之后更新数据库。最直接的方式就是删除旧表,但这样会丢失数据。
  • 更好的解决办法是使用数据库迁移框架,它可以追踪数据库模式的变化,然后把变动应用到数据库中。
  • 在Flask中可以使用Flask-Migrate扩展,来实现数据迁移。并且集成到Flask-Script中,所有操作通过命令就能完成。
  • 为了导出数据库迁移命令,Flask-Migrate提供了一个MigrateCommand类,可以附加到flask-script的manager对象上。

首先要在虚拟环境中安装Flask-Migrate。

pip install flask-migrate
  • 代码文件内容:
#coding=utf-8
from flask import Flask

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate,MigrateCommand
from flask_script import Shell,Manager

app = Flask(__name__)
manager = Manager(app)

app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/Flask_test'
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)

#第一个参数是Flask的实例,第二个参数是Sqlalchemy数据库实例
migrate = Migrate(app,db) 

#manager是Flask-Script的实例,这条语句在flask-Script中添加一个db命令
manager.add_command('db',MigrateCommand)

#定义模型Role
class Role(db.Model):
    # 定义表名
    __tablename__ = 'roles'
    # 定义列对象
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    user = db.relationship('User', backref='role')

    #repr()方法显示一个可读字符串,
    def __repr__(self):
        return 'Role:'.format(self.name)

#定义用户
class User(db.Model):
    __talbe__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    #设置外键
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

    def __repr__(self):
        return 'User:'.format(self.username)


if __name__ == '__main__':
    manager.run()

5.1.2 创建迁移仓库

#这个命令会创建migrations文件夹,所有迁移文件都放在里面。
python database.py db init

5.1.3 创建迁移脚本

  • 自动创建迁移脚本有两个函数
    • upgrade():函数把迁移中的改动应用到数据库中。
    • downgrade():函数则将改动删除。
  • 自动创建的迁移脚本会根据模型定义和数据库当前状态的差异,生成upgrade()和downgrade()函数的内容。
  • 对比不一定完全正确,有可能会遗漏一些细节,需要进行检查
python database.py db migrate -m 'initial migration'

5.1.4 更新数据库

python database.py db upgrade

5.1.5 返回以前的版本

可以根据history命令找到版本号,然后传给downgrade命令:

python app.py db history

输出格式:<base> ->  版本号 (head), initial migration

回滚到指定版本

python app.py db downgrade 版本号

5.2 实际操作

5.2.1 准备工作

新建项目:

新建demo:

代码如下:(准备两个模型类)

创建数据库:

5.2.2 执行数据库迁移

要想执行数据库迁移,需要增加如下代码:

接下来初始化:

初始化结果:在项目中创建了一个migrations文件夹

生成迁移文件:

结果:多了一个文件(迁移文件)

执行迁移:

结果:多出两张表

总结:

5.2.3 完善数据库迁移

增加字段

模型类中增加字段:

生成迁移文件:

迁移文件如下:

执行迁移:

结果:

再增加字段:

生成迁移文件:

执行迁移:

结果:

删除字段

删除字段:

生成迁移,并执行迁移:

结果:

修改字段

将name变为nick_name:

生成迁移文件:

迁移文件如下:

执行迁移:

效果:

降级 downgrade

如果现在我不想修改了呢?  想回退刚刚的操作怎么办?  downgrade降级:

但是这个降级的函数有问题:

修改如下:

降级:

效果:

历史版本&升级,降级指定具体版本

history查看版本:

降级可以指定具体某个版本:

效果: (fdc8fb218f90这个版本是最初的版本,所以字段也变为最初的)

升级指定版本:

效果如下:

5.3 实际操作顺序:

  • 1.python 文件 db init
  • 2.python 文件 db migrate -m"版本名(注释)"
  • 3.python 文件 db upgrade 然后观察表结构
  • 4.根据需求修改模型
  • 5.python 文件 db migrate -m"新版本名(注释)"
  • 6.python 文件 db upgrade 然后观察表结构
  • 7.若返回版本,则利用 python 文件 db history查看版本号
  • 8.python 文件 db downgrade(upgrade) 版本号

6 信号机制

6.1 Flask信号机制

  • Flask信号(signals, or event hooking)允许特定的发送端通知订阅者发生了什么(既然知道发生了什么,那我们可以根据自己业务需求实现自己的逻辑)。
  • Flask提供了一些信号(核心信号)且其它的扩展提供更多的信号。
  • 信号依赖于Blinker库。
    pip install blinker
    
  • flask内置信号列表:http://docs.jinkan.org/docs/flask/api.html#id17
    template_rendered = _signals.signal('template-rendered')
    request_started = _signals.signal('request-started')
    request_finished = _signals.signal('request-finished')
    request_tearing_down = _signals.signal('request-tearing-down')
    got_request_exception = _signals.signal('got-request-exception')
    appcontext_tearing_down = _signals.signal('appcontext-tearing-down')
    appcontext_pushed = _signals.signal('appcontext-pushed')
    appcontext_popped = _signals.signal('appcontext-popped')
    message_flashed = _signals.signal('message-flashed')
    

6.2 信号应用场景

Flask-User 这个扩展中定义了名为 user_logged_in 的信号,当用户成功登入之后,这个信号会被发送。我们可以订阅该信号去追踪登录次数和登录IP:

from flask import request
from flask_user.signals import user_logged_in

@user_logged_in.connect_via(app)
def track_logins(sender, user, **extra):
    user.login_count += 1
    user.last_login_ip = request.remote_addr
    db.session.add(user)
    db.session.commit()

6.3 Flask-SQLAlchemy 信号支持

在 Flask-SQLAlchemy 模块中,0.10 版本开始支持信号,可以连接到信号来获取到底发生什么了的通知。存在于下面两个信号:

  • models_committed
    • 这个信号在修改的模型提交到数据库时发出。发送者是发送修改的应用,模型 和 操作描述符 以 (model, operation) 形式作为元组,这样的元组列表传递给接受者的 changes 参数。
    • 该模型是发送到数据库的模型实例,当一个模型已经插入,操作是 'insert' ,而已删除是 'delete' ,如果更新了任何列,会是 'update' 。
  • before_models_committed
    • 除了刚好在提交发送前发生,与 models_committed 完全相同。
from flask_sqlalchemy import models_committed

# 给 models_committed 信号添加一个订阅者,即为当前 app
@models_committed.connect_via(app)
def models_committed(a, changes):
    print(a, changes)

对数据库进行增删改进行测试

6.4 具体演示

将追踪修改的这个配置改为true,意思就是数据库一旦发生改变就会发出信号:

我们增加一个监听信号的函数:

运行,发现打印的结果是,增加数据:

这是哪里发出的这个信号的呢? 如下:

猜你喜欢

转载自blog.csdn.net/apollo_miracle/article/details/83042541