docs/zh-hant/docs/tutorial/sql-databases.md
FastAPI 不強制你使用 SQL(關聯式)資料庫。但你可以使用任何你想要的資料庫。
這裡我們會用 SQLModel 作為範例。
SQLModel 建立在 SQLAlchemy 與 Pydantic 之上。它由 FastAPI 的作者開發,非常適合需要使用 SQL 資料庫的 FastAPI 應用。
/// tip | 提示
你可以使用任何你想用的 SQL 或 NoSQL 資料庫函式庫(有時稱為 <abbr title="Object Relational Mapper - 物件關聯對應器:一個用來描述某些類別代表 SQL 資料表且其實例代表資料表中資料列的函式庫的術語">"ORMs"</abbr>),FastAPI 不會強迫你使用特定工具。😎
///
因為 SQLModel 建立在 SQLAlchemy 之上,你可以輕鬆使用 SQLAlchemy 所支援的任何資料庫(因此 SQLModel 也支援),例如:
在這個範例中,我們會使用 SQLite,因為它只用到單一檔案,而且 Python 內建支援。你可以直接複製這個範例並原樣執行。
之後,在你的正式環境應用中,你可能會想使用像 PostgreSQL 這類的資料庫伺服器。
/// tip | 提示
有一個包含 FastAPI 與 PostgreSQL 的官方專案產生器,還有前端與更多工具:https://github.com/fastapi/full-stack-fastapi-template
///
這是一份非常簡單且簡短的教學,如果你想更全面學習資料庫、SQL,或更進階的功能,請參考 SQLModel 文件。
SQLModel { #install-sqlmodel }首先,請先建立你的虛擬環境、啟用它,然後安裝 sqlmodel:
$ pip install sqlmodel
---> 100%
我們先用單一 SQLModel 模型建立這個應用的最簡版。
接著我們會在下方用多個模型來提升安全性與彈性。🤓
匯入 SQLModel 並建立一個資料庫模型:
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *}
Hero 類別與 Pydantic 模型非常相似(事實上,在底層它其實就是一個 Pydantic 模型)。
有幾點差異:
table=True 告訴 SQLModel 這是一個資料表模型(table model),它應該代表 SQL 資料庫中的一個資料表,而不僅僅是資料模型(就像一般的 Pydantic 類別)。
Field(primary_key=True) 告訴 SQLModel,id 是 SQL 資料庫中的主鍵(你可以在 SQLModel 文件中進一步了解 SQL 主鍵)。
注意: 我們在主鍵欄位使用 int | None,這樣在 Python 程式碼中我們可以在沒有 id 的情況下建立物件(id=None),假設資料庫在儲存時會自動產生。SQLModel 瞭解資料庫會提供 id,並且在資料庫綱要中將該欄位定義為非空的 INTEGER。詳情請見 SQLModel 文件:主鍵。
Field(index=True) 告訴 SQLModel 應為此欄位建立 SQL 索引,以便在用此欄位過濾讀取資料時更快查詢。
SQLModel 會知道宣告為 str 的欄位在 SQL 中會是 TEXT(或 VARCHAR,依資料庫而定)。
SQLModel 的 engine(底層實際上是 SQLAlchemy 的 engine)是用來維護與資料庫連線的東西。
你的程式中應該只有單一 engine 物件來連到同一個資料庫。
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *}
使用 check_same_thread=False 允許 FastAPI 在不同執行緒中使用同一個 SQLite 資料庫。這是必要的,因為單一請求可能會使用多個執行緒(例如在依賴項中)。
別擔心,依照我們的程式結構,稍後我們會確保每個請求只使用單一 SQLModel 的 session,這其實就是 check_same_thread 想要達成的事。
接著我們新增一個函式,使用 SQLModel.metadata.create_all(engine) 為所有資料表模型 建立資料表。
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *}
Session 會在記憶體中保存物件並追蹤資料需要的任何變更,然後透過 engine 與資料庫溝通。
我們會用 yield 建立一個 FastAPI 的依賴,為每個請求提供一個新的 Session。這可確保每個請求只使用單一的 session。🤓
接著我們建立一個 Annotated 的依賴 SessionDep,讓後續使用這個依賴的程式碼更簡潔。
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *}
我們會在應用啟動時建立資料庫的資料表。
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[32:37] hl[35:37] *}
這裡我們在應用的啟動事件中建立資料表。
在正式環境中,你大概會在啟動應用前使用遷移腳本來處理。🤓
/// tip | 提示
SQLModel 之後會提供包裝 Alembic 的遷移工具,但目前你可以直接使用 Alembic。
///
因為每個 SQLModel 模型同時也是一個 Pydantic 模型,你可以在與 Pydantic 模型相同的型別標註中使用它。
例如,如果你宣告一個參數型別為 Hero,它會從 JSON body 中讀取。
同樣地,你也可以將它宣告為函式的回傳型別,然後在自動產生的 API 文件 UI 中就會顯示其資料結構。
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *}
這裡我們使用 SessionDep 依賴(即一個 Session),把新的 Hero 加入 Session 實例,提交變更到資料庫,刷新 hero 的資料,然後回傳它。
我們可以用 select() 從資料庫讀取多個 Hero。可以加入 limit 與 offset 來分頁。
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *}
我們可以讀取單一的 Hero。
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *}
我們也可以刪除一個 Hero。
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *}
你可以執行應用:
<div class="termy">$ fastapi dev
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
然後前往 /docs 的 UI,你會看到 FastAPI 使用這些模型來文件化 API,也會用它們來序列化與驗證資料。
現在我們稍微重構一下這個應用,以提升安全性與彈性。
如果你檢查前一版的應用,在 UI 中你會看到,到目前為止它讓用戶端自己決定要建立的 Hero 的 id。😱
我們不該允許這樣,因為他們可能會覆蓋資料庫中我們已分配的 id。決定 id 應該由後端或資料庫來做,不是用戶端。
另外,我們為 hero 建立了 secret_name,但目前我們在各處都把它回傳出去,這一點都不保密... 😅
我們會透過加入一些額外模型來修正這些問題。這正是 SQLModel 大放異彩的地方。✨
在 SQLModel 中,任何設了 table=True 的模型類別都是資料表模型。
而沒有設 table=True 的模型類別就是資料模型,這些其實就是 Pydantic 模型(只有一點小增強)。🤓
使用 SQLModel,我們可以利用繼承來避免重複在各種情況下一再宣告所有欄位。
HeroBase - 基底類別 { #herobase-the-base-class }先從 HeroBase 模型開始,它包含所有模型共享的欄位:
nameage{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *}
Hero - 資料表模型 { #hero-the-table-model }接著建立 Hero,也就是實際的資料表模型,它包含不一定會出現在其他模型中的額外欄位:
idsecret_name因為 Hero 繼承自 HeroBase,它也擁有 HeroBase 中宣告的欄位,因此 Hero 的完整欄位為:
idnameagesecret_name{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *}
HeroPublic - 公開的資料模型 { #heropublic-the-public-data-model }接下來建立 HeroPublic 模型,它是要回傳給 API 用戶端的模型。
它擁有與 HeroBase 相同的欄位,因此不會包含 secret_name。
終於,我們英雄的真實身分受保護了!🥷
它也重新宣告了 id: int。這麼做是與 API 用戶端訂立一個契約,讓他們可以確定 id 一定存在而且是 int(不會是 None)。
/// tip | 提示
讓回傳模型保證某個值一定存在、而且一定是 int(不是 None),對 API 用戶端非常有幫助。他們在有這個確信下可以寫出更簡單的程式碼。
此外,透過自動產生的客戶端也會有更簡潔的介面,讓要使用你 API 的開發者能有更好的開發體驗。😎
///
HeroPublic 中的欄位與 HeroBase 相同,僅 id 宣告為 int(非 None):
idnameage{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *}
HeroCreate - 用於建立 Hero 的資料模型 { #herocreate-the-data-model-to-create-a-hero }現在我們建立 HeroCreate 模型,這是用來驗證用戶端送來資料的模型。
它具有與 HeroBase 相同的欄位,並且還有 secret_name。
接下來,當用戶端建立新 hero 時,他們會送上 secret_name,它會被儲存在資料庫中,但這些祕密名稱不會在 API 中回傳給用戶端。
/// tip | 提示
這也就是你處理密碼的方式。接收它們,但不要在 API 中回傳。
你也應該在儲存前先對密碼做雜湊,永遠不要以明文儲存。
///
HeroCreate 的欄位有:
nameagesecret_name{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *}
HeroUpdate - 用於更新 Hero 的資料模型 { #heroupdate-the-data-model-to-update-a-hero }在前一版的應用中,我們沒有更新 hero 的方式,但現在有了多個模型,我們就能做到。🎉
HeroUpdate 這個資料模型有點特別,它包含建立新 hero 所需的所有欄位,但所有欄位都是可選的(都有預設值)。這樣在更新時,你只需要送出想要更新的欄位即可。
因為所有欄位其實都改變了(型別現在包含 None,而且預設值為 None),我們需要重新宣告它們。
其實不一定要繼承 HeroBase,因為我們會重新宣告所有欄位。我這裡保留繼承只是為了一致性,並非必要。這主要是個人偏好的問題。🤷
HeroUpdate 的欄位有:
nameagesecret_name{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *}
HeroCreate 建立並回傳 HeroPublic { #create-with-herocreate-and-return-a-heropublic }現在我們有了多個模型,可以更新應用中使用它們的部分。
我們在請求中接收 HeroCreate 資料模型,並由它建立一個 Hero 資料表模型。
這個新的資料表模型 Hero 會有用戶端傳來的欄位,並且會由資料庫產生一個 id。
然後我們直接從函式回傳這個資料表模型 Hero。但因為我們用 HeroPublic 資料模型當作 response_model,FastAPI 會用 HeroPublic 來驗證與序列化資料。
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *}
/// tip | 提示
現在我們用 response_model=HeroPublic,而不是用回傳型別標註 -> HeroPublic,因為我們實際回傳的值其實不是 HeroPublic。
如果我們宣告 -> HeroPublic,你的編輯器與 linter 會(理所當然地)抱怨你回傳的是 Hero 而不是 HeroPublic。
在 response_model 中宣告,就是要讓 FastAPI 去做它該做的事,而不影響型別標註,以及你的編輯器與其他工具提供的協助。
///
HeroPublic 讀取多個 Hero { #read-heroes-with-heropublic }我們可以像先前一樣讀取多個 Hero。同樣地,我們使用 response_model=list[HeroPublic] 來確保資料被正確驗證與序列化。
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *}
HeroPublic 讀取單一 Hero { #read-one-hero-with-heropublic }我們可以讀取單一 hero:
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *}
HeroUpdate 更新 Hero { #update-a-hero-with-heroupdate }我們可以更新 hero。為此我們使用 HTTP 的 PATCH 操作。
在程式碼中,我們會取得一個只包含用戶端有傳送的資料的 dict,只包含用戶端傳送的資料,不包含只是因為有預設值而存在的欄位。為了達成這點,我們使用 exclude_unset=True。這是關鍵。🪄
然後我們使用 hero_db.sqlmodel_update(hero_data) 以 hero_data 的資料更新 hero_db。
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *}
刪除 hero 基本上維持不變。
我們不會為了重構而重構一切。😅
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *}
你可以再次執行應用:
<div class="termy">$ fastapi dev
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
如果你前往 /docs 的 API UI,你會看到它已更新,建立 hero 時不再期待從用戶端接收 id,等等。
你可以使用 SQLModel 與 SQL 資料庫互動,並用資料模型與資料表模型讓程式碼更簡潔。
你可以在 SQLModel 文件學到更多內容,這裡還有一份更長的 使用 SQLModel 與 FastAPI 的教學。🚀