之前寫過一篇〈pydantic 小筆記〉,這集繼續紀錄 pydantic 的其他特性,在此大部分應用場景都是 pydantic + FastAPI,誰叫他們這麼香呢。

pydantic Model 繼承

pydantic model 可以繼承,例如 User 被幾個子類繼承:

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int

class UserCreate(User):
    password: str

class UserRead(User):
    pass

一旦繼承,子類也會有 Usernameage 屬性,以及子類本身特有的屬性,例如 UserCreate 就有 nameagepassword 三個屬性。

這是 FastAPI 中的一種設計模式,不同的子類對應不同的情境,同時也避免了打錯字、多打字的問題。

例如對於建立用戶的請求,大概會長這樣:

@app.post(path='/user', response_model=UserRead)
def create_user(user: UserCreate)
    # Create User logic
    return user

受惠於 pydantic 的檢查機制,傳送到 create_user() 的請求必須是有密碼的,而回傳的訊息則是不帶帳號密碼的,這中間不需要我們手動把 password 屬性摘除,pydantic 會自動過濾掉不存在 UserRead 類的屬性,這樣的設計也符合一般建立用戶的邏輯。

此 Model 非彼 Model

pydantic 最大的問題是它也有所謂的 model 和 schema,當再把資料庫的概念摻進來就造成語意上的混亂。

不僅是語意上,程式碼看起來也會相當冗餘,以前面的 User 資源為例,除了有 pydantic model 的定義外,在 ORM 大概也要寫一份極其相似的「model」:

class User(Model):
    id = fields.IntField(pk=True)
    name = fields.CharField(max_length=255)
    age = fields.IntField()
    password = fields.CharField(max_length=255)

這樣又回到了多打字、打錯字的老問題,於是各路人馬都看不下去,紛紛提出解決方案。

SQLModel

SQLModel

來源:SQLModel

FastAPI 本家作者另外開發的 SQLModel,它的基類 SQLModel 同時是 pydantic 的 model,也是 SQLAlchemy 的 declarative_base() 所產生的 model,只要繼承並依照 SQLModel 的規則訂定屬性就可以同時搞定兩方面的 model。

class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    age: int
    password: str

但 SQLModel 目前還在早期開發階段,文件不齊,沒有 migration 和 seeding 的說明,不太能夠用作生產力工具。

Tortoise ORM

Tortoise ORM

來源:Tortoise ORM

Tortoise ORM 是異步 ORM,它並不專為 FastAPI 打造,但還是有提供一些整合工具。

它的思路是用一個 pydantic_model_creator() 函式從 Tortoise model 生出一個 pydantic model:

class Users(models.Model):
    id = fields.IntField(pk=True)
    name = fields.CharField(max_length=255)
    age = fields.IntField()
    password = fields.CharField(max_length=255)

    class PydanticMeta:
        exclude = ["password"]


User_Pydantic = pydantic_model_creator(cls=Users, name="User")
UserIn_Pydantic = pydantic_model_creator(cls=Users, name="UserIn")

因為不是為 pydantic 或 FastAPI 量身打造的,看起來就沒那麼優雅了,但它發展較為成熟,也有確定的 migration 機制。

ormar

ormar 的思路和 SQLModel 一樣,都是讓 ormar model 同時具有 pydantic 和 SQLAlchemy 的特性,相較於 SQLModel,ormar 發展較為成熟,也有 migration 的文件可看。

它的 model 定義大概長這樣:

class User(ormar.Model):
    class Meta:
        metadata = db.metadata
        database = db.database

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=255)
    age: int = ormar.Integer()
    password: str = ormar.String(max_length=255)

pydbantic

長的和 SQLModel 有八成像:

class User(DataBaseModel):
    id: str = PrimaryKey()
    name: str
    age: int 
    password: str

一樣是以 SQLAlchemy 為基礎的。

pydbantic 的一項重要特性是「自動 migration」,這不知道該說是好還是不好,畢竟任何異動資料表的行為都應該謹慎為之,過於自動好像有風險。

Prisma

Prisma 原本是 JS 世界的 ORM,但因為核心是 Rust 撰寫,也可以被移植到 Python。

Prisma 最大的特色是它不那麼 ORM,它有自己的 schema 制定語言,不意外的就叫 Prisma,長得和 GraphQL 有八成像:

model User {
  id        Int     @id @default(autoincrement())
  name      String
  age       Int
  password  str
}

透過 Prisma 的機制,schema 會被兩方面的轉換,一方面是可以轉換成 SQL 以製作 migration 腳本,另一方面可以轉換成 Pydantic model,直接引入程式內調用,並且不只是單純的 model,其他該有的資料庫查詢接口也都傳便便了,使用過程和 gRPC 有八成像。

除了產出程式碼外,Prisma schema 作為中立的文件,也可以轉成 DBML 等其他文件形式,這又和 OpenAPI 有點像。

前面提到 schema 轉出的 migration 腳本是純 SQL 腳本,這意味著我輩的資料庫都得相同,如果生產環境是 PostgreSQL,那不好意思,整個開發團隊的個人開發環境也都要裝 PostgreSQL,否則 migration 腳本會跑不動,沒辦法用一個 SQLite 打天下,殘念,這對我的 286 來說當然是很吃力的。

cool-retro-term

來源:cool-retro-term

以上幾路方案,具體該選哪個,取決於心臟的大小,在我看來我輩開發者需要的是傻瓜 ORM,傻瓜 ORM 不需要命名資料表、不需要自己增設 idcreated_atupdated_at 欄位,也不用指定那 max_length,甚至不用管文字的長短、數字的大小,Rails 的 Active Record 就具備以上所有的傻瓜特性,上述之中最接近傻瓜 ORM 的應該是 Prisma,但它又逼我要灌大軟體。

除了以上介紹的這些,其實還有令一套也是很傻瓜的 ORM 叫 Masonite ORM,遺憾的是沒什麼人用,殘念。