天天看點

the flask mega tutorial自學記錄 之 第四章 資料庫遷移

第四章 資料庫

    • 一、 Flask架構中的資料庫
    • 二、DATABASE Migrate資料庫遷移
    • 三、Flask-SQLAlchemy設定
      • 1、設定SQLITE資料庫引擎uri
      • 2、初始化DB對象及遷移對象
    • 四、資料庫模型(類似關系型資料庫表結構)
    • 五、生成遷移庫
    • 六、第一個資料庫遷移
    • 七、資料庫更新和降級
    • 八、資料庫表關系
    • 九、玩樂時間

在實際網站開發中,開發者在本地進行開發,測試成功後再把新版本的功能釋出的正式應用環境。不斷更新更新的過程,怎麼能快速的在本地及正式環境中同步資料呢?那就需要下邊的知識——資料庫遷移技術。

一、 Flask架構中的資料庫

其實在FLASK中,是沒有自帶資料庫功能,但是,它可以與各種流行的資料庫軟體輕易結合,它把這種選擇留給開發人員,這正是Flask的靈活支援。

雖然可供選擇的資料庫很多,比如關系型資料:mysql、PostgreSQL、mssql、oracle等,還有非關系型資料庫。但是,本示例還是選用輕巧的關系型資料庫sqlite(沒有資料庫引擎)。如果要把此應用釋出到生産環境,僅需要把資料庫引擎切換到其他類型的資料庫。

第三章,我們就引進了幾個flask的擴充庫。這次,為了更好的管理資料庫操作,需要一個第三方的庫flask-sqlalchemy。

介紹下ORM

ORM 全稱 Object Relational Mapping, 翻譯過來叫對象關系映射。簡單的說,ORM 将資料庫中的表與面向對象語言中的類建立了一種對應關系。這樣,我們要操作資料庫,資料庫中的表或者表中的一條記錄就可以直接通過操作類、對象、方法或者類執行個體來完成。

SQLAlchemy 是Python 社群最知名的 ORM 工具之一,為高效和高性能的資料庫通路設計,實作了完整的企業級持久模型。它可以适用各種資料庫。

安裝flask-sqlalchemy。 注意:先要激活虛拟環境再安裝。

pip install flask-sqlalchemy
           

二、DATABASE Migrate資料庫遷移

個人了解:這裡的遷移不是僅指資料庫檔案的整體遷移(備份及還原、附加資料庫等),也包含微小資料結構改變操作。

實際應該是所有資料庫操作動作對應的語言腳本,通過這些腳本可以快速重複同樣的操作。

很多的資料教程會提到怎麼建立、使用資料庫操作,但是當APP應用的資料結構變化或擴充後的相關操作,教程很少會提及。

尤其關系型資料庫的關系結構複雜,一旦結構改變,相應的資料就需要對應改變。

安裝flask-migrate

pip install flask-migrate    注意:先要激活虛拟環境再安裝。
           

三、Flask-SQLAlchemy設定

1、設定SQLITE資料庫引擎uri

如前邊的章節介紹一樣,把此處的設定内容,增添到config.py檔案中。

path.dirname:去掉路徑中的檔案名稱。

os.path.abspath:擷取絕對路徑。

file:目前檔案的路徑。

import os
basedir = os.path.abspath(os.path.dirname(__file__))  #__file__用來獲得子產品所在的路徑.

class Config(object):
    # ...
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \    #擷取環境變量為DATABASE_URL值(windows系統中環境變量的path參數)
        'sqlite:///' + os.path.join(basedir, 'app.db')   #拼接一個sqlite接口引擎
    SQLALCHEMY_TRACK_MODIFICATIONS = False    #設定為False後,不會記錄資料的變更記錄。
           

2、初始化DB對象及遷移對象

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)                  #建立DB引擎
migrate = Migrate(app, db)            #建立Migratey引擎

from app import routes, models    #models是下邊将建立的子產品
           

Tip:更多的FLASK擴充也會像db、migrate這兩個一樣的初始化。

四、資料庫模型(類似關系型資料庫表結構)

使用者表:id、username、email、password_hash。

為什麼hash化password。主要是如果直接把密碼明文存儲,一旦資料庫被黑客攻陷,那這對使用者來說是災難性的。是以,通過hash化密碼,可以提升安全性。具體的講解放到後邊的章節。

the flask mega tutorial自學記錄 之 第四章 資料庫遷移

建立一個子產品:app/models.py,增加兩個類(表結構):

這裡請注意:如果完全按照教程中的代碼,會遇到一個坑:在送出的時候,背景語句會報錯,提示sqlite資料庫中沒有user表。是以,先用db.create_all()啟動。

db.Column的參數說明:

primary_key: 如果設為 True,這列就是表的主鍵;

unique:如果設為 True,這列不允許出現重複的值;

