1. 程式人生 > >Flask - 資料庫 - 2

Flask - 資料庫 - 2

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,意思就是資料庫一旦發生改變就會發出訊號:

我們增加一個監聽訊號的函式: