這個系列是學習《Flask Web開發:基於Python的Web應用開發實戰》的部分筆記html
對於用戶提交的信息,包括 帳號、文章 等,須要可以將這些數據保存下來python
持久存儲的三種方法:mysql
一般會使用數據庫保存信息,並向數據庫發起查詢獲取信息git
關係型數據庫把數據存儲在表中,表在程序中經過 Python 的類實現。例如,訂單管理程序的數據庫中可能有表 customers、products 和 orders。web
表的列數是固定的,行數是可變的。sql
列
定義表所表示的實體的數據屬性
。例如,customers表中可能有 name、address、phone 等列。表中的行
定義各列對應的真實數據
。shell
表中有個特殊的列,稱爲主鍵
,其值爲表中各行的惟一標識符
。表中還能夠有稱爲外鍵
的列,引用同一個表或不一樣表
中某行的主鍵
。行之間的這種聯繫稱爲關係
,這是關係型數據庫模型的基礎。數據庫
從這個例子能夠看出,關係型數據庫存儲數據很高效,並且避免了重複
。將這個數據庫中的用戶角色重命名也很簡單,由於角色名只出如今一個地方。一旦在 roles 表中修改完角色名,全部經過 role_id 引用這個角色的用戶都能當即看到更新。django
但從另外一方面來看,把數據分別存放在多個表中仍是很複雜的。生成一個包含角色的用戶列表會遇到一個小問題,由於在此以前要分別從兩個表中讀取用戶和用戶角色,再將其聯結起來。關係型數據庫引擎爲聯結操做提供了必要的支持。flask
將數據分開存放在多個表中,經過外鍵創建聯結。減小數據重複量。查詢比較麻煩,但修改方便。
關係型數據庫有:
MySQL
PostgreSQL
SQLite
比較特殊,是存儲於硬盤上單個文件中的數據庫。用一個文件保存每個數據庫的全部數據。Python 自帶。但同一時間只能有一個鏈接訪問。因此強烈建議不要在一個生產環境的web應用中使用。
等
鍵-值對數據存儲是基於散列映射的數據結構。
MongoDB
Riak
Apache CouchDB
Python 能夠經過數據庫接口程序(DB-API)
或對象關係映射(ORM)
訪問關係數據庫。
Python 程序能夠經過 API 鏈接到目標數據庫, 並用 SQL 語句進行數據讀取操做
connect(),建立鏈接 close(),關閉數據庫鏈接 commit(),提交 rollback(),回滾/取消當前
Python 的官方規範 PEP 0249
MySQL 和 PostgreSQL 是最多見的存儲 Python web 應用數據的開源數據庫。
惟一的 MySQL API:MySQLdb
有至少三個接口程序
建立數據庫、將數據庫的權限賦給某個/所有用戶
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 ;
使用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)
在用戶不知覺的狀況下把高層的面向對象操做轉換成低層的數據庫指令
。選擇一個能直接操做低層數據庫的抽象層,以防特定的操做須要直接使用數據庫原生指令優化
。基於以上因素,本書選擇使用的數據庫框架是 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()
得到參數風格的更多詳細信息。
你知道麼?
如今,當咱們建立一個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
關鍵字參數它實際在作什麼?
讓咱們先從標準的用戶和地址情境開始瞭解:
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
關鍵字只是一個快捷方式,用於構建兩個獨立的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