Flask系列:數據庫

這個系列是學習《Flask Web開發:基於Python的Web應用開發實戰》的部分筆記html

對於用戶提交的信息,包括 帳號、文章 等,須要可以將這些數據保存下來python

持久存儲的三種方法:mysql

  • 文件:shelve(pickle 和 DBM 的結合)等,提供相似字典的對象接口
  • 關係型數據庫(SQL)
  • 非關係型數據庫(NoSQL)
  • 其餘

一般會使用數據庫保存信息,並向數據庫發起查詢獲取信息git

SQL,關係型數據庫

關係型數據庫把數據存儲在表中,表在程序中經過 Python 的類實現。例如,訂單管理程序的數據庫中可能有表 customers、products 和 orders。web

表的列數是固定的,行數是可變的。sql

定義表所表示的實體的數據屬性。例如,customers表中可能有 name、address、phone 等列。表中的定義各列對應的真實數據shell

表中有個特殊的列,稱爲主鍵,其值爲表中各行的惟一標識符。表中還能夠有稱爲外鍵的列,引用同一個表或不一樣表某行的主鍵。行之間的這種聯繫稱爲關係,這是關係型數據庫模型的基礎。數據庫

從這個例子能夠看出,關係型數據庫存儲數據很高效,並且避免了重複。將這個數據庫中的用戶角色重命名也很簡單,由於角色名只出如今一個地方。一旦在 roles 表中修改完角色名,全部經過 role_id 引用這個角色的用戶都能當即看到更新。django

但從另外一方面來看,把數據分別存放在多個表中仍是很複雜的。生成一個包含角色的用戶列表會遇到一個小問題,由於在此以前要分別從兩個表中讀取用戶和用戶角色,再將其聯結起來。關係型數據庫引擎爲聯結操做提供了必要的支持。flask

將數據分開存放在多個表中,經過外鍵創建聯結。減小數據重複量。查詢比較麻煩,但修改方便。

關係型數據庫有:

  • 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

sqlite3

基本 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`

如何選擇

你要考慮如下幾個因素。

  • 易用性
    若是直接比較APIORM,顯而後者取勝。對象關係映射(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 %}

對象關係教程

SQLAlchemy 官方文檔

創建一個關係

翻譯自Building a Relationship

>>> 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.userUser.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

使用關聯對象

翻譯自Working with Related Objects

如今,當咱們建立一個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集合如今被加載,而且行爲就像一個普通的列表。咱們將討論如何優化這個集合的加載。

基本關係模式

翻譯自SQLAlchemy 官方文檔

  • 一對多

一個parent對多個child,一對多關係添加一個外鍵到child表,用於保存對應parent.id的值,引用parentrelationship()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 連接關係

翻譯自Linking Relationships with 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 || '%%' >>>

重用的參數都應該作「正確的事」——它只使用合適的參數,在 多對多 關係中,將對另外一端反向使用primaryjoinsecondaryjoin

最多見的狀況是, 咱們想在backref端指定另外一端使用的參數。這包括relationship()的參數,好比lazy,remote_sidecascadecascade_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 應自動每一個返回的Addressuser屬性填充。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

相關文章
相關標籤/搜索