05.flask資料庫
配套視訊教程
配套視訊教程
使用Flask-SQLAlchemy管理資料庫
Flask-SQLAlchemy 是一個 Flask 擴充套件,簡化了在 Flask 應用中使用 SQLAlchemy 的操作。SQLAlchemy 是一個強大的關係型資料庫框架,支援多種資料庫後臺。SQLAlchemy 提供了高層 ORM,也提供了使用資料庫原生 SQL 的低層功能。
與其他多數擴充套件一樣,Flask-SQLAlchemy 也使用 pip 安裝:
(venv) $ pip install flask-sqlalchemy
在 Flask-SQLAlchemy 中,資料庫使用 URL 指定。幾種最流行的資料庫引擎使用的 URL 格式如表 1 所示。
表1:FLask-SQLAlchemy資料庫URL
資料庫引擎 | URL |
---|---|
MySQL | mysql://username:password@hostname/database |
Postgres | postgresql://username:password@hostname/database |
SQLite(Linux,macOS) | sqlite:////absolute/path/to/database |
SQLite(Windows) | sqlite:///c:/absolute/path/to/database |
在這些 URL 中,hostname 表示資料庫服務所在的主機,可以是本地主機(localhost),也可以是遠端伺服器。資料庫伺服器上可以託管多個數據庫,因此 database 表示要使用的資料庫名。如果資料庫需要驗證身份,使用 username 和 password 提供資料庫使用者的憑據。
SQLite 資料庫沒有伺服器,因此不用指定 hostname、username 和 password。URL 中的 database 是磁碟中的檔名。
應用使用的資料庫 URL 必須儲存到 Flask 配置物件的 SQLALCHEMY_DATABASE_URI
鍵中。Flask-SQLAlchemy 文件還建議把 SQLALCHEMY_TRACK_MODIFICATIONS
鍵設為 False
,以便在不需要跟蹤物件變化時降低記憶體消耗。其他配置選項的作用參閱 Flask-SQLAlchemy 的文件。示例 1 展示如何初始化及配置一個簡單的 SQLite 資料庫。
示例 1 hello.py:配置資料庫
import os
from flask_sqlalchemy import SQLAlchemy
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
db
物件是 SQLAlchemy
類的例項,表示應用使用的資料庫,通過它可獲得 Flask-SQLAlchemy 提供的所有功能。
定義模型
模型這個術語表示應用使用的持久化實體。在 ORM 中,模型一般是一個 Python 類,類中的屬性對應於資料庫表中的列。
Flask-SQLAlchemy 建立的資料庫例項為模型提供了一個基類以及一系列輔助類和輔助函式,可用於定義模型的結構。圖中的 roles
表和 users
表可像示例 2 那樣,定義為 Role
和 User
模型。
示例 2 hello.py:定義
Role
和User
模型
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
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
def __repr__(self):
return '<User %r>' % self.username
類變數 __tablename__
定義在資料庫中使用的表名。如果沒有定義 __tablename__
,Flask-SQLAlchemy 會使用一個預設名稱,但預設的表名沒有遵守流行的使用複數命名的約定,所以最好由我們自己來指定表名。其餘的類變數都是該模型的屬性,定義為 db.Column
類的例項。
db.Column
類建構函式的第一個引數是資料庫列和模型屬性的型別。表 2 列出了一些可用的列型別以及在模型中使用的 Python 型別。
表2:最常用的SQLAlchemy列型別
型別名 | 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 |
db.Column
的其餘引數指定屬性的配置選項。表 3 列出了一些可用選項。
表3:最常用的SQLAlchemy列選項
選項名 | 說明 |
---|---|
primary_key |
如果設為 True ,列為表的主鍵 |
unique |
如果設為 True ,列不允許出現重複的值 |
index |
如果設為 True ,為列建立索引,提升查詢效率 |
nullable |
如果設為 True ,列允許使用空值;如果設為 False ,列不允許使用空值 |
default |
為列定義預設值 |
Flask-SQLAlchemy 要求每個模型都定義主鍵,這一列經常命名為
id
。
雖然沒有強制要求,但這兩個模型都定義了 __repr()__
方法,返回一個具有可讀性的字串表示模型,供除錯和測試時使用。
關係
關係型資料庫使用關係把不同表中的行聯絡起來。上圖所示的關係圖表示使用者和角色之間的一種簡單關係。這是角色到使用者的一對多關係,因為一個角色可屬於多個使用者,而每個使用者都只能有一個角色。
圖中的一對多關係在模型類中的表示方法如示例3 所示。
示例3 hello.py:在資料庫模型中定義關係
class Role(db.Model):
# ...
users = db.relationship('User', backref='role')
class User(db.Model):
# ...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
如上圖所示,關係使用 users
表中的外來鍵連線兩行。新增到 User
模型中的 role_id
列被定義為外來鍵,就是這個外來鍵建立起了關係。傳給 db.ForeignKey()
的引數 'roles.id'
表明,這列的值是 roles
表中相應行的 id
值。
從“一”那一端可見,新增到 Role
模型中的 users
屬性代表這個關係的面向物件視角。對於一個 Role
類的例項,其 users
屬性將返回與角色相關聯的使用者組成的列表(即“多”那一端)。db.relationship()
的第一個引數表明這個關係的另一端是哪個模型。如果關聯的模型類在模組後面定義,可使用字串形式指定。
db.relationship()
中的 backref
引數向 User
模型中新增一個 role
屬性,從而定義反向關係。通過 User
例項的這個屬性可以獲取對應的 Role
模型物件,而不用再通過 role_id
外來鍵獲取。
多數情況下,db.relationship()
都能自行找到關係中的外來鍵,但有時卻無法確定哪一列是外來鍵。例如,如果 User 模型中有兩個或以上的列定義為 Role
模型的外來鍵,SQLAlchemy 就不知道該使用哪一列。如果無法確定外來鍵,就要為 db.relationship()
提供額外的引數。表 4 列出了定義關係時常用的配置選項。
表4:常用的SQLAlchemy關係選項
選項名 | 說明 |
---|---|
backref |
在關係的另一個模型中新增反向引用 |
primaryjoin |
明確指定兩個模型之間使用的聯結條件;只在模稜兩可的關係中需要指定 |
lazy |
指定如何載入相關記錄,可選值有 select (首次訪問時按需載入)、immediate (源物件載入後就載入)、joined (載入記錄,但使用聯結)、subquery (立即載入,但使用子查詢),noload (永不載入)和 dynamic (不載入記錄,但提供載入記錄的查詢) |
uselist |
如果設為 False ,不使用列表,而使用標量值 |
order_by |
指定關係中記錄的排序方式 |
secondary |
指定多對多關係中關聯表的名稱 |
secondaryjoin |
SQLAlchemy 無法自行決定時,指定多對多關係中的二級聯結條件 |
資料庫操作
現在模型已經按照上圖所示的資料庫關係圖完成配置,可以隨時使用了。學習使用模型的最好方法是在 Python shell 中實際操作。接下來的幾節將介紹最常用的資料庫操作。shell 使用 flask shell
命令啟動。不過在執行這個命令之前,要把 FLASK_APP
環境變數設為 hello.py
。
建立表
首先,要讓 Flask-SQLAlchemy 根據模型類建立資料庫。db.create_all()
函式將尋找所有 db.Model
的子類,然後在資料庫中建立對應的表:
(venv) $ flask shell
>>> from hello import db
>>> db.create_all()
現在檢視應用目錄,你會發現有個名為 data.sqlite 的檔案,檔名與配置中指定的一樣。如果資料庫表已經存在於資料庫中,那麼 db.create_all()
不會重新建立或者更新相應的表。如果修改模型後要把改動應用到現有的資料庫中,這一行為會帶來不便。更新現有資料庫表的蠻力方式是先刪除舊錶再重新建立:
>>> db.drop_all()
>>> db.create_all()
遺憾的是,這個方法有個我們不想看到的副作用,它把資料庫中原有的資料都銷燬了。本章末尾將介紹一種更好的資料庫更新方式。
插入行
下面這段程式碼建立一些角色和使用者:
>>> from hello import Role, User
>>> admin_role = Role(name='Admin')
>>> mod_role = Role(name='Moderator')
>>> user_role = Role(name='User')
>>> user_john = User(username='john', role=admin_role)
>>> user_susan = User(username='susan', role=user_role)
>>> user_david = User(username='david', role=user_role)
模型的建構函式接受的引數是使用關鍵字引數指定的模型屬性初始值。注意,role
屬性也可使用,雖然它不是真正的資料庫列,但卻是一對多關係的高階表示。新建物件時沒有明確設定 id
屬性,因為在多數資料庫中主鍵由資料庫自身管理。現在這些物件只存在於 Python 中,還未寫入資料庫。因此,id
尚未賦值:
>>> print(admin_role.id)
None
>>> print(mod_role.id)
None
>>> print(user_role.id)
None
對資料庫的改動通過資料庫會話管理,在 Flask-SQLAlchemy 中,會話由 db.session
表示。準備把物件寫入資料庫之前,要先將其新增到會話中:
>>> db.session.add(admin_role)
>>> db.session.add(mod_role)
>>> db.session.add(user_role)
>>> db.session.add(user_john)
>>> db.session.add(user_susan)
>>> db.session.add(user_david)
或者簡寫成:
>>> db.session.add_all([admin_role, mod_role, user_role,
... user_john, user_susan, user_david])
為了把物件寫入資料庫,我們要呼叫 commit()
方法提交會話:
>>> db.session.commit()
提交資料後再檢視 id
屬性,現在它們已經賦值了:
>>> print(admin_role.id)
1
>>> print(mod_role.id)
2
>>> print(user_role.id)
3
資料庫會話能保證資料庫的一致性。提交操作使用原子方式把會話中的物件全部寫入資料庫。如果在寫入會話的過程中發生了錯誤,那麼整個會話都會失效。如果你始終把相關改動放在會話中提交,就能避免因部分更新導致的資料庫不一致。
修改行
在資料庫會話上呼叫 add()
方法也能更新模型。我們繼續在之前的 shell 會話中進行操作,下面這個例子把 "Admin"
角色重新命名為 "Administrator"
:
>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
刪除行
資料庫會話還有個 delete()
方法。下面這個例子把 "Moderator"
角色從資料庫中刪除:
>>> db.session.delete(mod_role)
>>> db.session.commit()
注意,刪除與插入和更新一樣,提交資料庫會話後才會執行。
查詢行
Flask-SQLAlchemy 為每個模型類都提供了 query
物件。最基本的模型查詢是使用 all()
方法取回對應表中的所有記錄:
>>> Role.query.all()
[<Role 'Administrator'>, <Role 'User'>]
>>> User.query.all()
[<User 'john'>, <User 'susan'>, <User 'david'>]
使用過濾器可以配置 query
物件進行更精確的資料庫查詢。下面這個例子查詢角色為 "User"
的所有使用者:
>>> User.query.filter_by(role=user_role).all()
[<User 'susan'>, <User 'david'>]
若想檢視 SQLAlchemy 為查詢生成的原生 SQL 查詢語句,只需把 query
物件轉換成字串:
>>> str(User.query.filter_by(role=user_role))
'SELECT users.id AS users_id, users.username AS users_username,
users.role_id AS users_role_id \nFROM users \nWHERE :param_1 = users.role_id'
如果你退出了 shell 會話,前面這些例子中建立的物件就不會以 Python 物件的形式存在,但在資料庫表中仍有對應的行。如果開啟一個新的 shell 會話,要從資料庫中讀取行,重新建立 Python 物件。下面這個例子發起一個查詢,載入名為 "User"
的使用者角色:
>>> user_role = Role.query.filter_by(name='User').first()
注意,這裡發起查詢的不是 all()
方法,而是 first()
方法。all()
方法返回所有結果構成的列表,而 first()
方法只返回第一個結果,如果沒有結果的話,則返回 None
。因此,如果知道查詢最多返回一個結果,就可以用這個方法。
filter_by()
等過濾器在 query
物件上呼叫,返回一個更精確的 query
物件。多個過濾器可以一起呼叫,直到獲得所需結果。
表 5 列出了可在 query
物件上呼叫的常用過濾器。完整的列表參見 SQLAlchemy 文件(http://docs.sqlalchemy.org)。
表5:常用的SQLAlchemy查詢過濾器
過濾器 | 說明 |
---|---|
filter() |
把過濾器新增到原查詢上,返回一個新查詢 |
filter_by() |
把等值過濾器新增到原查詢上,返回一個新查詢 |
limit() |
使用指定的值限制原查詢返回的結果數量,返回一個新查詢 |
offset() |
偏移原查詢返回的結果,返回一個新查詢 |
order_by() |
根據指定條件對原查詢結果進行排序,返回一個新查詢 |
group_by() |
根據指定條件對原查詢結果進行分組,返回一個新查詢 |
在查詢上應用指定的過濾器後,呼叫 all()
方法將執行查詢,以列表的形式返回結果。除了 all()
方法之外,還有其他方法能觸發查詢執行。表6 列出了執行查詢的其他方法。
表6:最常用的SQLAlchemy查詢執行方法
方法 | 說明 |
---|---|
all() |
以列表形式返回查詢的所有結果 |
first() |
返回查詢的第一個結果,如果沒有結果,則返回 None |
first_or_404() |
返回查詢的第一個結果,如果沒有結果,則終止請求,返回 404 錯誤響應 |
get() |
返回指定主鍵對應的行,如果沒有對應的行,則返回 None |
get_or_404() |
返回指定主鍵對應的行,如果沒找到指定的主鍵,則終止請求,返回 404 錯誤響應 |
count() |
返回查詢結果的數量 |
paginate() |
返回一個 Paginate 物件,包含指定範圍內的結果 |
關係與查詢的處理方式類似。下面這個例子分別從關係的兩端查詢角色和使用者之間的一對多關係:
>>> users = user_role.users
>>> users
[<User 'susan'>, <User 'david'>]
>>> users[0].role
<Role 'User'>
這個例子中的 user_role.users
查詢有個小問題。執行 user_role.users
表示式時,隱式的查詢會呼叫 all()
方法,返回一個使用者列表。此時,query
物件是隱藏的,無法指定更精確的查詢過濾器。就這個示例而言,返回一個按照字母順序排列的使用者列表可能更好。在示例4 中,我們修改了關係的設定,加入了 lazy='dynamic'
引數,從而禁止自動執行查詢。
示例4 hello.py:動態資料庫關係
class Role(db.Model):
# ...
users = db.relationship('User', backref='role', lazy='dynamic')
# ...
這樣配置關係之後,user_role.users
將返回一個尚未執行的查詢,因此可以在其上新增過濾器:
>>> user_role.users.order_by(User.username).all()
[<User 'david'>, <User 'susan'>]
>>> user_role.users.count()
2
在檢視函式中操作資料庫
前一節介紹的資料庫操作可以直接在檢視函式中進行。示例 5 是首頁路由的新版本,把使用者輸入的名字記錄到資料庫中。
示例 5 hello.py:在檢視函式中操作資料庫
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)
db.session.add(user)
db.session.commit()
session['known'] = False
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
return redirect(url_for('index'))
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False))
在這個修改後的版本中,提交表單後,應用會使用 filter_by()
查詢過濾器在資料庫中查詢提交的名字。變數 known
被寫入使用者會話中,因此重定向之後,可以把資料傳給模板,用於顯示自定義的歡迎訊息。注意,為了讓應用正常執行,必須按照前面介紹的方法,在 Python shell 中建立資料庫表。
對應的模板新版本如示例 6 所示。這個模板使用 known
引數在歡迎訊息中加入了第二行,從而對已知使用者和新使用者顯示不同的內容。
示例 6 templates/index.html:在模板中定製歡迎訊息
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
{% if not known %}
<p>Pleased to meet you!</p>
{% else %}
<p>Happy to see you again!</p>
{% endif %}
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
整合Python shell
每次啟動 shell 會話都要匯入資料庫例項和模型,這真是份枯燥的工作。為了避免一直重複匯入,我們可以做些配置,讓 flask shell
命令自動匯入這些物件。
若想把物件新增到匯入列表中,必須使用 app.shell_context_processor
裝飾器建立並註冊一個 shell 上下文處理器,如示例7 所示。
示例 7 hello.py:新增一個 shell 上下文
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
這個 shell 上下文處理器函式返回一個字典,包含資料庫例項和模型。除了預設匯入的 app
之外,flask shell
命令將自動把這些物件匯入 shell。
$ flask shell
>>> app
<Flask 'hello'>
>>> db
<SQLAlchemy engine='sqlite:////home/flask/flasky/data.sqlite'>
>>> User
<class 'hello.User'>
使用Flask-Migrate實現資料庫遷移
在開發應用的過程中,你會發現有時需要修改資料庫模型,而且修改之後還要更新資料庫。僅當資料庫表不存在時,Flask-SQLAlchemy 才會根據模型建立。因此,更新表的唯一方式就是先刪除舊錶,但是這樣做會丟失資料庫中的全部資料。
更新表更好的方法是使用資料庫遷移框架。原始碼版本控制工具可以跟蹤原始碼檔案的變化;類似地,資料庫遷移框架能跟蹤資料庫模式的變化,然後以增量的方式把變化應用到資料庫中。
SQLAlchemy 的開發人員編寫了一個遷移框架,名為 Alembic。除了直接使用 Alembic 之外,Flask 應用還可使用 Flask-Migrate 擴充套件。這個擴充套件是對 Alembic 的輕量級包裝,並與 flask
命令做了整合。
建立遷移倉庫
首先,要在虛擬環境中安裝 Flask-Migrate:
(venv) $ pip install flask-migrate
這個擴充套件的初始化方法如示例 8 所示。
示例 8 hello.py:初始化 Flask-Migrate
from flask_migrate import Migrate
# ...
migrate = Migrate(app, db)
為了開放資料庫遷移相關的命令,Flask-Migrate 添加了 flask db
命令和幾個子命令。在新專案中可以使用 init
子命令新增資料庫遷移支援:
(venv) $ flask db init
Creating directory /home/flask/flasky/migrations...done
Creating directory /home/flask/flasky/migrations/versions...done
Generating /home/flask/flasky/migrations/alembic.ini...done
Generating /home/flask/flasky/migrations/env.py...done
Generating /home/flask/flasky/migrations/env.pyc...done
Generating /home/flask/flasky/migrations/README...done
Generating /home/flask/flasky/migrations/script.py.mako...done
Please edit configuration/connection/logging settings in
'/home/flask/flasky/migrations/alembic.ini' before proceeding.
這個命令會建立 migrations 目錄,所有遷移指令碼都存放在這裡。
建立遷移指令碼
在 Alembic 中,資料庫遷移用遷移指令碼表示。指令碼中有兩個函式,分別是 upgrade()
和 downgrade()
。upgrade()
函式把遷移中的改動應用到資料庫中,downgrade()
函式則將改動刪除。Alembic 具有新增和刪除改動的能力,意味著資料庫可重設到修改歷史的任意一點。
使用 Flask-Migrate 管理資料庫模式變化的步驟如下。
(1) 對模型類做必要的修改。
(2) 執行 flask db migrate
命令,自動建立一個遷移指令碼。
(3) 檢查自動生成的指令碼,根據對模型的實際改動進行調整。
(4) 把遷移指令碼納入版本控制。
(5) 執行 flask db upgrade
命令,把遷移應用到資料庫中。
flask db migrate
子命令用於自動建立遷移指令碼:
(venv) $ flask db migrate -m "initial migration"
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate] Detected added table 'roles'
INFO [alembic.autogenerate] Detected added table 'users'
INFO [alembic.autogenerate.compare] Detected added index
'ix_users_username' on '['username']'
Generating /home/flask/flasky/migrations/versions/1bc
594146bb5_initial_migration.py...done
更新資料庫
檢查並修正好遷移指令碼之後,執行 flask db upgrade
命令,把遷移應用到資料庫中:
(venv) $ flask db upgrade
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.migration] Running upgrade None -> 1bc594146bb5, initial migration
對第一個遷移來說,其作用與呼叫 db.create_all()
方法一樣。但在後續的遷移中,flask db upgrade
命令能把改動應用到資料庫中,且不影響其中儲存的資料。
如果你按照之前的說明操作過,那麼已經使用
db.create_all()
函式建立了資料庫檔案。此時,flask db upgrade
命令將失敗,因為它試圖建立已經存在的資料庫表。一種簡單的處理方法是,把 data.sqlite 資料庫檔案刪掉,然後執行flask db upgrade
命令,通過遷移框架重新建立資料庫。