Skip to content

Flask Web Development 05

changwu edited this page Mar 21, 2016 · 47 revisions

第五章

資料庫 (!!! 重要的章節)

p.69

資料庫應用程式通常需要儲存資料, 最常見的是關聯式資料庫, 而近來 NoSQL 資料庫成為另一種選擇.

SQL 資料庫 (SQL Database)

關聯式資料庫, 資料會存放在表格 (table), 隨著應用程式的功能不同, 會有不同的單元, 舉訂單管理應用為例, 可能會包含 customers, products, rders 等表格.

一個表格中, 會有固定數目的欄 (column), 與可變動數目的列 (row), 欄定義資料的屬性, 舉例來說, 欄包含以下欄位 name, address, phone, 則每一列, 會包含對應欄位的數值 (value). 表格中有種特殊欄位叫做主鍵 (primary key), 該欄位的值是唯一值用於辨識資料, 另種欄位叫做外鍵 (foreign key), 用來關連其他表格的主鍵, 這樣的關聯稱作關係 (relationship), 是關聯式資料庫的基本要素.

下圖表示表格間的關係:

relationship

在 roles 表格中, id 欄位為主鍵, 代表不同的 role, users 表格包含不同的欄位, 其中 role_id 為外鍵, 用於參考 roles 表格.

通常外鍵的名稱, 會取做"表格名稱_主鍵名字", 為慣例, 也方便辨識

在上述架構中, 可看出關聯式資料庫想盡可能有效率的存放資料避免資料重複, 定義良好的組織, 當 roles.name 有變時, 也能夠很快更新. 另一方面, 切割多個表格也帶來複雜性, 當想要存取 users 資料時, 必須在不同的資料表格當中將資料讀回來, 或 join 不同的表格.

NoSQL 資料庫 (NoSQL Database)

在 NoSQL 資料庫中, 表格稱作 collections, 記錄稱作 documents. 在 NoSQL 一般不具 join 操作, 因為其資料存放的方式而造成操作困難.

nosql

對於 NoSQL 的資料架構, 展示如上, 所有資料存於 collection 之中, 但是假設 role 的值發生改變, 對於所有使用者, 包含 role 的 document 都要全部更新; 但相較關連性資料庫的好處是, 查詢快, 無需做 join.

SQL or NoSQL?

對於這兩種資料庫架構, 本書不探討孰好孰劣, 對於小型或中等規模的應用來說, 差別較不明顯.

Python Database Framework

flask 有各式資料庫引擎套件, 支援 MySQL, Postgres, SQLite, Redis, MongoDB, CouchDB 等不同資料庫, 無論是開源或商業化資料庫; 另外, flask 也支援資料庫抽象層套件, 像是 SQLAlchemy, MongoEngine, 以 high-level 的方式操作資料.

使用 Database Framework 的好處有:

  1. 簡單使用, object-relational mappers (ORMs) 或 object-document mappers (ODMs) 提供一致性的語法, 轉成對應的資料庫指令
  2. 效能, 雖然 ORM 或 ODM 需要重新映設為資料庫語法, 中間經過一層包裝, overhead 較高, 但對於生產的方便性則有很大幫助, 適當的選擇框架, 了解框架是否提供最佳化操作.
  3. 可攜性, 當機器移轉到其他平台時, 後端資料庫可能有所不同, 使用 ORM 則無需關心資料庫為何, 只要 ORM 支援該資料庫, 則可進行對應.

本書將採 Flask-SQLAlchemy

安裝

$ pip install flask-sqlalchemy

SQLAlchemy 是關聯式資料庫的框架, 支援不同的資料庫後端.

在 Flask-SQLAlchemy 中, 資料庫是表示為 url 的形式

flask-sqlalchemy

  • hostname: 指主機位置, localhost 或 remote server
  • username/password: 視資料庫設置而定

hello.py

from flask.ext.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_COMMIT_ON_TEARDOWN'] = True

db = SQLAlchemy(app)
  • app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True: 當 request 完成後, 資料庫有所改變時, 會自動 commit
  • db 物件: 若設置成功, 資料庫可建立連線, db 被實例化, 則可透過 db 存取資料庫

