這個系列是學習《Flask Web開發:基於Python的Web應用開發實戰》的部分筆記
對於用戶提交的信息,包括 賬號、文章 等,需要能夠將這些數據保存下來
持久存儲的三種方法:
- 文件:shelve(pickle 和 DBM 的結合)等,提供類似字典的對象接口
- 關系型數據庫(SQL)
- 非關系型數據庫(NoSQL)
- 其他
通常會使用數據庫保存信息,並向數據庫發起查詢獲取信息
SQL,關系型數據庫
關系型數據庫把數據存儲在表中,表在程序中通過 Python 的類實現。例如,訂單管理程序的數據庫中可能有表 customers、products 和 orders。
表的列數是固定的,行數是可變的。
列
定義表所表示的實體的數據屬性
。例如,customers表中可能有 name、address、phone 等列。表中的行
定義各列對應的真實數據
。
表中有個特殊的列,稱為主鍵
,其值為表中各行的唯一標識符
。表中還可以有稱為外鍵
的列,引用同一個表或不同表
中某行的主鍵
。行之間的這種聯系稱為關系
,這是關系型數據庫模型的基礎。
從這個例子可以看出,關系型數據庫存儲數據很高效,而且避免了重復
。將這個數據庫中的用戶角色重命名也很簡單,因為角色名只出現在一個地方。一旦在 roles 表中修改完角色名,所有通過 role_id 引用這個角色的用戶都能立即看到更新。
但從另一方面來看,把數據分別存放在多個表中還是很復雜的。生成一個包含角色的用戶列表會遇到一個小問題,因為在此之前要分別從兩個表中讀取用戶和用戶角色,再將其聯結起來。關系型數據庫引擎為聯結操作提供了必要的支持。
將數據分開存放在多個表中,通過外鍵建立聯結。減少數據重復量。查詢比較麻煩,但修改方便。
關系型數據庫有:
-
MySQL
-
PostgreSQL
-
SQLite
比較特殊,是存儲於硬盤上單個文件中的數據庫。用一個文件保存每一個數據庫的所有數據。Python 自帶。但同一時間只能有一個連接訪問。所以強烈建議不要在一個生產環境的web應用中使用。
等
NoSQL,非關系型數據庫
- 鍵值對
鍵-值對數據存儲是基於散列映射的數據結構。
- 面向文檔的
MongoDB
Riak
Apache CouchDB
訪問關系型數據庫
Python 可以通過數據庫接口程序(DB-API)
或對象關系映射(ORM)
訪問關系數據庫。
DB-API
Python 程序可以通過 API 連接到目標數據庫, 並用 SQL 語句進行數據讀取操作
connect(),創建連接 close(),關閉數據庫連接 commit(),提交 rollback(),回滾/取消當前
Python 的官方規范 PEP 0249
MySQL 和 PostgreSQL 是最常見的存儲 Python web 應用數據的開源數據庫。
- MySQL
唯一的 MySQL API:MySQLdb
- PostgreSQL
有至少三個接口程序
- SQLite
基本 SQL 語句
-
創建數據庫、將數據庫的權限賦給某個/全部用戶
CREATT DATABASE test;
GRANT ALL ON test.* to user; -
選擇要使用的數據庫
USE test; -
刪除數據庫
DROP DATABASE test; -
創建表
CREAT TABLE users; -
刪除表
DROP TABLE users; -
插入行
INSERT INTO users VALUES(); -
更新行
UPDATE users SET XXX; -
刪除行
DELETE FROM users ;
ORM
使用DB-API
訪問數據庫,需要懂 SQL 語言,能夠寫 SQL 語句,如果不想懂 SQL,又想使用關系型數據庫,可以使用 ORM
對象關系映射(Object Relational Mapping,簡稱ORM)
一個 ORM , 它的一端連着 Database, 一端連着 Python DataObject 對象。有了 ORM,可以通過對 Python 對象的操作,實現對數據庫的操作,不需要直接寫 SQL 語句。ORM 會自動將 Python 代碼轉換成對應的 SQL 語句。其余的操作,包括數據檢查,生成 SQL 語句、事務控制、回滾等交由 ORM 框架來完成。
DataObject 可以通過 Query 執行產生,也可以通過用戶自己創建產生。
當然,ORM 還是可以執行原始的 SQL 語句,以便執行一些復雜的/特別的操作。
查找角色為 "User" 的所有用戶: >>> user_role = Role(name='User') >>> User.query.filter_by(role=user_role).all() # [<User u'susan'>, <User u'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 FROM users WHERE :param_1 = users.role_id'
數據庫將很多 SQL 的功能抽象為 Python 對象,這樣,不需要寫 SQL 也能完成對數據庫的操作。
在Flask 中通過 Python 的類定義數據庫的表
from flask.ext.sqlalchemy import SQLAlchemy # 從 flask 擴展中導入 SQLAlchemy db = SQLAlchemy() class Post(db.Model): __tablename__ = 'posts' id = db.Column(db.Integer, primary_key=True) body = db.Column(db.Text) # 博客正文,不限長度 timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) # 發布博文的時間 body_html = db.Column(db.Text) # 存放轉換后的 HTML 代碼 author_id = db.Column(db.Integer, db.ForeignKey('users.id')) # 外鍵使用 ForeignKey,指向 User 表的 id comments = db.relationship('Comment', backref='post', lazy='dynamic')
ORM 類似標准的數據庫接口,但很多工作由 ORM 代為處理了,不需要直接使用接口。
Python 的 ORM 模塊:SQLAlchemy 等
一些大型 web 開發工具/框架 有自己的 ORM 組件。
import os basedir = os.path.abspath(os.path.dirname(__file__)) # 項目根目錄 SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db') # 數據庫文件的路徑、文件名 # print SQLALCHEMY_DATABASE_URI # sqlite:////Users/chao/Desktop/projects/flask/flask_blog/app.db SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') # 文件夾,保存`SQLAlchemy-migrate`數據文件,也就是遷移策略文件 # print SQLALCHEMY_MIGRATE_REPO # /Users/chao/Desktop/projects/flask/flask_blog/db_repository
hello.py
from flask.ext.sqlalchemy import SQLAlchemy # 從 flask 擴展中導入 SQLAlchemy db = SQLAlchemy() # 創建數據庫實例`db`
如何選擇
你要考慮以下幾個因素。
- 易用性
如果直接比較API
和ORM
,顯然后者取勝。對象關系映射(Object-Relational Mapper,ORM)
在用戶不知覺的情況下把高層的面向對象操作轉換成低層的數據庫指令
。 - 性能
ORM 把對象業務轉換成數據庫業務會有一定的損耗。大多數情況下,這種性能的降低微不足道,但也不一定都是如此。一般情況下,ORM 對生產率的提升遠遠超過了這一丁點兒的性能降低,所以性能降低這個理由不足以說服用戶完全放棄 ORM。真正的關鍵點在於如何選擇一個能直接操作低層數據庫的抽象層,以防特定的操作需要直接使用數據庫原生指令優化
。 - 可移植性
選擇數據庫時,必須考慮其是否能在你的開發平台和生產平台中使用。例如,如果你打算利用雲平台托管程序,就要知道這個雲服務提供 了哪些數據庫可供選擇。可移植性還針對 ORM。盡管有些 ORM 只為一種數據庫引擎提供抽象層,但其他 ORM 可能做了更高層的抽象,它們支持不同的數據庫引擎,而且都使用相同的面向對象接口。SQLAlchemy ORM 就是一個很好的例子,它支持很多關系型數據庫引擎,包 括流行的 MySQL、Postgres 和 SQLite。 - FLask集成度
選擇框架時,你不一定非得選擇已經集成了 Flask 的框架,但選擇這些框架可以節省你編寫集成代碼的時間。使用集成了 Flask 的框架可以簡化配置和操作,所以專門為 Flask 開發的擴展是你的首選。
基於以上因素,本書選擇使用的數據庫框架是 Flask-SQLAlchemy,這個 Flask 擴展包裝了SQLAlchemy框架。
數據庫模型
定義模型
在 ORM 中,模型
一般是一個 Python 類
, 代表數據庫中的一張表, 類中的屬性
對應數據庫表中的列
。
Flask-SQLAlchemy 創建的數據庫實例
為模型提供了一個基類db.Model
以及一系列輔助類和輔助函數,可用於定義 模型/表 的結構。
下面的例子定義了兩個表,一個是用戶角色,一個是用戶信息
hello.py
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 會使用一個默認名字,但默認的表名沒有遵守 使用復數形式進行命名(加 s ) 的約定, 所以最好由我們自己來指定表名。
其余的類變量都是該 模型的屬性/表的列,被定義為 db.Column 類的實例。
db.Column 類構造函數的第一個參數是數據庫表列/模型屬性 的類型
。
db.Column 中其余的參數指定屬性的配置選項
。
選項名 | 說 明 |
---|---|
primary_key | 如果設為 True,這列就是表的主鍵 |
unique | 如果設為 True,這列不允許出現重復的值 |
index | 如果設為 True,為這列創建索引,提升查詢效率 |
nullable | 如果設為 True,這列允許使用空值;如果設為 False,這列不允許使用空值 |
default | 為這列定義默認值 |
Flask-SQLAlchemy 要求每個模型都要定義主鍵,這一列經常命名為 id。ID 由 Flask-SQLAlchemy 控制。
雖然沒有強制要求,但這兩個模型都定義了__repr()__
方法,返回一個 具有可讀性的字符串 表示 模型,可在調試和測試時使用。
數據庫操作
學習如何使用模型的最好方法是在 Python shell 中實際操作。
- 創建表
首先,我們要讓 Flask-SQLAlchemy 根據模型類創建數據庫
。方法是使用 db.create_all() 函數:
(venv) $ python hello.py shell # 進入 Python shell >>> from hello import db # 從`hello.py`導入創建的數據庫實例 >>> db.create_all()
如果你查看程序目錄,會發現新建了一個名為app.db
的文件。這個 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 屬性並沒有明確設定,因為主鍵是由 Flask-SQLAlchemy 管理
的。現在這些對象只存在於 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
數據庫會話能保證數據庫的一致性。提交操作使用原子方式把會話中的對象全部寫入數據 庫。如果在寫入會話的過程中發生了錯誤,整個會話都會失效。如果你始終把相關改動放 在會話中提交,就能避免因部分更新導致的數據庫不一致性
。 一致性:數據庫中數據與實際保存的數據不一致。
數據庫會話也可回滾。調用 db.session.rollback() 后,添加到
數據庫會話
中、還未提交的所有對象都會還原到它們在數據庫中
的版本。
- 修改行
在數據庫會話
上調用 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 對象
。最基本的模型查詢是取回對應表中的所有記錄:

