在現代應用開發領域,選擇合適的資料函式庫和程式語言特性往往決定了應用程式的效能表現。MongoDB作為領先的檔案資料函式庫,結合Python的非同步功能,創造出一種特別強大的技術組合。這種組合透過Beanie ODM(物件檔案對映器)和Pydantic的加持,讓開發者能夠構建既快速又可靠的應用程式。

MongoDB的崛起與技術優勢

MongoDB自問世以來,已迅速成為開發者社群中最受歡迎的資料函式庫之一。根據Stack Overflow的調查資料,MongoDB的採用率持續攀升,目前與PostgreSQL並列為最受歡迎的非關聯式資料函式庫選項。

這種受歡迎程度帶來幾個明顯優勢:

  • 豐富的人才資源:在市場上容易找到熟悉MongoDB的開發人才
  • 就業市場競爭力:掌握MongoDB技能的開發者更具市場價值
  • 完善的生態系統:龐大的社群支援和豐富的資源函式庫

MongoDB受歡迎的核心原因在於其速度和易用性。不僅資料函式庫本身執行快速,使用MongoDB構建高效能應用也相對直觀。開發者無需深陷關聯式資料函式庫中常見的複雜連線操作,能夠更自然地設計和實作應用邏輯。

實際案例:MongoDB的效能表現

以一個實際執行的網站為例,某Python培訓平台展示了MongoDB在真實環境中的效能表現:

課程頁面效能:

  • 包含四個獨立的MongoDB查詢
  • 處理使用者登入狀態、課程資訊等多項資料
  • 平均回應時間僅為33毫秒
  • 執行在標準規格的伺服器上

課程觀看頁面效能:

  • 包含10個MongoDB查詢
  • 處理課程內容、章節、講座、使用者許可權等複雜資訊
  • 部分集合(MongoDB中的「表」)包含數百萬筆記錄
  • 平均回應時間僅為4毫秒

值得注意的是,這些資料並非透過快取實作,而是每次請求都實際查詢資料函式庫的結果。這種效能水平在網頁應用中相當出色,展示了MongoDB在實際應用中的強大能力。

Python的非同步I/O:效能的關鍵

Python的非同步I/O(Async I/O)是實作高效能MongoDB應用的關鍵技術。與傳統的平行處理方法(如多執行緒或多處理)相比,Async I/O採用了一種更為優雅的並發模型。

非同步I/O的工作原理

在傳統的同步程式中,當執行I/O操作(如資料函式庫查詢)時,程式會阻塞等待操作完成。而在非同步模型中,程式的運作方式完全不同:

  1. 程式將I/O操作分解為多個小任務
  2. 在等待I/O完成的時間內,可以執行其他任務
  3. 所有任務在同一個執行緒中交錯執行,無需建立額外的執行緒

例如,當查詢MongoDB時,非同步程式會:

  • 開始查詢操作
  • 在等待資料函式庫回應時執行其他工作
  • 收到回應後繼續處理結果

這種模型特別適合I/O密集型應用,如網頁後端和資料函式庫操作,能夠顯著提高應用的吞吐量和回應速度。

非同步程式設計的優勢

