因為以往被 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。
  • 子類 Metatable 屬性聲明了該 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 等等,或許以後有碰到再寫吧,欽此。