1. 程式人生 > 實用技巧 >05.flask資料庫

05.flask資料庫

配套視訊教程

本文B站配套視訊教程

配套視訊教程

本文B站配套視訊教程

使用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 那樣,定義為 RoleUser 模型。

示例 2 hello.py:定義 RoleUser 模型

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 intlong 不限制精度的整數
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 命令,通過遷移框架重新建立資料庫。