flask學習:數據庫

Flask中的數據庫
Flask本身不支持數據庫,相信你已經聽說過了。 正如表單那樣,這也是Flask有意爲之。對使用的數據庫插件自由選擇,豈不是比被迫適應其中之一,更讓人擁有主動權嗎?

絕大多數的數據庫都提供了Python客戶端包,它們之中的大部分都被封裝成Flask插件以便更好地和Flask應用結合。數據庫被劃分爲兩大類,遵循關係模型的一類是關係數據庫,另外的則是非關係數據庫,簡稱NoSQL,表現在它們不支持流行的關係查詢語言SQL(譯者注:部分人也宣稱NoSQL代表不僅僅只是SQL)。雖然兩類數據庫都是偉大的產品,但我認爲關係數據庫更適合具有結構化數據的應用程序,例如用戶列表,用戶動態等,而NoSQL數據庫往往更適合非結構化數據。 本應用可以像大多數其他應用一樣,使用任何一種類型的數據庫來實現,但是出於上述原因,我將使用關係數據庫。

在第三章中,我向你展示了第一個Flask擴展,在本章中,我還要用到兩個。 第一個是Flask-SQLAlchemy,這個插件爲流行的SQLAlchemy包做了一層封裝以便在Flask中調用更方便,類似SQLAlchemy這樣的包叫做Object Relational Mapper,簡稱ORM。 ORM允許應用程序使用高級實體(如類,對象和方法)而不是表和SQL來管理數據庫。 ORM的工作就是將高級操作轉換成數據庫命令。

SQLAlchemy不只是某一款數據庫軟件的ORM,而是支持包含MySQL、PostgreSQL和SQLite在內的很多數據庫軟件。簡直是太強大了,你可以在開發的時候使用簡單易用且無需另起服務的SQLite,需要部署應用到生產服務器上時,則選用更健壯的MySQL或PostgreSQL服務,並且不需要修改應用代碼(譯者注:只需修改應用配置)。

確認激活虛擬環境之後,利用如下命令來安裝Flask-SQLAlchemy插件:

(venv) $ pip install flask-sqlalchemy

數據庫遷移
我所見過的絕大多數數據庫教程都是關於如何創建和使用數據庫的,卻沒有指出當需要對現有數據庫更新或者添加表結構時,應當如何應對。 這是一項困難的工作,因爲關係數據庫是以結構化數據爲中心的,所以當結構發生變化時,數據庫中的已有數據需要被遷移到修改後的結構中。

我將在本章中介紹的第二個插件是Flask-Migrate。 這個插件是Alembic的一個Flask封裝,是SQLAlchemy的一個數據庫遷移框架。 使用數據庫遷移增加了啓動數據庫時候的一些工作,但這對將來的數據庫結構穩健變更來說,是一個很小的代價。

安裝Flask-Migrate和安裝你見過的其他插件的方式一樣:

(venv) $ pip install flask-migrate

Flask-SQLAlchemy配置
開發階段,我會使用SQLite數據庫,SQLite數據庫是開發小型乃至中型應用最方便的選擇,因爲每個數據庫都存儲在磁盤上的單個文件中,並且不需要像MySQL和PostgreSQL那樣運行數據庫服務。

讓我們給配置文件添加兩個新的配置項:

import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config(object):
    # ...
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

Flask-SQLAlchemy插件從SQLALCHEMY_DATABASE_URI配置變量中獲取應用的數據庫的位置。 當回顧第三章可以發現,首先從環境變量獲取配置變量,未獲取到就使用默認值,這樣做是一個好習慣。 本處,我從DATABASE_URL環境變量中獲取數據庫URL,如果沒有定義,我將其配置爲basedir變量表示的應用頂級目錄下的一個名爲app.db的文件路徑。

SQLALCHEMY_TRACK_MODIFICATIONS配置項用於設置數據發生變更之後是否發送信號給應用,我不需要這項功能,因此將其設置爲False。

數據庫在應用的表現形式是一個數據庫實例,數據庫遷移引擎同樣如此。它們將會在應用實例化之後進行實例化和註冊操作。app/init.py文件變更如下:

from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)

from app import routes, models