許多開發者認為並發程式設計很困難,但Python的Async I/O模型相對簡單:

  • 程式碼結構與同步程式碼相似
  • 只需增加少量關鍵字(如asyncawait
  • 概念上需要一些調整,但語法上變化不大
  • 效能提升顯著,特別是在I/O密集型應用中

Beanie ODM:連線MongoDB與Async Python

Beanie是一個專為MongoDB設計的非同步物件檔案對映器(ODM),它根據Pydantic構建,提供了一種優雅的方式來定義和操作MongoDB檔案。

Beanie的核心特性

Beanie結合了多種強大技術:

  1. 非同步操作:使用Python的async/await語法,允許在等待資料函式庫操作完成時執行其他工作
  2. 物件檔案對映:類別似於關聯式資料函式庫的ORM,但專為檔案資料函式庫設計
  3. 根據Pydantic:利用Pydantic提供的資料驗證、序列化和型別安全功能
  4. 與FastAPI等框架整合:與其他使用Pydantic的工具無縫整合

Beanie使用範例

以下是一個簡單的Beanie模型定義和查詢範例:

from beanie import Document, Indexed
from pydantic import BaseModel
from typing import List

# 定義嵌入式檔案
class Category(BaseModel):
    name: str
    description: str

# 定義主檔案模型
class Product(Document):
    name: str
    description: str
    price: Indexed(float)  # 建立索引以加速查詢
    categories: List[Category]  # 嵌入式檔案列表
    
    class Settings:
        name = "products"  # 集合名稱

# 查詢範例
async def find_cheap_chocolates():
    chocolates = await Product.find(
        Product.categories.name == "chocolate",
        Product.price < 5.0
    ).to_list()
    return chocolates

這段程式碼展示了Beanie的幾個關鍵特性:

  1. 首先定義了一個Category類別,它繼承自Pydantic的BaseModel,代表一個嵌入式檔案,包含名稱和描述兩個欄位。

  2. 接著定義了Product類別,它繼承自Beanie的Document類別,代表MongoDB中的一個檔案。這個類別包含了產品名稱、描述、價格和分類別列表。

  3. Indexed(float)表示在price欄位上建立索引,這能加速根據價格的查詢操作。

  4. categories欄位是一個Category物件的列表,展示了MongoDB中嵌入式檔案的使用方式。

  5. 內部的Settings類別用於設定MongoDB相關設定,這裡指定了集合名稱為"products"。

  6. 最後的查詢函式find_cheap_chocolates()展示瞭如何使用Beanie的查詢API。它使用了非同步語法(async/await),並透過直覺的物件導向方式構建查詢條件,找出所有分類別為"chocolate"與價格低於5.0的產品。

這種寫法讓MongoDB的查詢變得更加直覺和型別安全,同時保持了非同步操作的高效能。

MongoDB與檔案資料函式庫基礎

在探討Beanie和非同步操作之前,讓我們先了解MongoDB作為檔案資料函式庫的基本概念。

檔案資料函式庫與關聯式資料函式庫的區別

檔案資料函式庫與傳統關聯式資料函式庫有著根本的不同:

  1. 資料結構

    • 關聯式資料函式庫:資料儲存在表格中,具有固定的結構和關聯
    • 檔案資料函式庫:資料儲存在靈活的檔案中,可以有不同的結構和巢狀層次
  2. 關聯處理

    • 關聯式資料函式庫:透過外部索引鍵和連線操作處理關聯
    • 檔案資料函式庫:傾向於嵌入相關資料,減少連線操作
  3. 擴充套件性

    • 關聯式資料函式庫:通常垂直擴充套件(增加單一伺服器的資源)
    • 檔案資料函式庫:設計用於水平擴充套件(增加伺服器數量)

MongoDB的核心概念

MongoDB使用一些特定術語來描述其元件:

  • 檔案(Document):MongoDB中的基本資料單位,類別似於關聯式資料函式庫中的「行」
  • 集合(Collection):檔案的分組,類別似於關聯式資料函式庫中的「表」
  • 資料函式庫(Database):集合的容器
  • 嵌入式檔案:檔案中的巢狀檔案,允許在單一檔案中儲存複雜的層次結構
  • BSON:MongoDB使用的二進位JSON格式,支援額外的資料型別

MongoDB的這種結構使其特別適合處理:

  • 半結構化資料
  • 頻繁變化的資料結構
  • 需要快速讀取的應用
  • 需要水平擴充套件的大規模應用

Pydantic:資料驗證與序列化的基礎

在使用Beanie之前,我們需要了解Pydantic,這是Beanie的核心依賴,也是現代Python應用中最流行的資料驗證函式庫之一。

Pydantic的核心功能

Pydantic提供了一種宣告式的方式來定義資料模型,具有以下特點:

  1. 資料驗證:自動驗證資料符合定義的型別和約束
  2. 型別提示:利用Python的型別提示系統提供編輯器支援和靜態型別檢查
  3. 資料轉換:自動將輸入資料轉換為適當的型別
  4. JSON序列化/反序列化:輕鬆在Python物件和JSON之間轉換

Pydantic基本模型定義

以下是一個簡單的Pydantic模型定義:

from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime

class User(BaseModel):
    id: str
    username: str
    email: str
    full_name: Optional[str] = None
    created_at: datetime = Field(default_factory=datetime.now)
    tags: List[str] = []

# 使用模型
user_data = {
    "id": "user123",
    "username": "pythonista",
    "email": "user@example.com",
    "tags": ["python", "mongodb"]
}
user = User(**user_data)
print(user.model_dump_json(indent=2))

這段程式碼展示了Pydantic的基本用法:

  1. 首先匯入必要的模組,包括Pydantic的BaseModelField,以及用於型別註解的ListOptional

  2. 定義了一個User類別,繼承自BaseModel,包含多種型別的欄位:

    • 必填的字串欄位:idusernameemail
    • 可選的字串欄位:full_name,預設為None
    • 日期時間欄位:created_at,使用Fielddefault_factory引數設定預設值為當前時間
    • 字串列表欄位:tags,預設為空列表
  3. 建立了一個字典user_data,包含使用者資料。

  4. 使用**user_data語法將字典展開並傳入User類別的建構函式,建立一個User例項。

  5. 使用model_dump_json方法將User例項轉換為格式化的JSON字串並輸出。

這個例子展示了Pydantic如何簡化資料驗證和處理:它自動檢查資料型別,處理預設值,並提供序列化功能。在實際應用中,這大減少了手動驗證和轉換的工作量。

Pydantic與MongoDB的結合

Pydantic模型非常適合表示MongoDB檔案,因為:

  1. 兩者都支援巢狀結構
  2. 兩者都根據類別似JSON的資料格式
  3. Pydantic的驗證確保寫入MongoDB的資料符合預期

Async Python:非同步程式設計基礎

在深入Beanie之前,讓我們先了解Python的非同步程式設計基礎,這是高效能MongoDB應用的關鍵。

非同步程式設計的核心概念

Python的非同步程式設計根據以下核心概念:

  1. 協程(Coroutines):使用async def定義的特殊函式,可以在執行過程中暫停和還原
  2. 事件迴圈(Event Loop):管理和執行協程的中央機制
  3. await表示式:暫停協程執行,等待另一個協程完成
  4. 任務(Tasks):事件迴圈中的協程例項,可以平行執行

基本非同步程式範例

以下是一個簡單的非同步程式範例:

import asyncio

async def fetch_data(delay, name):
    print(f"開始取得 {name} 資料...")
    await asyncio.sleep(delay)  # 模擬I/O操作
    print(f"{name} 資料取得完成")
    return f"{name} 結果"

async def main():
    # 平行執行多個協程
    results = await asyncio.gather(
        fetch_data(2, "使用者"),
        fetch_data(1, "產品"),
        fetch_data(3, "訂單")
    )
    print(f"所有結果: {results}")

# 執行非同步程式
asyncio.run(main())

這段程式碼展示了Python非同步程式設計的基本概念:

  1. fetch_data函式使用async def定義為協程,它接受兩個引數:延遲時間和名稱。

  2. 函式內部使用await asyncio.sleep(delay)模擬I/O操作(如網路請求或資料函式庫查詢)。await關鍵字表示在這個點上,函式會暫停執行,將控制權交還給事件迴圈,讓其他協程有機會執行。

  3. main函式也是一個協程,它使用asyncio.gather同時啟動三個fetch_data協程,並等待它們全部完成。

  4. asyncio.run(main())是啟動非同步程式的入口點,它建立一個新的事件迴圈,執行main協程直到完成,然後關閉事件迴圈。

執行結果會是:

開始取得 使用者 資料...
開始取得 產品 資料...
開始取得 訂單 資料...
產品 資料取得完成
使用者 資料取得完成
訂單 資料取得完成
所有結果: ['使用者 結果', '產品 結果', '訂單 結果']

注意事項:

  • 所有協程在同一個執行緒中執行
  • 當一個協程等待I/O時,控制權回傳給事件迴圈,執行其他協程
  • 最終結果按照原始順序回傳,而不是完成順序

非同步與資料函式庫操作

非同步程式設計特別適合資料函式庫操作,因為:

  1. 資料函式庫查詢通常涉及網路I/O,有大量等待時間
  2. 在等待一個查詢結果時,可以執行其他查詢或處理其他請求
  3. 單一執行緒可以處理多個並發連線,減少資源消耗

在MongoDB應用中,非同步操作可以顯著提高效能,特別是在處理多個並發請求或執行多個資料函式庫操作時。

Beanie ODM:快速入門

現在我們已經瞭解了Pydantic和非同步Python的基礎,讓我們開始使用Beanie ODM來操作MongoDB。

安裝與設定

首先,我們需要安裝必要的套件:

pip install beanie motor pydantic

這將安裝:

  • Beanie:非同步ODM
  • Motor:MongoDB的非同步Python驅動程式
  • Pydantic:資料驗證函式庫

初始化資料函式庫連線

接下來,我們需要初始化與MongoDB的連線:

import asyncio
from beanie import init_beanie
from motor.motor_asyncio import AsyncIOMotorClient

async def init_db():
    # 建立Motor客戶端
    client = AsyncIOMotorClient("mongodb://localhost:27017")
    
    # 初始化Beanie
    await init_beanie(
        database=client.my_database,
        document_models=[
            # 這裡將放置我們的檔案模型類別
        ]
    )

# 在應用啟動時執行
asyncio.run(init_db())

這段程式碼展示瞭如何初始化Beanie與MongoDB的連線:

  1. 首先匯入必要的模組,包括asyncio用於執行非同步程式,init_beanie函式用於初始化Beanie,以及AsyncIOMotorClient用於建立MongoDB連線。

  2. 定義了一個非同步函式init_db,它負責初始化資料函式庫連線。

  3. 在函式內部,首先建立一個Motor客戶端,連線到本地的MongoDB伺服器(localhost:27017)。

  4. 然後使用init_beanie函式初始化Beanie,指定要使用的資料函式庫(client.my_database)。document_models引數是一個列表,用於指定所有需要註冊的檔案模型類別。

  5. 最後使用asyncio.run(init_db())執行初始化函式。

這個初始化過程必須在應用啟動時執行,確保在使用Beanie模型之前已經建立了與MongoDB的連線。在實際應用中,這通常會在應用的啟動階段執行,例如在FastAPI應用中,可以在@app.on_event("startup")處理函式中執行。

定義檔案模型

現在,讓我們定義一些檔案模型:

from beanie import Document, Indexed
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime

# 嵌入式檔案模型
class Address(BaseModel):
    street: str
    city: str
    country: str
    postal_code: str

# 主檔案模型
class User(Document):
    name: str
    email: Indexed(str, unique=True)  # 建立唯一索引
    age: Optional[int] = None
    addresses: List[Address] = []
    created_at: datetime = datetime.now()
    
    class Settings:
        name = "users"  # 集合名稱

這段程式碼定義了兩個模型:一個嵌入式檔案模型和一個主檔案模型:

  1. Address類別是一個嵌入式檔案模型,繼承自BaseModel。它包含地址的基本資訊:街道、城市、國家和郵遞區號。

  2. User類別是一個主檔案模型,繼承自Document。它包含:

    • 基本資訊:名稱和電子郵件
    • 使用Indexed裝飾的電子郵件欄位,設定為唯一索引,確保不會有重複的電子郵件
    • 可選的年齡欄位,預設為None
    • 地址列表,使用前面定義的Address類別,預設為空列表
    • 建立時間,預設為當前時間
  3. 內部的Settings類別用於設定MongoDB相關設定,這裡指定了集合名稱為"users"。

這種模型定義方式結合了Pydantic的資料驗證能力和Beanie的檔案對映功能,讓我們能夠以型別安全的方式操作MongoDB檔案。嵌入式檔案(如addresses)直接對映到MongoDB的嵌入式檔案結構,這是檔案資料函式庫的一個重要特性。

基本CRUD操作

現在我們可以執行基本的CRUD(建立、讀取、更新、刪除)操作:

async def demo_crud():
    # 建立使用者
    user = User(
        name="玄貓",
        email="blackcat@example.com",
        age=30,
        addresses=[
            Address(
                street="科技路 123 號",
                city="台北",
                country="台灣",
                postal_code="10001"
            )
        ]
    )
    
    # 儲存到資料函式庫
    await user.insert()
    print(f"已建立使用者: {user.id}")
    
    # 查詢使用者
    found_user = await User.find_one(User.email == "blackcat@example.com")
    print(f"找到使用者: {found_user.name}")
    
    # 更新使用者
    found_user.age = 31
    await found_user.save()
    print("使用者已更新")
    
    # 刪除使用者
    await found_user.delete()
    print("使用者已刪除")

# 執行範例
asyncio.run(demo_crud())

這段程式碼展示了使用Beanie進行基本CRUD操作的流程:

  1. 建立(Create)

    • 建立一個新的User例項,設定名稱、電子郵件、年齡和地址
    • 地址是一個Address物件的列表,展示了嵌入式檔案的使用
    • 使用await user.insert()將使用者資料儲存到MongoDB
  2. 讀取(Read)

    • 使用User.find_one()方法查詢特定使用者
    • 查詢條件使用物件導向語法:User.email == "blackcat@example.com"
    • 結果直接回傳為User物件,可以直接存取其屬性
  3. 更新(Update)

    • 直接修改找到的使用者物件的屬性(found_user.age = 31
    • 使用await found_user.save()將變更儲存到資料函式庫
  4. 刪除(Delete)

    • 使用await found_user.delete()從資料函式庫中刪除使用者

整個過程都使用非同步操作(async/await),確保高效能。Beanie的API設計非常直覺,讓MongoDB操作變得簡單而自然,同時保持了型別安全和資料驗證的優勢。

複雜查詢

Beanie支援複雜的查詢操作:

async def advanced_queries():
    # 插入一些測試資料
    await User(name="Teresa", email="teresa@example.com", age=25).insert()
    await User(name="Rita", email="rita@example.com", age=30).insert()
    await User(name="BlackCat", email="blackcat@example.com", age=35).insert()
    
    # 基本查詢
    users = await User.find(User.age >= 30).to_list()
    print(f"30歲以上的使用者: {[user.name for user in users]}")
    
    # 排序
    users = await User.find().sort(User.age).to_list()
    print(f"按年齡排序的使用者: {[user.name for user in users]}")
    
    # 限制結果數量
    users = await User.find().limit(2).to_list()
    print(f"限制結果的使用者: {[user.name for user in users]}")
    
    # 複合條件
    users = await User.find(
        User.age > 20,
        User.name.match("^[AB]")  # 名字以A或B開頭
    ).to_list()
    print(f"複合條件的使用者: {[user.name for user in users]}")
    
    # 聚合查詢
    avg_age = await User.find().aggregate([
        {"$group": {"_id": None, "avg_age": {"$avg": "$age"}}}
    ]).to_list()
    print(f"平均年齡: {avg_age[0]['avg_age']}")

# 執行範例
asyncio.run(advanced_queries())

這段程式碼展示了Beanie支援的多種複雜查詢操作:

  1. 資料準備

    • 首先插入三個測試使用者,年齡分別為25、30和35歲
  2. 條件查詢

    • 使用User.find(User.age >= 30)查詢30歲以上的使用者
    • 查詢條件使用直覺的比較運算元
  3. 排序

    • 使用.sort(User.age)按年齡升序排序
    • 若要降序排序,可以使用-User.age.sort(-User.age)
  4. 分頁

    • 使用.limit(2)限制回傳結果數量為2
    • 在實際應用中,通常會結合.skip()實作分頁功能
  5. 複合條件

    • 同時使用多個條件:年齡大於20與名字以A或B開頭
    • 使用.match("^[AB]")進行正規表示式比對
  6. 聚合查詢

    • 使用MongoDB的聚合管道計算平均年齡
    • 聚合查詢允許進行複雜的資料處理和統計

這些查詢功能展示了Beanie如何將MongoDB的強大查詢能力以Python友好的方式呈現出來。開發者可以使用熟悉的物件導向語法構建複雜查詢,同時享受非同步操作的效能優勢。

檔案建模實踐:從關聯式思維到檔案思維

在使用MongoDB和Beanie時,我們需要調整思維方式,從關聯式資料函式庫的表格思維轉向檔案資料函式庫的巢狀思維。

關聯式模型與檔案模型的對比

假設我們有一個簡單的部落格系統,在關聯式資料函式庫中,我們可能會有以下表格:

users:
- id
- name
- email

posts:
- id
- title
- content
- user_id (外部索引鍵)

comments:
- id
- content
- post_id (外部索引鍵)
- user_id (外部索引鍵)

在MongoDB中,我們有多種建模選擇:

選項1:完全嵌入(適合一對多關係,子檔案數量有限)

class Comment(BaseModel):
    content: str
    author_name: str
    created_at: datetime = datetime.now()

class Post(Document):
    title: str
    content: str
    author: User  # 嵌入完整使用者檔案
    comments: List[Comment] = []  # 嵌入評論
    created_at: datetime = datetime.now()

選項2:參照ID(適合多對多關係或子檔案數量大的情況)

class User(Document):
    name: str
    email: str

class Comment(Document):
    content: str
    post_id: PydanticObjectId  # 參照帖子ID
    user_id: PydanticObjectId  # 參照使用者ID
    created_at: datetime = datetime.now()

class Post(Document):
    title: str
    content: str
    user_id: PydanticObjectId  # 參照使用者ID
    created_at: datetime = datetime.now()

選項3:混合方法(平衡效能和靈活性)

class CommentSummary(BaseModel):
    id: PydanticObjectId
    content_preview: str  # 評論內容預覽
    author_name: str

class Post(Document):
    title: str
    content: str
    user_id: PydanticObjectId  # 參照使用者ID
    author_name: str  # 冗餘儲存作者名稱,避免查詢
    comment_count: int = 0  # 評論計數
    recent_comments: List[CommentSummary] = []  # 最近評論的摘要
    created_at: datetime = datetime.now()

建模決策

在決定如何建模MongoDB檔案時,考慮以下因素:

  1. 讀寫比例

    • 讀多寫少:傾向於嵌入和冗餘
    • 寫多讀少:傾向於參照
  2. 關係基數

    • 一對一或一對少:適合嵌入
    • 一對多(數量大)或多對多:適合參照
  3. 檔案大小

    • MongoDB檔案大小限制為16MB
    • 如果嵌入可能導致檔案過大,使用參照
  4. 查詢模式

    • 如果總是一起查詢:嵌入
    • 如果經常單獨查詢:分開儲存
  5. 原子性需求

    • 如果需要原子更新:嵌入(MongoDB只保證單檔案原子性)

實際建模範例:電子商務系統

以下是一個電子商務系統的檔案模型範例:

from beanie import Document, Indexed, Link
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime

# 嵌入式檔案
class Address(BaseModel):
    street: str
    city: str
    country: str
    postal_code: str
    is_default: bool = False

class ProductVariant(BaseModel):
    name: str
    sku: str
    price: float
    stock: int

# 主檔案
class User(Document):
    name: str
    email: Indexed(str, unique=True)
    addresses: List[Address] = []
    created_at: datetime = datetime.now()
    
    class Settings:
        name = "users"

class Product(Document):
    name: str
    description: str
    base_price: float
    category: str
    variants: List[ProductVariant] = []
    created_at: datetime = datetime.now()
    
    class Settings:
        name = "products"

class OrderItem(BaseModel):
    product_id: PydanticObjectId
    product_name: str  # 冗餘儲存
    variant_name: str
    price: float  # 購買時的價格
    quantity: int

class Order(Document):
    user_id: PydanticObjectId
    user_email: str  # 冗餘儲存
    items: List[OrderItem]
    total_amount: float
    shipping_address: Address
    status: str = "pending"
    created_at: datetime = datetime.now()
    
    class Settings:
        name = "orders"

這個電子商務系統的檔案模型展示了MongoDB建模的多種技巧:

  1. 嵌入式檔案

    • AddressProductVariant作為嵌入式檔案,分別用於使用者地址和產品變體
    • OrderItem作為訂單中的嵌入式檔案,包含訂單專案的詳細資訊
  2. 參照關係

    • Order中的user_id參照了User檔案
    • OrderItem中的product_id參照了Product檔案
  3. 冗餘儲存

    • Order中儲存了user_email,避免每次查詢訂單時都需要查詢使用者資訊
    • OrderItem中儲存了product_name和購買時的price,這樣即使產品資訊後續變更,訂單中的資訊仍保持不變
  4. 索引最佳化

    • User.email上建立唯一索引,確保電子郵件不重複並加速查詢
  5. 狀態管理

    • Order包含status欄位,用於追蹤訂單狀態
    • 使用預設值"pending"初始化訂單狀態

這種建模方式平衡了效能和靈活性:

  • 使用嵌入式檔案(如地址和產品變體)減少查詢次數
  • 使用參照ID處理核心實體間的關係
  • 使用冗餘儲存最佳化常見查詢
  • 保持檔案大小在合理範圍內

在實際應用中,這種模型能夠高效處理常見的電子商務操作,如瀏覽產品、下訂單和查詢訂單歷史。

實際案例:PyPI資料建模

為了展示更複雜的檔案建模,讓我們使用Beanie和Pydantic來建模PyPI(Python Package Index)的資料。

PyPI資料結構分析

PyPI包含以下主要實體:

  1. 套件(Package):Python套件,如requests或pandas
  2. 發布版本(Release):套件的特定版本,如requests 2.28.1
  3. 發布檔案(Release File):特定版本的下載檔案,可能有多種格式(wheel、sdist)
  4. 維護者(Maintainer):負責套件的使用者
  5. 使用者(User):PyPI使用者,可能是維護者或下載者

使用Beanie建模PyPI資料

from beanie import Document, Indexed, Link
from pydantic import BaseModel, HttpUrl
from typing import List, Optional, Dict
from datetime import datetime

# 嵌入式檔案
class ReleaseFile(BaseModel):
    filename: str
    url: HttpUrl
    size: int
    md5_digest: str
    sha256_digest: str
    upload_time: datetime
    python_version: str
    requires_python: Optional[str] = None
    packagetype: str  # 'sdist', 'bdist_wheel' 等

class Release(BaseModel):
    version: str
    released_at: datetime
    summary: Optional[str] = None
    description: Optional[str] = None
    requires_dist: List[str] = []
    files: List[ReleaseFile] = []
    yanked: bool = False
    yanked_reason: Optional[str] = None

class ProjectUrl(BaseModel):
    name: str  # 例如 'Homepage', 'Documentation', 'Source'
    url: HttpUrl

# 主檔案
class User(Document):
    username: Indexed(str, unique=True)
    email: Optional[str] = None
    name: Optional[str] = None
    joined_at: datetime = datetime.now()
    
    class Settings:
        name = "users"

class Package(Document):
    name: Indexed(str, unique=True)
    normalized_name: Indexed(str, unique=True)  # 用於不區分大小寫的查詢
    summary: Optional[str] = None
    description: Optional[str] = None
    author: Optional[str] = None
    author_email: Optional[str] = None
    maintainers: List[str] = []  # 維護者使用者名列表
    project_urls: List[ProjectUrl] = []
    license: Optional[str] = None
    keywords: List[str] = []
    classifiers: List[str] = []
    releases: Dict[str, Release] = {}  # 版本號 -> 發布訊息
    latest_version: Optional[str] = None
    created_at: datetime = datetime.now()
    last_updated: datetime = datetime.now()
    download_count: int = 0
    
    class Settings:
        name = "packages"
        indexes = [
            "keywords",
            "classifiers",
            "maintainers",
            [("normalized_name", "text"), ("summary", "text"), ("description", "text")]  # 全文索引
        ]

class Download(Document):
    package_name: str
    version: str
    file_name: str
    timestamp: datetime = datetime.now()
    ip_address: Optional[str] = None
    user_agent: Optional[str] = None
    country_code: Optional[str] = None
    
    class Settings:
        name = "downloads"
        indexes = [
            "package_name",
            "version",
            "timestamp"
        ]

這個PyPI資料模型展示瞭如何使用MongoDB處理複雜的資料結構:

  1. 嵌入式檔案的層次結構

    • ReleaseFile嵌入在Release
    • Release以字典形式嵌入在Package中,使用版本號作為鍵
    • ProjectUrl作為列表嵌入在Package
  2. 索引策略

    • Package中定義了多種索引,包括單欄位索引和複合全文索引
    • 全文索引允許對套件名稱、摘要和描述進行文字搜尋
    • Download檔案也定義了多個索引,最佳化下載統計查詢
  3. 資料正規化

    • Package.normalized_name用於不區分大小寫的查詢,通常儲存套件名稱的小寫版本
    • 這是MongoDB中常見的模式,因為MongoDB的查詢是區分大小寫的
  4. 關係處理

    • 使用者與套件之間的關係透過Package.maintainers(使用者名列表)表示
    • 這種方式適合多對多關係,與維護者數量通常較少
  5. 時間序列資料

    • Download檔案用於記錄下載事件,形成時間序列資料
    • 這些資料可用於分析趨勢和生成統計資訊
  6. 狀態追蹤

    • Package.latest_versionPackage.last_updated用於追蹤套件的最新狀態
    • Release.yanked用於標記已撤回的版本

這個模型平衡了查詢效能和資料完整性:

  • 將版本資訊嵌入套件檔案中,使常見的"查詢套件及其版本"操作高效
  • 將下載事件分離為獨立檔案,避免套件檔案過大
  • 使用適當的索引最佳化各種查詢場景

查詢範例

使用上述模型,我們可以執行各種查詢:

async def pypi_queries():
    # 查詢特定套件
    requests = await Package.find_one(Package.normalized_name == "requests")
    
    # 取得最新版本訊息
    latest_version = requests.latest_version
    latest_release = requests.releases.get(latest_version)
    
    # 查詢特定維護者的所有套件
    kennethreitz_packages = await Package.find(
        Package.maintainers.contains("kennethreitz")
    ).to_list()
    
    # 全文搜尋
    http_packages = await Package.find(
        {"$text": {"$search": "http client"}}
    ).sort([("$text", {"$meta": "textScore"})]).to_list()
    
    # 查詢最受歡迎的套件(按下載量)
    popular_packages = await Package.find().sort(-Package.download_count).limit(10).to_list()
    
    # 查詢最近更新的套件
    recent_packages = await Package.find().sort(-Package.last_updated).limit(10).to_list()
    
    # 查詢特定分類別的套件
    web_frameworks = await Package.find(
        Package.classifiers.contains("Framework :: Django")
    ).to_list()

這段程式碼展示了使用Beanie模型進行各種PyPI相關查詢的方法:

  1. 精確查詢

    • 使用find_one和精確比對條件查詢特定套件
    • 使用標準化名稱(小寫)進行不區分大小寫的查詢
  2. 嵌入式檔案存取

    • 透過requests.releases.get(latest_version)直接存取嵌入在套件中的版本資訊
    • 這展示了嵌入式檔案的便利性,無需額外查詢
  3. 陣列查詢

    • 使用contains方法查詢陣列欄位(如maintainersclassifiers
    • 這相當於MongoDB的$in運算元
  4. 全文搜尋

    • 使用MongoDB的全文搜尋功能查詢包含特定關鍵字的套件
    • 使用textScore進行相關性排序,將最比對的結果排在前面
  5. 排序與分頁

    • 使用sort方法按特定欄位排序(如下載量或更新時間)
    • 使用limit限制結果數量,實作基本分頁功能
  6. 複合條件

    • 組合多種條件進行查詢,如特定分類別的套件

這些查詢展示了MongoDB和Beanie如何簡化複雜資料的查詢操作。特別是全文搜尋和陣列查詢等功能,在關聯式資料函式庫中實作起來會複雜得多,而在MongoDB中卻非常直覺。

資料更新策略

對於PyPI資料,我們需要考慮以下更新策略:

  1. 新套件發布:
async def add_new_release(package_name: str, version: str, release_data: dict):
    package = await Package.find_one(Package.normalized_name == package_name.lower())
    
    if not package:
        # 建立新套件
        package = Package(
            name=package_name,
            normalized_name=package_name.lower(),
            # 其他欄位...
        )
    
    # 增加新版本
    release = Release(
        version=version,
        released_at=datetime.now(),
        # 從release_data填充其他欄位...
    )
    
    package.releases[version] = release
    
    # 更新最新版本(如果適用)
    if not package.latest_version or version > package.latest_version:
        package.latest_version = version
    
    package.last_updated = datetime.now()
    
    await package.save()
  1. 下載計數更新:
async def record_download(package_name: str, version: str, file_name: str, request_info: dict):
    # 記錄下載
    download = Download(
        package_name=package_name,
        version=version,
        file_name=file_name,
        ip_address=request_info.get("ip"),
        user_agent=request_info.get("user_agent"),
        country_code=request_info.get("country")
    )
    await download.insert()
    
    # 更新套件下載計數(可以使用原子更新)
    await Package.find_one(
        Package.normalized_name == package_name.lower()
    ).update({"$inc": {"download_count": 1}})

這兩個函式展示了PyPI資料的常見更新操作:

  1. 新增發布版本

    • 首先查詢套件,如果不存在則建立新套件
    • 建立新的Release物件,並將其增加到套件的releases字典中
    • 根據版本號比較更新latest_version欄位
    • 更新last_updated時間戳
    • 使用save()方法將變更儲存到資料函式庫
  2. 記錄下載

    • 建立新的Download檔案記錄下載事件,包含詳細資訊如IP、使用者代理等
    • 使用MongoDB的原子更新操作($inc)增加套件的下載計數
    • 這種方式避免了讀取-修改-寫入的競爭條件

這些更新策略展示了MongoDB的幾個重要特性:

  • 檔案的靈活性:可以輕鬆增加新欄位或嵌入式檔案
  • 原子更新操作:可以直接在資料函式庫層面執行計數器增加等操作
  • 嵌入式檔案的更新:可以直接修改嵌入式檔案而無需額外查詢

這種PyPI資料模型展示瞭如何使用Beanie和MongoDB處理複雜的資料關係和查詢需求。