全棧工程師開發手冊 (作者:欒鵬)
python教程全解
SQLAlchemy 基礎
下面是一段官方 SQLAlchemy 使用示例,我們從這個例子出發,認識 SQLAlchemy。
from sqlalchemy import create_engine
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# sqlite3/mysql/postgres engine
# 請先自己在 MySQL 中創建一個名爲 test_tmp 的 database
engine = create_engine('mysql://root@localhost/test_tmp', echo=False)
Base = declarative_base()
Session = sessionmaker(bind=engine)
session1 = Session()
session2 = Session()
SessionNoAutoflush = sessionmaker(bind=engine, autoflush=False)
session3 = SessionNoAutoflush()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(64))
autoflush 和 autocommit
session 是什麼?
目前還不知道怎樣直接給 session 下定義,但是我們可以通過它的一些用途來認識它, 在腦海裏腦補出這個東西。
- session 會在需要的時候(比如用戶讀取數據、更新數據時)和數據庫進行通信,獲取數據對象,並有一個池子來維護這些對象,保證你訪問數據時不出現意外的問題
- session 和連接(connection) 不等同,session 通過連接和數據庫進行通信
- session 是 Query 的入口,當你想要發起查詢的時候,一般用法是:
session.Query(Model).filter_by(...).first()
如果不完全理解它,也沒關係,有個大概印象即可,以後碰到具體的問題再具體分析, 到時候就可以針對性解決。
官方介紹 session 的資料:https://docs.sqlalchemy.org/en/13/orm/session_basics.html#what-does-the-session-do
autoflush 參數
首先,學習兩個概念:flush 和 commit。
- flush 的意思就是將當前 session 存在的變更發給數據庫,換句話說,就是讓數據庫執行 SQL 語句。
- commit 的意思是提交一個事務。一個事務裏面可能有一條或者多條 SQL 語句
- SQLAlchemy 在執行 commit 之前,肯定會執行 flush 操作;而在執行 flush 的時候,不一定執行 commit,這個主要視 autocommit 參數而定,後面會詳細講
當 autoflush 爲 True 時(默認是 True),session 進行查詢之前會自動把當前累計的修改發送到數據庫(注意:autoflush 並不是說在 session.add 之後會自動 flush),舉個例子(結合開始的代碼):
# 創建了一個對象,這時,這個對象幾乎沒有任何意義,session 不知道它的存在
>>> user = User(name='cosven')
>>>
# session1.add 這個對象之後,它被 session 放到它的對象池裏面去了,但這時不會發送任何 SQL 語句給數據庫,數據庫目前仍然不知道它的存在
>>> session1.add(user)
>>>
# session1.Query 執行之前,由於 autoflush 是 True,session1 會先執行 session1.flush(),然後再發送查詢語句
# 當 session 進行 flush 操作時,session 會先建立(選)一個和數據庫的連接,然後將創建 user 的 SQL 語句發送給數據庫
# 所以,這個查詢是能查到 user 的
>>> session1.query(User).filter_by(name='cosven').first()
<__main__.User object at 0x1108f04e0>
如果 session 的 autoflush 爲 False 的話,session 進行查詢之前不會把當前累計的修改發送到數據庫,而直接發送查詢語句,所以下面這個查詢是查不到對象的。
>>> session3.add(User(name='haha'))
>>> session3.query(User).filter_by(name='haha').first() # None
再重複的總結一下:
session.flush 的意義:session 計算自己積累的變更,將變更對應的 SQL 語句發送給數據庫。 autoflush 的意義:session 在進行查詢之前,自動的進行一次 flush 操作。
autocommit 參數
commit 對應的概念是事務(transaction),默認情況下,session 參數 autocommit 的值是 False,SQLAlchemy 也推薦將它設置爲 False。
注:MySQL client 默認是將 autocommit 設爲 True 的,所以我們在 cli 中執行一條 SQL 語句,數據庫的數據就會發生變化
這裏複習一下一個基礎知識點:在一個事務被提交之前,事務裏面的修改只對當前事務可見,其它事務看不見。什麼意思?我們看個例子
# ps: session1 的 autocommit 參數爲 False, autoflush 參數爲 True
# 當 session1 執行 add 操作時,
>>> session1.add(User(name='miao'))
# session1 中是可以查到這個 user 的
>>> session1.query(User).filter_by(name='miao').first()
<__main__.User object at 0x1108f00000>
# session3 中查不到
>>> session3.query(User).filter_by(name='miao').first() # None
# 讓 session1 提交一下當前的事務
>>> session1.commit()
# 再從 session3 中查
>>> session3.query(User).filter_by(name='miao').first() is not None
True
事務不僅可以提交,還可以 rollback,這裏就不講。
relationship & backref
falsk 官方文檔 http://flask-sqlalchemy.pocoo.org/2.1/models/ 後發現,以前試圖孤立地理解backref
是問題之源,backref
是與relationship
配合使用的。
一對多關係
db.relationship()
用於在兩個表之間建立一對多關係
。例如書中 roles 表中一個 User 角色,可以對應 users 表中多個實際的普通用戶。實現這種關係時,要在“多”這一側加入一個外鍵,指向“一”這一側聯接的記錄。
class Role(db.Model):
# ...
users = db.relationship('User', backref='role')
class User(db.Model):
# ...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
relationship & ForeighKey
大多數情況下, db.relationship() 都能自行找到關係中的外鍵, 但有時卻無法決定把 哪一列作爲外鍵。 例如, 如果 User 模型中有兩個或以上的列定義爲 Role 模型的外鍵, SQLAlchemy 就不知道該使用哪列。如果無法決定外鍵,你就要爲 db.relationship() 提供額外參數,從而確定所用外鍵。(見書 P49)
relationship & backref
通過db.relationship()
,Role 模型有了一個可以獲得對應角色所有用戶的屬性users
。默認是列表形式,lazy='dynamic'
時返回的是一個 query 對象。即relationship
提供了 Role 對 User 的訪問。
而backref
正好相反,提供了 User 對 Role 的訪問。
不妨設一個 Role 實例爲 user_role
,一個 User 實例爲 u
。relationship 使 user_role.users
可以訪問所有符合角色的用戶,而 backref 使 u.role
可以獲得用戶對應的角色。
示例
$ p manage.py shell
>>> user_role = Role.query.filter_by(name='User').all()
>>> user_role
[<Role u'User'>]
>>> user_role = Role.query.filter_by(name='User').first()
>>> user_role
<Role u'User'>
>>> user_role.users
<sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x1087c1050>
>>> user_role.users.order_by(User.username).all()
[<User u'alice78'>, <User u'andrea86'>, <User u'hmr'>]
>>> Role.query.all()
[<Role u'Moderator'>, <Role u'Administrator'>, <Role u'User'>]
>>> user_role.users.count()
3
>>> u = User.query.filter_by(username='hmr').first()
>>> u
<User u'hmr'>
>>> u.role
<Role u'User'>
一對一關係
除了一對多之外, 還有幾種其他的關係類型。一對一關係可以用前面介紹的一對多關係表示, 但調用 db.relationship() 時要把 uselist 設爲 False , 把“多”變成“一”。
多對多關係
如果你想要用多對多關係,你需要定義一個用於關係的輔助表。對於這個輔助表, 強烈建議 不 使用模型,而是採用一個實際的表:
tags = db.Table('tags',
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')),
db.Column('page_id', db.Integer, db.ForeignKey('page.id'))
)
class Page(db.Model):
id = db.Column(db.Integer, primary_key=True)
tags = db.relationship('Tag', secondary=tags,
backref=db.backref('pages', lazy='dynamic'))
classs Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
這裏我們配置 Page.tags 加載後作爲標籤的列表,因爲我們並不期望每頁出現 太多的標籤。而每個 tag 的頁面列表( Tag.pages )是一個動態的反向引用。 正如上面提到的,這意味着你會得到一個可以發起 select 的查詢對象。
SQLAlchemy MySQL 調試小技巧
爲 MySQL 打開查詢 log
SET GLOBAL log_output = "FILE"; the default.
SET GLOBAL general_log_file = "/path/to/your/mysql.log";
SET GLOBAL general_log = 'ON';
然後在 shell 中 tail -f mysql.log
,這樣一來,當 MySQL 收到請求時,你就能看到一條日誌, 這樣可以方便你判斷 session 執行什麼操作時,會發送 SQL 語句,什麼時候建立連接。
日誌示例:
2018-11-08T15:12:41.332513Z 53 Query commit
2018-11-08T15:12:41.333753Z 53 Query rollback
2018-11-08T15:12:45.999996Z 43 Query select * from user
將上面的腳本導入 python 或者 ipython
python -i test.py
使用 flask migrate 來遷移數據結構
Flask Migrate 基於 Alembic,Alembic 是 SQLAlchemy 作者開發的數據遷移工具。
文檔主頁:https://flask-migrate.readthedocs.io/en/latest/
安裝
pip install Flask-Migrate
在安裝完成之後需要在代碼中添加如下代碼
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db = SQLAlchemy(app)
migrate = Migrate(app, db)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))
在添加代碼之後需要運行
flask db init
來初始化項目,命令運行之後會在項目中生成 migrations 文件夾,該文件夾需要添加到版本控制。
之後可以使用 migrate 命令來初始化遷移
flask db migrate
遷移腳本不會檢測models 的所有變更, Alembic 目前無法檢測表名修改,列名修改,其他限制可以在Alembic網站查看。
之後可以應用該遷移
flask db upgrade
運行該命令來將修改應用到數據庫,以後對model的每一次修改需要重複 migrate 和 upgrade 命令。如果要在不同機器之間同步數據庫結構,只需要同步 migrations 文件夾,並且在另一臺機器上運行 flask db upgrade 即可。
Flask Migrate 也支持直接使用腳本的方式運行,具體可參考官方的文檔,非常易懂。
自動生成的可能會有些錯誤,下面給幾個示例
from alembic import op
import sqlalchemy as sa
from sqlalchemy_utils import EncryptedType
batch_op.add_column(sa.Column("extra_json", sa.Text(), nullable=True))
batch_op.drop_column("extra_json")
op.create_table(
"user_attribute",
sa.Column("created_on", sa.DateTime(), nullable=True),
sa.Column("changed_on", sa.DateTime(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("welcome_dashboard_id", sa.Integer(), nullable=True),
sa.Column("created_by_fk", sa.Integer(), nullable=True),
sa.Column("changed_by_fk", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(["changed_by_fk"], ["ab_user.id"]),
sa.ForeignKeyConstraint(["created_by_fk"], ["ab_user.id"]),
sa.ForeignKeyConstraint(["user_id"], ["ab_user.id"]),
sa.ForeignKeyConstraint(["welcome_dashboard_id"], ["dashboards.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.drop_table("user_attribute")
batch_op.alter_column(
"position_json",
existing_type=sa.Text(),
type_=MediumText(),
existing_nullable=True,
)
op.add_column("dashboards", sa.Column("slug", sa.String(length=255), nullable=True))
try:
op.create_unique_constraint("idx_unique_slug", "dashboards", ["slug"])
except:
pass
op.drop_constraint(None, "dashboards", type_="unique")
op.drop_column("dashboards", "slug")
sa.Column('metadata_last_refreshed', sa.DateTime(), nullable=True),
sa.Column('cache_timeout', sa.Integer(), nullable=True),
sa.Column('broker_user', sa.String(length=255), nullable=True),
sa.Column('broker_pass', EncryptedType(), nullable=True),
sa.Column('position_json', sa.Text(), nullable=True),
sa.Column('impersonate_user', sa.Boolean(), nullable=True),
sa.Column('delivery_type', sa.Enum('attachment', 'inline', name='emaildeliverytype'), nullable=True),
參考:https://blog.csdn.net/mr_hui_/article/details/83217566
https://zhuanlan.zhihu.com/p/48994990
http://einverne.github.io/post/2018/05/flask-migrate-tutorial.html