定義 model

在 ORM 中, model 可看做 Python class, 對應到相應的資料庫表格.

relationship

hello.py

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    users = db.relationship('User', backref='role', lazy='dynamic')

    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)
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

    def __repr__(self):
        return '<User %r>' % self.username

類別變數

  • __tablename__: 定義資料庫的表格名稱, 若無定義, Flask-SQLAlchemy 會自動指定名稱, 由於預設名稱不遵行表格命名慣例, 即表格名稱為複數, 故不建議省略
  • 其他變數: 定義 model 屬性, 為 db.Column class 中的實例, db.Column 的第一個參數為指定欄位型態

常用的 SQLAlchemy 欄位型態

table type 1

table type 2

其他 SQLAlchemy 欄位選項

column options

在 Flask-SQLAlchemy 中, 需要所有 model 定義主鍵, 並命名為 id

  • __repr__(): 為了 debug 或測試時, 字串能夠順利印出, 原為物件

Relationships

hello.py 建立 role 與 user 間 one-to-many 的關係

class Role(db.Model):
    # ...
    users = db.relationship('User', backref='role', lazy='dynamic')


class User(db.Model):
    # ...
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

relationships

除了 one-to-many 的關係之外, 還有 one-to-one, many-to-one 及 many-to-many

  • 若要設定 one-to-one, db.relationship(uselist=False)
  • 若要設定 many-to-one, 則在 many 那端建立 db.relationship()
  • many-to-many 會在 12 章說明

建立資料表

$ python  hello.py shell
>>> from hello import db
>>> db.create_all()

當資料表已產生, 再次執行 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)
>>>
>>> print(admin_role.id)
None

資料庫的改變, 需透過 database session, 在 Flask-SQLAlchemy 中, 是由 db.session 來處理, 最後透過 db.session.commit() 將資料寫入

>>> 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.commit()

簡潔一點的方式

>>> db.session.add_all([admin_role, mod_role, user_role, user_john, user_susan, user_david])
>>> db.session.commit()
>>>

commit 後, Flask-SQLAlchemy 會自動建立 id

>>> print admin_role.id
1
>>> print mod_role.id
2
>>> print user_role.id
3

這裡的 Database session, 不是之前提過的 session, 資料庫通常也稱此為 transaction

為了維護每次 transaction 的正確性, 資料庫的操作, 在 commit 之後要確保 db.session 中的每個動作都順利執行, 才會將資料寫入. 因為可能遇到異常情況, 當資料庫操作突然中斷時, 資料在資料庫中會不一致, 尤其是銀行交易時. 若操作不成功, 則退回之前狀態, 即 db.session.rollback().

修改列資料

>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
>>>

刪除列資料

>>> db.session.delete(mod_role)
>>> db.session.commit()
>>>

查詢列資料

查詢表格中的所有資料

>>> Role.query.all()
[<Role u'Administrator'>, <Role u'User'>]
>>> User.query.all()
[<User u'john'>, <User u'susan'>, <User u'david'>]
>>>

利用 filter 找出特定資料

>>> User.query.filter_by(role=user_role).all()
[<User u'susan'>, <User u'david'>]
>>>

Common SQLAlchemy query filters

filter

Flask-SQLAlchemy 提供查看 SQLAlchemy 產生的 SQL 語法

>>> 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'
>>> 

找出 Role 中, name 為 User 的資料, 並取出第一筆

>>> user_role = Role.query.filter_by(name='User').first()
>>> user_role
<Role u'User'>
>>> user_role.id
3
>>> user_role.name
u'User'
>>> user_role.users
<sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x108af76d0>
>>> user_role.users.all()
[<User u'susan'>, <User u'david'>]
>>>

Most common SQLAlchemy query executors

executor

排序, 計數

>>> user_role.users.order_by(User.username).all()
[<User u'david'>, <User u'susan'>]
>>> user_role.users.count()
2
>>>