在這個初始化腳本中我更改了三處。首先,我添加了一個db對象來表示數據庫。然後,我又添加了數據庫遷移引擎migrate。這種註冊Flask插件的模式希望你瞭然於胸,因爲大多數Flask插件都是這樣初始化的。最後,我在底部導入了一個名爲models的模塊,這個模塊將會用來定義數據庫結構。

數據庫模型
定義數據庫中一張表及其字段的類,通常叫做數據模型。ORM(SQLAlchemy)會將類的實例關聯到數據庫表中的數據行,並翻譯相關操作。

就讓我們從用戶模型開始吧,利用 WWW SQL Designer工具,我畫了一張圖來設計用戶表的各個字段(譯者注:實際表名爲user):

在這裏插入圖片描述 用戶表

id字段通常存在於所有模型並用作主鍵。每個用戶都會被數據庫分配一個id值,並存儲到這個字段中。大多數情況下,主鍵都是數據庫自動賦值的,我只需要提供id字段作爲主鍵即可。

username,email和password_hash字段被定義爲字符串(數據庫術語中的VARCHAR),並指定其最大長度,以便數據庫可以優化空間使用率。 username和email字段的用途不言而喻,password_hash字段值得提一下。 我想確保我正在構建的應用採用安全最佳實踐,因此我不會將用戶密碼明文存儲在數據庫中。 明文存儲密碼的問題是,如果數據庫被攻破,攻擊者就會獲得密碼,這對用戶隱私來說可能是毀滅性的。 如果使用哈希密碼,這就大大提高了安全性。 這將是另一章的主題,所以現在不需分心。

用戶表構思完畢之後,我將其用代碼實現,並存儲到新建的模塊app/models.py中,代碼如下:

from app import db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))

    def __repr__(self):
        return '<User {}>'.format(self.username)    

上面創建的User類繼承自db.Model,它是Flask-SQLAlchemy中所有模型的基類。 這個類將表的字段定義爲類屬性,字段被創建爲db.Column類的實例,它傳入字段類型以及其他可選參數,例如,可選參數中允許指示哪些字段是唯一的並且是可索引的,這對高效的數據檢索十分重要。

該類的__repr__方法用於在調試時打印用戶實例。在下面的Python交互式會話中你可以看到__repr__()方法的運行情況:

>>> from app.models import User
>>> u = User(username='susan', email='[email protected]')
>>> u
<User susan>

創建數據庫遷移存儲庫
上一節中創建的模型類定義了此應用程序的初始數據庫結構(元數據)。 但隨着應用的不斷增長,很可能會新增、修改或刪除數據庫結構。 Alembic(Flask-Migrate使用的遷移框架)將以一種不需要重新創建數據庫的方式進行數據庫結構的變更。

這是一個看起來相當艱鉅的任務,爲了實現它,Alembic維護一個數據庫遷移存儲庫,它是一個存儲遷移腳本的目錄。 每當對數據庫結構進行更改後,都需要向存儲庫中添加一個包含更改的詳細信息的遷移腳本。 當應用這些遷移腳本到數據庫時,它們將按照創建的順序執行。

Flask-Migrate通過flask命令暴露來它的子命令。 你已經看過flask run,這是一個Flask本身的子命令。 Flask-Migrate添加了flask db子命令來管理與數據庫遷移相關的所有事情。 那麼讓我們通過運行flask db init來創建microblog的遷移存儲庫:

(venv) $ flask db init
  Creating directory /home/miguel/microblog/migrations ... done
  Creating directory /home/miguel/microblog/migrations/versions ... done
  Generating /home/miguel/microblog/migrations/alembic.ini ... done
  Generating /home/miguel/microblog/migrations/env.py ... done
  Generating /home/miguel/microblog/migrations/README ... done
  Generating /home/miguel/microblog/migrations/script.py.mako ... done
  Please edit configuration/connection/logging settings in
  '/home/miguel/microblog/migrations/alembic.ini' before proceeding.

請記住,flask命令依賴於FLASK_APP環境變量來知道Flask應用入口在哪裏。 對於本應用,正如第一章,你需要設置FLASK_APP = microblog.py。

運行遷移初始化命令之後,你會發現一個名爲migrations的新目錄。該目錄中包含一個名爲versions的子目錄以及若干文件。從現在起,這些文件就是你項目的一部分了,應該添加到代碼版本管理中去。

