因為以往被 Active Record 慣壞了,導致在 Python 圈一直找不到相同順手的 ORM,又因為用的不是像 Django 那樣的大禮包框架,導致我在 ORM 的路上始終尋尋覓覓,今天又又又又又要來介紹另一款 Tortoise ORM。
先吹一波特性:
- 支援 SQLite、PostgreSQL、MariaDB、MySQL,以及透過 ODBC 支援 SQL Server、Oracle。
- 異步。
- 有 migration 機制。
- 支援多個套件、框架整合,包括 UnitTest、FastAPI、Quart、Sanic、Starlette、aiohttp、BlackSheep、Pydantic,以上有得熱門有得冷門,但奇怪的是竟然沒有 Flask。即便不在上述清單內,只要搞懂 Tortoise 的初始化機制也可以自幹整合。
- 支援多資料庫。
- 支援讀寫分離。
- 但沒有 seeding 機制,得自幹。
下面是 Tortoise 和 FastAPI / pydantic 整合的快速筆記。
定義 Model
這裡的 model 指的是 ORM model,而 pydantic model 則由 ORM model 衍生而來,例如下面這個 models.py:
from tortoise import fields, models
from tortoise.contrib.pydantic import pydantic_model_creator
class Word(models.Model):
id = fields.IntField(pk=True)
#: `word` may duplicate, do not have to be unique.
word = fields.CharField(max_length=255, null=False)
accent_notation = fields.CharField(max_length=255, null=False)
class Meta:
table = 'words' # Always name with snake_case
WordRead = pydantic_model_creator(
cls=Word,
name='WordRead'
)
WordCreate = pydantic_model_creator(
cls=Word,
name='WordCreate',
exclude_readonly=True # Exclude `id` on creating
)
這裡我們定義了一個 Word model,旗下欄位由 fields
系列函式定義,應該是可以望文生義。
除欄位定義外,裡面還有一些值得一提的聲明:
word
上面有一行以#:
開頭的註解,這種格式的註解會變成 pydantic 的 field description,因此也會變成 OpenAPI 的 propertie description。- 子類
Meta
的table
屬性聲明了該 model 的資料表名稱,因為個人習慣在資料表用 snake case,因此就得額外聲明此項囉。
有了 ORM model,後續我們用 pydantic_model_creator()
建立兩個衍生的 pydantic model,習慣上他們的變數名和 model 名會取相同。
最終這份 models.py 有三個成員:
- 一個 Word ORM model。
- 一個 WordRead pydantic model。
- 一個 WordCreate pydantic model,不帶
id
欄位,因為 ID 是資料庫自動產生的,無須用戶提供。
後續我們會在 FastAPI 端點函式中使用到他們。
Tortoise ORM 整合 FastAPI
Tortoise 提供了傻瓜整合機制,自動在 FastAPI 啟動時帶起 Tortoise,結束時關閉 Tortoise 和資料庫的連線。
在 FastAPI 主程式 main.py 如此這般:
from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise
from app.models import WordCreate, Word, WordRead
app: FastAPI = FastAPI()
register_tortoise(
app=app,
db_url='sqlite://db.sqlite',
modules={'models': ['app.models']},
generate_schemas=True,
add_exception_handlers=True,
)
那句 register_tortoise()
一行就搞定,它傻瓜你聰明。
在 FastAPI 調用 Tortoise Model
前面定義的三個 model,其中的 Word model 用於實操資料庫,其餘的兩個 model 則用於在 FastAPI 函式中聲明參數或回傳值型態,例如下面這個函式:
@app.post(
path="/",
response_model=WordRead,
)
async def create_word(
word: WordCreate,
):
new_word: WordRead = await Word.create(
**word.dict(
exclude_unset=True,
)
)
return new_word
WordRead 被聲明成回傳的型態、WordCreate 被聲明成參數 word
的型態,而真正實操資料庫的語句則是由 Word.create()
構成,他們之間的轉換由 Tortoise ORM 處理到好。
下面是 CRUD 的另一個例子:
@app.get(
path='/{word}',
response_model=list[WordRead],
)
async def get_word(
word: str
):
response = await Word.filter(
word=word
)
return response
沿前例類推,WordRead 同樣被聲明成回傳型態,而根據 Word model 的設計,欄位 word
不一定唯一,因此此處調用 Word.filter()
下查詢,回傳的應該是陣列,因此我們將回傳 model 聲明為 list[WordRead]
,如果確定查詢只會有單筆紀錄那可以下 Word.get()
做查詢。
結語
本篇是 Tortoise ORM 在 FastAPI 的快速筆記,沒有提到某些也很重要的特性,例如各種花式欄位定義、關聯性、quary API、migration 等等,或許以後有碰到再寫吧,欽此。