Dynamic relationships

  • 指定在 model 中, 資料如何匯入
  • 若不指定, 資料會自動以 all() 的方式回傳
  • 若想對資料做排序或其他額外操作時, 則無法操作

models.py

class Role(db.Model): 
    # ...
    users = db.relationship('User', backref='role', lazy='dynamic') 
    # ...

Database Use in View Functions

$ git checkout 5b

hello.py

  • 對於 post 的資料, 利用 filter_by 查詢 user 是否在資料表中, 沒有的話就新增 user, 有的話, 將 session['known'] 設成 True, 表示之前 user 來訪過
@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)
            session['known'] = False
        else:
            session['known'] = True
        session['name'] = form.name.data
        return redirect(url_for('index'))
    return render_template('index.html', form=form, name=session.get('name'),
                           known=session.get('known', False))

改寫 index.html, 因為 session['known'] 中記錄使用者是否來訪過, 根據記錄顯示不同 greeting

{% 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 整合

由於在 Python Shell 中, 每次都要重複 import 等乏味的工作, 透過 make_context 這個 callback function, 可以幫忙註冊物件, 讓 shell 方便呼叫

$ git checkout 5c

註冊要讓 shell 使用的物件

from flask.ext.script import Manager, Shell

def make_shell_context():
    return dict(app=app, db=db, User=User, Role=Role)

manager.add_command("shell", Shell(make_context=make_shell_context))

直接在 shell 將 model 當物件操作

$ python hello.py shell

>>> app
<Flask 'hello'>
>>> db
<SQLAlchemy engine='sqlite:////Users/user/sandbox/flasky/data.sqlite'>
>>> User
<class '__main__.User'>
>>> Role
<class '__main__.Role'>
>>> User.query.all()
[<User u'john'>, <User u'susan'>, <User u'david'>]
>>>

使用 Flask-Migrate 進行資料遷移

因為 Flask-SQLAlchemy 在新增資料表時, 只有當資料表不存在才會重新建立, 當想更動資料庫欄位, 除非刪除並新建資料表, 但如此一來則造成資料遺失, database migration framework 讓資料庫遷移更彈性, 它會保持追蹤 databse schema, 如版本控制般, 每次的變動都會被記錄並關聯到一個遞增的 id

安裝 flask-migrate

$ pip install flask-migrate
$ git checkout 5d

套件的初始化使用如下

from flask.ext.migrate import Migrate, MigrateCommand

# ...
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)

在 Migrate 能夠維護資料庫表動前, 必須先初始化

$ python hello.py db init
  Creating directory /Users/user/sandbox/flasky/migrations ... done
  Creating directory /Users/user/sandbox/flasky/migrations/versions ... done
  Generating /Users/user/sandbox/flasky/migrations/alembic.ini ... done
  Generating /Users/user/sandbox/flasky/migrations/env.py ... done
  Generating /Users/user/sandbox/flasky/migrations/env.pyc ... done
  Generating /Users/user/sandbox/flasky/migrations/README ... done
  Generating /Users/user/sandbox/flasky/migrations/script.py.mako ... done
  Please edit configuration/connection/logging settings in
  '/Users/user/sandbox/flasky/migrations/alembic.ini' before proceeding.

由於 migration 資料夾與 data.sqlite 檔案已經存在, 先砍掉資料夾與檔案, 再執行 migration

建立 migration script

Alembic 裡, 資料庫遷移需要 migration script, 通常包含兩個函數 upgrade() 和 downgrade().

Automatic migrations 目前不是很準確, 對於其結果可能要再檢查一下

Alembic 會去讀取 model, 建立 upgrade() 和 downgrade()

$ python hello.py db migrate -m "initial migration"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'roles'
INFO  [alembic.autogenerate.compare] Detected added table 'users'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_users_username' on '['username']'
  Generating /Users/user/sandbox/flasky/migrations/versions/22e40d65b3cb_initial_migration.py ... done

進行 upgrade(), 會自動執行 upgrade() 建立的指令, 建立資料表

$ python hello.py db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 22e40d65b3cb, initial migration

若是資料庫沒有變動, 重複執行指令不會有任何變化

Clone this wiki locally