第一次數據庫遷移
包含映射到User數據庫模型的用戶表的遷移存儲庫生成後,是時候創建第一次數據庫遷移了。 有兩種方法來創建數據庫遷移:手動或自動。 要自動生成遷移,Alembic會將數據庫模型定義的數據庫模式與數據庫中當前使用的實際數據庫模式進行比較。 然後,使用必要的更改來填充遷移腳本,以使數據庫模式與應用程序模型匹配。 當前情況是,由於之前沒有數據庫,自動遷移將把整個User模型添加到遷移腳本中。 flask db migrate子命令生成這些自動遷移:

(venv) $ flask db migrate -m "users table"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'user'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']'
  Generating /home/miguel/microblog/migrations/versions/e517276bb1c2_users_table.py ... done

通過命令輸出,你可以瞭解到Alembic在創建遷移的過程中執行了哪些邏輯。前兩行是常規信息,通常可以忽略。 之後的輸出表明檢測到了一個用戶表和兩個索引。 然後它會告訴你遷移腳本的輸出路徑。 e517276bb1c2是自動生成的一個用於遷移的唯一標識(你運行的結果會有所不同)。 -m可選參數爲遷移添加了一個簡短的註釋。

生成的遷移腳本現在是你項目的一部分了,需要將其合併到源代碼管理中。 如果你好奇,並檢查了它的代碼,就會發現它有兩個函數叫upgrade()和downgrade()。 upgrade()函數應用遷移,downgrade()函數回滾遷移。 Alembic通過使用降級方法可以將數據庫遷移到歷史中的任何點,甚至遷移到較舊的版本。

flask db migrate命令不會對數據庫進行任何更改,只會生成遷移腳本。 要將更改應用到數據庫,必須使用flask db upgrade命令。

(venv) $ flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> e517276bb1c2, users table

因爲本應用使用SQLite,所以upgrade命令檢測到數據庫不存在時,會創建它(在這個命令完成之後,你會注意到一個名爲app.db的文件,即SQLite數據庫)。 在使用類似MySQL和PostgreSQL的數據庫服務時,必須在運行upgrade之前在數據庫服務器上創建數據庫。

數據庫升級和降級流程
目前,本應用還處於初期階段,但討論一下未來的數據庫遷移戰略也無傷大雅。 假設你的開發計算機上存有應用的源代碼,並且還將其部署到生產服務器上,運行應用並上線提供服務。

而應用在下一個版本必須對模型進行更改,例如需要添加一個新表。 如果沒有遷移機制,這將需要做許多工作。無論是在你的開發機器上,還是在你的服務器上,都需要弄清楚如何變更你的數據庫結構才能完成這項任務。

通過數據庫遷移機制的支持,在你修改應用中的模型之後,將生成一個新的遷移腳本(flask db migrate),你可能會審查它以確保自動生成的正確性,然後將更改應用到你的開發數據庫(flask db upgrade)。 測試無誤後,將遷移腳本添加到源代碼管理並提交。

當準備將新版本的應用發佈到生產服務器時,你只需要獲取包含新增遷移腳本的更新版本的應用,然後運行flask db upgrade即可。 Alembic將檢測到生產數據庫未更新到最新版本,並運行在上一版本之後創建的所有新增遷移腳本。

正如我前面提到的,flask db downgrade命令可以回滾上次的遷移。 雖然在生產系統上不太可能需要此選項,但在開發過程中可能會發現它非常有用。 你可能已經生成了一個遷移腳本並將其應用,只是發現所做的更改並不完全是你所需要的。 在這種情況下,可以降級數據庫,刪除遷移腳本,然後生成一個新的來替換它。

數據庫關係
關係數據庫擅長存儲數據項之間的關係。 考慮用戶發表動態的情況, 用戶將在user表中有一個記錄,並且這條用戶動態將在post表中有一個記錄。 標記誰寫了一個給定的動態的最有效的方法是鏈接兩個相關的記錄。

一旦建立了用戶和動態之間的關係,數據庫就可以在查詢中展示它。最小的例子就是當你看一條用戶動態的時候需要知道是誰寫的。一個更復雜的查詢是, 如果你好奇一個用戶時,你可能想知道這個用戶寫的所有動態。 Flask-SQLAlchemy有助於實現這兩種查詢。

讓我們擴展數據庫來存儲用戶動態,以查看實際中的關係。 這是一個新表post的設計(譯者注:實際表名分別爲user和post):
在這裏插入圖片描述
post表將具有必須的id、用戶動態的body和timestamp字段。 除了這些預期的字段之外,我還添加了一個user_id字段,將該用戶動態鏈接到其作者。 你已經看到所有用戶都有一個唯一的id主鍵, 將用戶動態鏈接到其作者的方法是添加對用戶id的引用,這正是user_id字段所在的位置。 這個user_id字段被稱爲外鍵。 上面的數據庫圖顯示了外鍵作爲該字段和它引用的表的id字段之間的鏈接。 這種關係被稱爲一對多,因爲“一個”用戶寫了“多”條動態。

修改後的app/models.py如下:

from datetime import datetime
from app import db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))
    posts = db.relationship('Post', backref='author', lazy='dynamic')

    def __repr__(self):
        return '<User {}>'.format(self.username)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.String(140))
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))

    def __repr__(self):
        return '<Post {}>'.format(self.body)

新的“Post”類表示用戶發表的動態。 timestamp字段將被編入索引,如果你想按時間順序檢索用戶動態,這將非常有用。 我還爲其添加了一個default參數,並傳入了datetime.utcnow函數。 當你將一個函數作爲默認值傳入後,SQLAlchemy會將該字段設置爲調用該函數的值(請注意,在utcnow之後我沒有包含(),所以我傳遞函數本身,而不是調用它的結果)。 通常,在服務應用中使用UTC日期和時間是推薦做法。 這可以確保你使用統一的時間戳,無論用戶位於何處,這些時間戳會在顯示時轉換爲用戶的當地時間。

user_id字段被初始化爲user.id的外鍵,這意味着它引用了來自用戶表的id值。本處的user是數據庫表的名稱,Flask-SQLAlchemy自動設置類名爲小寫來作爲對應表的名稱。 User類有一個新的posts字段,用db.relationship初始化。這不是實際的數據庫字段,而是用戶和其動態之間關係的高級視圖,因此它不在數據庫圖表中。對於一對多關係,db.relationship字段通常在“一”的這邊定義,並用作訪問“多”的便捷方式。因此,如果我有一個用戶實例u,表達式u.posts將運行一個數據庫查詢,返回該用戶發表過的所有動態。 db.relationship的第一個參數表示代表關係“多”的類。 backref參數定義了代表“多”的類的實例反向調用“一”的時候的屬性名稱。這將會爲用戶動態添加一個屬性post.author,調用它將返回給該用戶動態的用戶實例。 lazy參數定義了這種關係調用的數據庫查詢是如何執行的,這個我會在後面討論。不要覺得這些細節沒什麼意思,本章的結尾將會給出對應的例子。

一旦我變更了應用模型,就需要生成一個新的數據庫遷移:

(venv) $ flask db migrate -m "posts table"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'post'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_post_timestamp' on '['timestamp']'
  Generating /home/miguel/microblog/migrations/versions/780739b227a7_posts_table.py ... done

並將這個遷移應用到數據庫:

(venv) $ flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade e517276bb1c2 -> 780739b227a7, posts table

如果你對項目使用了版本控制,記得將新的遷移腳本添加進去並提交。

表演時刻
經歷了一個漫長的過程來定義數據庫,我卻還沒向你展示它們如何使用。 由於應用還沒有任何數據庫邏輯,所以讓我們在Python解釋器中來使用以便熟悉它。 立即運行python命令來啓動Python(在啓動解釋器之前,確保您的虛擬環境已被激活)。

進入Python交互式環境後,導入數據庫實例和模型:

>>> from app import db
>>> from app.models import User, Post

開始階段,創建一個新用戶:

>>> u = User(username='john', email='[email protected]')
>>> db.session.add(u)
>>> db.session.commit()

對數據庫的更改是在會話的上下文中完成的,你可以通過db.session進行訪問驗證。 允許在會話中累積多個更改,一旦所有更改都被註冊,你可以發出一個指令db.session.commit()來以原子方式寫入所有更改。 如果在會話執行的任何時候出現錯誤,調用db.session.rollback()會中止會話並刪除存儲在其中的所有更改。 要記住的重要一點是,只有在調用db.session.commit()時纔會將更改寫入數據庫。 會話可以保證數據庫永遠不會處於不一致的狀態。

添加另一個用戶:

>>> u = User(username='susan', email='[email protected]')
>>> db.session.add(u)
>>> db.session.commit()

數據庫執行返回所有用戶的查詢:

>>> users = User.query.all()
>>> users
[<User john>, <User susan>]
>>> for u in users:
...     print(u.id, u.username)
...
1 john
2 susan

所有模型都有一個query屬性,它是運行數據庫查詢的入口。 最基本的查詢就是返回該類的所有元素,它被適當地命名爲all()。 請注意,添加這些用戶時,它們的id字段依次自動設置爲1和2。

另外一種查詢方式是,如果你知道用戶的id,可以用以下方式直接獲取用戶實例:

>>> u = User.query.get(1)
>>> u
<User john>

現在添加一條用戶動態:

>>> u = User.query.get(1)
>>> p = Post(body='my first post!', author=u)
>>> db.session.add(p)
>>> db.session.commit()

我不需要爲timestamp字段設置一個值,因爲這個字段有一個默認值,你可以在模型定義中看到。 那麼user_id字段呢? 回想一下,我在User類中創建的db.relationship爲用戶添加了posts屬性,併爲用戶動態添加了author屬性。 我使用author虛擬字段來調用其作者,而不必通過用戶ID來處理。 SQLAlchemy在這方面非常出色,因爲它提供了對關係和外鍵的高級抽象。

爲了完成演示,讓我們看看另外的數據庫查詢案例:

>>> # get all posts written by a user
>>> u = User.query.get(1)
>>> u
<User john>
>>> posts = u.posts.all()
>>> posts
[<Post my first post!>]

>>> # same, but with a user that has no posts
>>> u = User.query.get(2)
>>> u
<User susan>
>>> u.posts.all()
[]

>>> # print post author and body for all posts 
>>> posts = Post.query.all()
>>> for p in posts:
...     print(p.id, p.author.username, p.body)
...
1 john my first post!

# get all users in reverse alphabetical order
>>> User.query.order_by(User.username.desc()).all()
[<User susan>, <User john>]

Flask-SQLAlchemy文檔是學習其對應操作的最好去處。

學完本節內容,我們需要清除這些測試用戶和用戶動態,以便保持數據整潔和爲下一章做好準備:

>>> users = User.query.all()
>>> for u in users:
...     db.session.delete(u)
...
>>> posts = Post.query.all()
>>> for p in posts:
...     db.session.delete(p)
...
>>> db.session.commit()

Shell上下文
還記得上一節的啓動Python解釋器之後你做過什麼嗎?第一件事是運行兩條導入語句:

>>> from app import db
>>> from app.models import User, Post

開發應用時,你經常會在Python shell中測試,所以每次重複上面的導入都會變得枯燥乏味。 flask shell命令是flask命令集中的另一個非常有用的工具。 shell命令是Flask在繼run之後的實現第二個“核心”命令。 這個命令的目的是在應用的上下文中啓動一個Python解釋器。 這意味着什麼? 看下面的例子:

(venv) $ python
>>> app
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'app' is not defined
>>>

(venv) $ flask shell
>>> app
<Flask 'app'>

使用常規的解釋器會話時,除非明確地被導入,否則app對象是未知的,但是當使用flask shell時,該命令預先導入應用實例。 flask shell的絕妙之處不在於它預先導入了app,而是你可以配置一個“shell上下文”,也就是可以預先導入一份對象列表。

在microblog.py中實現一個函數,它通過添加數據庫實例和模型來創建了一個shell上下文環境:

from app import app, db
from app.models import User, Post

@app.shell_context_processor
def make_shell_context():
    return {'db': db, 'User': User, 'Post': Post}

app.shell_context_processor裝飾器將該函數註冊爲一個shell上下文函數。 當flask shell命令運行時,它會調用這個函數並在shell會話中註冊它返回的項目。 函數返回一個字典而不是一個列表,原因是對於每個項目,你必須通過字典的鍵提供一個名稱以便在shell中被調用。

在添加shell上下文處理器函數後,你無需導入就可以使用數據庫實例:

(venv) $ flask shell
>>> db
<SQLAlchemy engine=sqlite:////Users/migu7781/Documents/dev/flask/microblog2/app.db>
>>> User
<class 'app.models.User'>
>>> Post
<class 'app.models.Post'>
發佈了22 篇原創文章 · 獲贊 11 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章