SQLAlchemy 基礎知識 - (autoflush 和 autocommit)(relationship 和 backref)(flask migrate遷移數據)

全棧工程師開發手冊 (作者:欒鵬)
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 下定義,但是我們可以通過它的一些用途來認識它, 在腦海裏腦補出這個東西。

  1. session 會在需要的時候(比如用戶讀取數據、更新數據時)和數據庫進行通信,獲取數據對象,並有一個池子來維護這些對象,保證你訪問數據時不出現意外的問題
  2. session 和連接(connection) 不等同,session 通過連接和數據庫進行通信
  3. 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章