index:如果設為 True,為這列建立索引,提升查詢效率;

nullable:如果設為 True,這列允許使用空值;如果設為 False,這列不允許使用空值;

default:為這列定義預設值.

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)    
           

五、生成遷移庫

上邊,我們已經建立一個資料表結構。但是,随着開發的進一步發展,可能會增加字段或删除一些項目。

Alembic(适用Flask-Migrate的遷移架構,Flask作者所作)可以不用重新建構表而改變schema(結構)。

Alembic包含一個遷移庫,這個庫有一個由這些遷移腳本組成目錄。隻要資料庫結構一改變,記錄這些變動的遷移腳本将添加到遷移庫中。

類似第一章運作flask run 一樣:

flask db init
           

另外用Pycharm配置後直接運作:

the flask mega tutorial自學記錄 之 第四章 資料庫遷移

初始化後flask自動生成的語句如下:

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.
           

六、第一個資料庫遷移

上邊建立遷移庫,現在我們開始建立一個資料的遷移。

Alembic 會自動檢測資料庫model與實際資料庫裡的變動差異。由于先前沒有建立過user的schema,遷移腳步會自動增加建立一個完整的user的腳本。

-m “users table”:增加migrate的簡短描述。

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 
#e517276bb1c2是系統自動生成的遷移版本的版本号  /home/miguel/microblog/migrations/versions/e517276bb1c2_users_table.py ... done 
           

雖然運作了migrate,但是,此文法不能影響實際資料庫的資料,它僅僅是生成了遷移腳本。

如果要通過遷移改變實際資料庫的資料,需要用到如下兩個函數

upgrade()和downgrade()。

upgrade():運用遷移腳本;

downgrade():移除目前腳本,可回溯到以往的曆史節點。

七、資料庫更新和降級

假設你開發一個APP應用,先是在本地進行開發。如果model發生了變化,在沒有遷移功能情況下,你就需要在開發環境中修改model,同時在生産環境中修改model。

有了遷移功能,在開發環境中,我們就先執行migrate,然後審查這些變動能否正常運作。最後運作upgrade來更新這些變動。

如果發現有問題,可以運作downgrade回到先前的某個更新節點。

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
           

Tip:Flask-SQLAlchemy采用蛇形命名規則(用下劃線連接配接多個單詞)來命名資料庫表名稱。

例如上邊的User模型,則會在資料庫中建立一個user表。

如果要改變這預設的表名稱,可以在模型類增加一個屬性__tablename__,并指派你所期望的名稱。

八、資料庫表關系

在關系資料庫中,可以在“一”表中建立一個變量Post,用來建構user與Post的對應關系。

from datetime import datetime
from app import db              #db是上邊建立
from flask_login import UserMixin

#注意添加UserMixin,否則在送出注冊的時候報錯:User沒有active這個屬性。
class User(db.Model,UserMixin):  
	db.create_all()     #這個語句是教程中沒有,必須添加
    id = db.Column(db.Integer, primary_key=True)    #db.Column定義列字段:第一個參數字段類型,第二個是主鍵
    username = db.Column(db.String(64), index=True, unique=True)  #unique代表唯一性
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))
    #relationship生成一個關系,posts不是一個資料庫的字段,僅是一個視圖關系
    #在一對多的模式下,第一個參數是代表多方的類(表)名稱
    #第二個參數backref,将向post類中添加一個author屬性,進而定義反向關系。這一屬性可替代user_id 通路user模型,此時擷取的是模型對象,而不是外鍵的值(類似表的一行或一個執行個體)。
    #u = User.query.get(1)
    #author使用的例子:p = Post(body='my first post!', author=u)
    posts = db.relationship('Post', backref='author', lazy='dynamic')  
     
    #__repr__主要是自定義一種輸出格式,重組print的列印效果。另外一種__str__是更上一層的輸出格式。
    #終端使用者顯示使用__str__,而程式員在開發期間則使用底層的__repr__來顯示
    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)
           

九、玩樂時間

下邊的執行個體是在python自帶的shell中運作。

>>> from app import db
>>> from app.models import User, Post
>>> u = User(username='john', email='[email protected]')
>>> db.session.add(u)    #先建立回話後才可以進行資料庫操作:新增執行個體
>>> db.session.commit()    #執行操作
>>> u = User(username='susan', email='[email protected]')
>>> db.session.add(u)
>>> db.session.commit()
>>> users = User.query.all()    #查詢user所有記錄
>>> users
[<User john>, <User susan>]
>>> for u in users:
...     print(u.id, u.username)
...
1 john
2 susan

>>> u = User.query.get(1)      #查詢主鍵是1的記錄
>>> u
<User john>
>>> u = User.query.get(1)
>>> p = Post(body='my first post!', author=u)   #按照author指派POST
>>> db.session.add(p)
>>> db.session.commit()
>>> # 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>]
>>> 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()