>>> Role.query.all() [<Role u'Administrator'>, <Role u'User'>] >>> User.query.all() [<User u'john'>, <User u'susan'>, <User u'david'>]
使用過濾器
可以配置 query 對象進行更精確的數據庫查詢
。下面這個例子查找角色為 "User" 的所有用戶:
>>> User.query.filter_by(role=user_role).all() # user_role = Role(name='User'), role=user_role [<User u'susan'>, <User u'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 FROM users WHERE :param_1 = users.role_id'
如果你退出了 shell 會話,前面這些例子中創建的對象就不會以 Python 對象的形式存在,而是作為各自數據庫表中的行。如果你打開了一個新的 shell 會話,就要從數據庫中讀取行, 再重新創建 Python 對象。
下面這個例子發起了一個查詢,加載名為 "User" 的用戶角色:
>>> user_role = Role.query.filter_by(name='User').first()
filter_by() 等過濾器在 query 對象上調用,返回一個更精確的 query 對象。多個過濾器可以一起調用,直到獲得所需結果。
可在 query 對象上調用的常用過濾器。
過濾器 | 說明 |
---|---|
filter() | 把過濾器添加到原查詢上,返回一個新查詢 |
filter_by() | 把等值過濾器添加到原查詢上,返回一個新查詢 |
limit() | 使用指定的值限制原查詢返回的結果數量,返回一個新查詢 |
offset() | 偏移原查詢返回的結果,返回一個新查詢 |
order_by() | 根據指定條件對原查詢結果進行排序,返回一個新查詢 |
group_by() | 根據指定條件對原查詢結果進行分組,返回一個新查詢 |
在查詢上應用指定的過濾器后
,通過調用 all() 執行查詢,以列表的形式返回結果。除了 all() 之外,還有其他方法能觸發查詢執行
。
常用查詢執行函數
方法 | 說明 |
---|---|
all() | 以列表形式返回查詢的所有結果 |
first() | 返回查詢的第一個結果,如果沒有結果,則返回 None |
first_or_404() | 返回查詢的第一個結果,如果沒有結果,則終止請求,返回 404 錯誤響應 |
get() | 返回指定主鍵對應的行,如果沒有對應的行,則返回 None |
get_or_404() | 返回指定主鍵對應的行,如果沒找到指定的主鍵,則終止請求,返回 404 錯誤響應 |
count() | 返回查詢結果的數量 |
paginate() | 返回一個 Paginate 對象,它包含指定范圍內的結果 |
關系和查詢的處理方式類似。
完整的列表參見SQLAlchemy query
下面這個例子分別從關系的兩端查詢角色和用戶之間的一對 多關系:
>>> users = user_role.users
>>> users
[<User u'susan'>, <User u'david'>] >>> users[0].role <Role u'User'>
這個例子中的 user_role.users 查詢有個小問題。執行 user_role.users 表達式時,隱含的查詢會調用 all() 返回一個用戶列表。query 對象是隱藏的,因此無法指定更精確的查詢 過濾器。就這個特定示例而言,返回一個按照字母順序排序的用戶列表可能更好。
在示例 5-4中,我們修改了關系的設置,加入了lazy = 'dynamic'參數,從而禁止自動執行查詢。
class Role(db.Model): # ... users = db.relationship('User', backref='role', lazy='dynamic') # ...
這樣配置關系之后,user_role.users 會返回一個尚未執行的查詢,因此可以在其上添加過 濾器:
>>> user_role.users.order_by(User.username).all()
[<User u'david'>, <User u'susan'>] >>> user_role.users.count() 2
在視圖函數中操作數據庫
在 Python shell 中做過練習后,可以直接在視圖函數中進行數據庫的操作了。
@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) # 沒有提交?? 配置對象中有一個選項,即 SQLALCHEMY_COMMIT_ON_TEARDOWN 鍵,將其設為 True 時,`每次請求結束后都會自動提交數據庫中的變動` 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 中創建數據庫表。
對應的模板新版本。這個模板使用 known 參數在歡迎消息中加入了第二行,從而對已知用戶和新用戶顯示不同的內容。
{% 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 %}
對象關系教程
建立一個關系
>>> from sqlalchemy import Column, Integer, String >>> class User(Base): ... __tablename__ = 'users' ... ... id = Column(Integer, primary_key=True) ... name = Column(String) ... fullname = Column(String) ... password = Column(String) ... ... def __repr__(self): ... return "<User(name='%s', fullname='%s', password='%s')>" % ( self.name, self.fullname, self.password)
讓我們考慮第二個表與User
關聯,可以被映射和查詢。Users 在可以存儲任意數量的電子郵件地址關聯的用戶名。這意味着一個從users
到一個存儲電子郵件地址的新表Addresses
的一對多
關聯。我們在Address
中使用聲明定義這張表與User
的映射:
>>> from sqlalchemy import ForeignKey >>> from sqlalchemy.orm import relationship, backref >>> class Address(Base): ... __tablename__ = 'addresses' ... id = Column(Integer, primary_key=True) ... email_address = Column(String, nullable=False) ... user_id = Column(Integer, ForeignKey('users.id')) ... ... user = relationship("User", backref=backref('addresses', order_by=id)) ... ... def __repr__(self): ... return "<Address(email_address='%s')>" % self.email_address
上述類使用了ForeignKey
函數,它是一個應用於Column
的指令,表明這一列的值應該保存指定名稱的遠程列的值
。這是關系數據庫的一個核心特征,是“膠水”,將原本無關的表變為有豐富的重疊關系的集合。上面的ForeignKey
表示,Addresses.user_id
列的值應該等於users.id
列中的值,即,users
的主鍵。
第二個函數,稱為relationship()
, 它告訴 ORM ,Address
類本身應該使用屬性Address.user
鏈接到User
類。relationship()
使用兩個表之間的外鍵關系來確定這個鏈接的性質,這個例子中,確定Address.user
將要成為多對一
中多的一側。relationship()
的參數中有一個稱為backref()
的relationship()
的子函數,反向提供詳細的信息, 即在users
中添加User
對應的Address
對象的集合,保存在User.addresses
中。多對一
關系的反向始終是一對多
的關系。一個完整的可用的relationship()
配置目錄在基本關系模式。
兩個互補關系, Address.user
和User.addresses
被稱為一個雙向關系,並且這是SQLAlchemy ORM
的一個關鍵特性。小節Linking Relationships with Backref詳細討論了“Backref”特性。
relationship()
的參數中,關聯的遠程類可以通過字符串指定,如果聲明系統在使用。在上面的例子的User
類中,一旦所有映射完成,這些字符串被認為是用於產生實際參數的 Python 表達式。允許的名字在這個評估包括,除其他方面外,所有類的名稱已被創建的聲明的基礎。
下面我們舉例說明,用User
代替Address
創建相同的 地址/用戶 雙向關系:
class User(Base): # .... addresses = relationship("Address", order_by="Address.id", backref="user")
通過relationship()
獲得參數風格的更多詳細信息。
你知道么?
- 外鍵約束的大部分(盡管不是全部)關系數據庫只能鏈接到一個主鍵列,或一個有獨特約束的列。
- 一個外鍵約束,引用多個主鍵列,並且本身有多個列,被稱為“復合外鍵”。它還可以引用這些列的一個子集。
- 外鍵列可以自動更新,以應對引用的列或行的改變。這就是所謂的級聯引用行為,是一個關系數據庫的內建函數。
- 外鍵可以引用自己的表。這是被稱為“自我引用”的外鍵。
- 更多關於外鍵的信息Foreign Key - Wikipedia
使用關聯對象
現在,當我們創建一個User
對象、將出現一個空白Addresses
集合。集合有很多類型,如sets
和詞典,這里都有可能(詳細信息Customizing Collection Access),但默認情況下,集合是一個Python列表
。
>>> jack = User(name='jack', fullname='Jack Bean', password='gjffdd') >>> jack.addresses []
我們可以向User
對象自由的添加Address
對象。在這個例子中,我們直接分配一個完整列表:
>>> jack.addresses = [ ... Address(email_address='jack@google.com'), ... Address(email_address='j25@yahoo.com')]
當使用一個雙向關系時, 元素在一側被添加后,會自動在出現在另一側。這種行為的發生,基於屬性的改變
事件,並且由 Python 判斷,不需要使用任何SQL語句:
>>> jack.addresses[1] <Address(email_address='j25@yahoo.com')> >>> jack.addresses[1].user <User(name='jack', fullname='Jack Bean', password='gjffdd')>
我們將Jack Bean
添加到數據庫會話,並提交到數據庫。jack
以及相應的addresses
集合中的兩個Address
成員都被一次性添加到會話中, 這使用了一個叫級聯
的處理:
>>> session.add(jack) >>> session.commit()
INSERT INTO users (name, fullname, password) VALUES (?, ?, ?) ('jack', 'Jack Bean', 'gjffdd') INSERT INTO addresses (email_address, user_id) VALUES (?, ?) ('jack@google.com', 5) INSERT INTO addresses (email_address, user_id) VALUES (?, ?) ('j25@yahoo.com', 5) COMMIT
查詢 jack, Jack傑克回來了。SQL中沒有提到Jack的地址:
>>> jack = session.query(User).filter_by(name='jack').one() >>> jack <User(name='jack', fullname='Jack Bean', password='gjffdd')>
BEGIN (implicit) SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.password AS users_password FROM users WHERE users.name = ? ('jack',)
讓我們看一下addresses
集合。觀察SQL:
>>> jack.addresses
[<Address(email_address='jack@google.com')>, <Address(email_address='j25@yahoo.com')>]
SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses WHERE ? = addresses.user_id ORDER BY addresses.id (5,)
當我們訪問addresses
集合時,SQL突然提到了。這是一個延遲加載的例子。addresses
集合現在被加載,並且行為就像一個普通的列表。我們將討論如何優化這個集合的加載。
基本關系模式
- 一對多
一個parent對多個child,一對多關系添加一個外鍵到child
表,用於保存對應parent.id
的值,引用parent
。relationship()在parent
中指定,引用/保存 一批 child 表中關聯的條目。
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) children = relationship("Child") class Child(Base): __tablename__ = 'child' id = Column(Integer, primary_key=True) parent_id = Column(Integer, ForeignKey('parent.id'))
在一對多模式中,建立一個雙向關系,ForeignKey
所在的是多,在relationship
中指定backref
選項:
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) children = relationship("Child", backref="parent") class Child(Base): __tablename__ = 'child' id = Column(Integer, primary_key=True) parent_id = Column(Integer, ForeignKey('parent.id'))
表child
將因此獲得一個parent
屬性, 值為對應的parent
表中的條目。
- 多對一
多個parent對一個child。多到一 在parent
表添加一個外鍵,保存child.id
的值。relationship()
在parent
中被宣告,創建一個新的屬性child
,保存關聯的child
表的條目。
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) child_id = Column(Integer, ForeignKey('child.id')) child = relationship("Child") class Child(Base): __tablename__ = 'child' id = Column(Integer, primary_key=True)
雙向行為的實現,是通過在relationship
中設置值為"parents"
的backref
可選參數。在Child
類中產生集合,收集parent
表中對應條目。
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) child_id = Column(Integer, ForeignKey('child.id')) child = relationship("Child", backref="parents")
- 一對一
一對一本質上是一種同時在兩邊設置一個數量的屬性的雙向關系。為了達到這個目標, 設置一個限制數量的屬性uselist=False
替代關系的many
側的集合。
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) child = relationship("Child", uselist=False, backref="parent") class Child(Base): __tablename__ = 'child' id = Column(Integer, primary_key=True) parent_id = Column(Integer, ForeignKey('parent.id'))
或者轉換一個 一對多 引用 到 一對一,使用backref()
函數為反向
端提供uselist=False
參數:
class Parent(Base):
__tablename__ = 'parent' id = Column(Integer, primary_key=True) child_id = Column(Integer, ForeignKey('child.id')) child = relationship("Child", backref=backref("parent", uselist=False)) class Child(Base): __tablename__ = 'child' id = Column(Integer, primary_key=True)
- 多到多
多對多關系需要在兩個類之間增加一個關系表
。關系表通過relationship()
的secondary
參數標識。通常,Table
使用基類的MetaData
對象關聯宣告,所以ForeignKey
的聲明可以定位鏈路遠端的表。
association_table = Table('association', Base.metadata, Column('left_id', Integer, ForeignKey('left.id')), Column('right_id', Integer, ForeignKey('right.id')) ) class Parent(Base): __tablename__ = 'left' id = Column(Integer, primary_key=True) children = relationship("Child", secondary=association_table) class Child(Base): __tablename__ = 'right' id = Column(Integer, primary_key=True)
對於一個雙向關系,關系兩邊都包含一個集合。backref
關鍵字將自動使用同樣的secondary
參數用於反向關系:
association_table = Table('association', Base.metadata, Column('left_id', Integer, ForeignKey('left.id')), Column('right_id', Integer, ForeignKey('right.id')) ) class Parent(Base): __tablename__ = 'left' id = Column(Integer, primary_key=True) children = relationship("Child", secondary=association_table, backref="parents") class Child(Base): __tablename__ = 'right' id = Column(Integer, primary_key=True)
relationship()
的secondary
參數還接受一個可以返回最終參數的調用,只有當映射第一次使用時進行評估。使用這個,我們可以在以后定義association_table
,只要在所有模塊初始化完成后能夠被調用:
class Parent(Base): __tablename__ = 'left' id = Column(Integer, primary_key=True) children = relationship("Child", secondary=lambda: association_table, backref="parents")
通過使用擴展的聲明,傳統的"表的字符串名稱"被接受,匹配的表名存儲在Base.metadata.tables
中:
class Parent(Base): __tablename__ = 'left' id = Column(Integer, primary_key=True) children = relationship("Child", secondary="association", backref="parents")
使用 Backref 鏈接關系
backref
關鍵字參數它實際在做什么?
讓我們先從標准的用戶和地址情境開始了解:
from sqlalchemy import Integer, ForeignKey, String, Column from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship Base = declarative_base() class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) name = Column(String) addresses = relationship("Address", backref="user") class Address(Base): __tablename__ = 'address' id = Column(Integer, primary_key=True) email = Column(String) user_id = Column(Integer, ForeignKey('user.id'))
以上配置在User
中建立一個名為User.addresses
的,關聯的Address
對象/條目的集合。它還在Address
中建立了一個user
屬性,保存關聯的User
條目。
事實上,backref
關鍵字只是一個常見的快捷方式, 用於將第二個relationship()
放置到關系另一端的Address
, 同時在兩邊建立一個事件偵聽器,在關系兩邊對屬性操作進行鏡像復制。以上配置相當於:
from sqlalchemy import Integer, ForeignKey, String, Column from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship Base = declarative_base() class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) name = Column(String) addresses = relationship("Address", back_populates="user") class Address(Base): __tablename__ = 'address' id = Column(Integer, primary_key=True) email = Column(String) user_id = Column(Integer, ForeignKey('user.id')) user = relationship("User", back_populates="addresses")
在上面,我們明確地向Address
添加了一個名為User
的關系。在關系的兩邊,back_populates
參數將關聯的對端信息告訴給每一個relationship
,表明他們應該互相之間建立“雙向”的行為。這個配置的主要作用是將事件處理程序添加到relationship
, ”當一個添加或設置事件發生時,設置使用這個屬性名稱傳入屬性”。這種行為說明如下。從一個User
和一個Address
的實例開始。addresses
集合是空的, 並且user
屬性是None
:
>>> u1 = User() >>> a1 = Address() >>> u1.addresses [] >>> print a1.user None
無論如何,一旦Address
被添加到u1.addresses
集合,所有的集合和標量屬性將被填充:
>>> u1.addresses.append(a1) >>> u1.addresses [<__main__.Address object at 0x12a6ed0>] >>> a1.user <__main__.User object at 0x12a6590>
這種行為在反向刪除操作中當然也一樣 ,同樣兩邊等效操作。例如,當user
屬性再次設置為None
,Address
對象從反向集合中被刪除:
>>> a1.user = None >>> u1.addresses []
對addresses
集合和user
屬性的操作,完全發生在 Python 中, 沒有任何與SQL數據庫的交互。如果不這樣處理, 需要將數據更新到數據庫,然后在一個提交或過期操作發生后重新加載,才能在兩邊看到正確的狀態。backref/back_populates
行為的優點是常見的雙向操作可以反映正確的狀態,不需要一個數據庫往返。
記住,當在一個關系的一邊使用backref
關鍵字,和上面 在關系的兩邊單獨使用back_populates
是一樣的。
Backref 參數
我們已經建立backref
關鍵字只是一個快捷方式,用於構建兩個獨立的relationship()
結構去引用對方。這個快捷方式的部分行為,是確定 應用到relationship()
的配置參數 也將被應用到另一個方向——即那些參數描述模式層面的關系,不太可能在相反的方向不同。通常的情況是一個多對多關系,有一個secondary
參數,或者一對多或多對一有一個primaryjoin
參數。比如如果我們限制列表中的Address
對象以tony
開頭:
from sqlalchemy import Integer, ForeignKey, String, Column from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship Base = declarative_base() class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) name = Column(String) addresses = relationship("Address", primaryjoin="and_(User.id==Address.user_id, " "Address.email.startswith('tony'))", backref="user") class Address(Base): __tablename__ = 'address' id = Column(Integer, primary_key=True) email = Column(String) user_id = Column(Integer, ForeignKey('user.id'))
我們可以觀察到,通過檢查生成的內容,關系的兩邊應用jion條件:
>>> print User.addresses.property.primaryjoin "user".id = address.user_id AND address.email LIKE :email_1 || '%%' >>> >>> print Address.user.property.primaryjoin "user".id = address.user_id AND address.email LIKE :email_1 || '%%' >>>
重用的參數都應該做“正確的事”——它只使用合適的參數,在 多對多 關系中,將對另一端反向使用primaryjoin
和secondaryjoin
。
最常見的情況是, 我們想在backref
端指定另一端使用的參數。這包括relationship()
的參數,比如lazy
,remote_side
、cascade
、cascade_backrefs
。對於這種情況,我們使用backref()
函數代替字符串:
# <other imports> from sqlalchemy.orm import backref class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) name = Column(String) addresses = relationship("Address", backref=backref("user", lazy="joined"))
上面,我們僅在Address.user
放置一個lazy="joined"
參數,表明當一個針對Address
的查詢發生,一個User
實例的 join 應自動每個返回的Address
的user
屬性填充。backref()
函數格式化的參數 我們將它變成一種被relationship()
解釋為應用到它創建的新關系的附加參數
。
數據庫遷移
在開發程序的過程中,你會發現有時需要修改數據庫模型
, 比如 增加表、添加列 ,而且修改之后還需要更新數據庫
, 就需要對數據庫進行遷移
更新表的更好方法是使用數據庫遷移框架
。源碼版本控制工具可以跟蹤源碼文件的變化, 類似地,數據庫遷移框架能跟蹤數據庫模式的變化,然后增量式的把變化應用到數據庫中
。
SQLAlchemy 的主力開發人員編寫了一個遷移框架,稱為 Alembic。除了直接使用 Alembic 之外,Flask 程序還可使用 Flask-Migrate擴展。這個擴展對 Alembic 做了輕量級包裝,並集成到 Flask-Script 中,所有操作都通過 Flask-Script 命令完成。
- 創建遷移倉庫
安裝 Flask-Migrate:
(venv) $ pip install flask-migrate
初始化、配置 Flask-Migrate
from flask.ext.migrate import Migrate, MigrateCommand # ... migrate = Migrate(app, db) # 初始化 manager.add_command('db', MigrateCommand) # 在命令行中,用`db`調用`MigrateCommand`
➜ flask_blog git:(master) ✗ python run.py usage: run.py [-?] {shell,db,runserver} ... positional arguments: {shell,db,runserver} shell Runs a Python shell inside Flask application context. db Perform database migrations runserver Runs the Flask development server i.e. app.run() optional arguments: -?, --help show this help message and exit
為了導出數據庫遷移命令
,Flask-Migrate 提供了一個 MigrateCommand 類,可附加到 Flask- Script 的 manager 對象上。在這個例子中,MigrateCommand 類使用 db 命令附加。
在維護數據庫遷移之前,要使用 init 子命令創建遷移倉庫
:
(venv) $ python hello.py db init # 將向應用添加一個`migrations`文件夾。文件夾中的文件需要和其他源文件一起進行版本控制。➜
flask_blog git:(master) ✗ python run.py db init
Creating directory /Users/chao/Desktop/projects/flask/flask_blog/migrations ... done
Creating directory /Users/chao/Desktop/projects/flask/flask_blog/migrations/versions ... done
Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/alembic.ini ... done
Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/env.py ... done
Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/env.pyc ... done
Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/README ... done
Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/script.py.mako ... done
Please edit configuration/connection/logging settings in '/Users/chao/Desktop/projects/flask/flask_blog/migrations/alembic.ini' before proceeding.
這個命令會創建 migrations 文件夾
,所有遷移腳本
都存放其中。
數據庫遷移倉庫中的文件要和程序的其他文件一起納入版本控制。
- 創建遷移腳本
在 Alembic 中,數據庫遷移用遷移腳本
表示。腳本中有兩個函數,分別是 upgrade() 和 downgrade()。upgrade() 函數把遷移中的改動應用到數據庫中
,downgrade() 函數則將改動刪除
。Alembic 具有添加和刪除改動的能力,因此數據庫可重設到修改歷史的任意一點。
我們可以使用 revision 命令手動創建 Alembic 遷移,也可使用 migrate 命令自動創建。
手動創建的遷移只是一個骨架,upgrade() 和 downgrade() 函數都是空的,開發者要使用 Alembic 提供的 Operations 對象指令實現具體操作。
自動創建的遷移會根據模型定義
和數據庫當前狀態
之間的差異生成 upgrade() 和 downgrade() 函數的內容。
自動創建的遷移不一定總是正確的,有可能會漏掉一些細節。自動生成遷移 腳本后一定要進行檢查。
migrate 子命令用來自動創建遷移腳本:
(venv) $ python hello.py 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_blog git:(master) ✗ python run.py db migrate -m 'migration' 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']' INFO [alembic.autogenerate.compare] Detected added table 'post' Generating /Users/chao/Desktop/projects/flask/flask_blog/migrations/versions/0fb164ef6c11_migration.py ... done
- 更新數據庫
檢查並修正好遷移腳本之后,我們可以使用db upgrade命令把遷移應用到數據庫
中:
(venv) $ python hello.py 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
➜ flask_blog git:(master) ✗ python run.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 -> 0fb164ef6c11, migration
對第一個遷移來說,其作用和調用 db.create_all() 方法一樣
。但在后續的遷移中, upgrade 命令能把改動應用到數據庫中,且不影響其中保存的數據
。
原文連接:http://www.jianshu.com/p/0c88017f9b46