05.flask數據庫


配套視頻教程

本文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 類,類中的屬性對應於數據庫表中的列。
image.png

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 命令,通過遷移框架重新創建數據庫。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM