Flask - 資料庫 - 2
1 學習目標
- 能夠按照步驟實現綜合圖書管理的相關案例
- 能夠使用 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 需求分析
- 學生可以網上選課,學生有多個,課程也有多個
- 學生有:張三、李四、王五
- 課程有:物理、化學、生物
- 選修關係有:
- 張三選修了化學和生物
- 李四選修了化學
- 王五選修了物理、化學和生物
- 需求:
- 查詢某個學生選修了哪些課程
- 查詢某個課程都有哪些學生選擇
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"
如果不指定該值,那麼當 student 查詢資料之後,courses 就已經有值(已經從Course表裡面把資料查詢出來了)
如果指定該值,那麼當 student 查詢資料之後,courses 並沒有具體的值,而只是查詢物件
如果只是查詢物件,那麼就可以在用的時候再去資料庫查詢,避免不必要的查詢操作,影響效能
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,意思就是資料庫一旦發生改變就會發出訊號:
我們增加一個監聽訊號的函式: