0Pricing
FastAPI Backend Development Bootcamp · 课时

模式演进与文档迁移

为文档模式进行版本管理,并随着数据模型增长迁移现有集合而无需停机。

模式演进与文档迁移 是 CoddyKit 上的免费 FastAPI Backend Development Bootcamp 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 FastAPI Backend Development Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Why Schemas Evolve

In a FastAPI backend, your data model rarely stays frozen. New features mean new fields, renamed properties, and changed shapes. With a relational database you'd run an ALTER TABLE migration. MongoDB is schemaless at the storage layer, so nothing stops you from writing a new shape next to an old one.

  • Old documents keep their original fields until you touch them.
  • New documents follow the latest model.
  • Your code must tolerate BOTH shapes during the transition.

This lesson shows how Beanie (the async ODM built on Motor + Pydantic) lets you version your documents and migrate collections without downtime.

The Mixed-Shape Problem

Imagine a Product document that started with a single price float. Later you split it into price_cents (int) plus currency. After deploy, your collection holds a mix:

  • Old docs: { "price": 19.99 }
  • New docs: { "price_cents": 1999, "currency": "USD" }

If your Pydantic-backed model declares only the new fields as required, reading an old document raises a validation error. The core skill of schema evolution is making the model forgiving enough to load both, then upgrading data behind the scenes.

Track a Schema Version

The cleanest pattern is to stamp every document with a schema_version integer. Beanie documents are Pydantic models, so you add it as a field with a default. New writes get the current version automatically; old documents that lack the field fall back to version 1 because Pydantic applies the default when the key is missing.

This is the same idea as a plain Python class with a version attribute.

class ProductDoc:
    CURRENT_VERSION = 2

    def __init__(self, data):
        self.schema_version = data.get("schema_version", 1)
        self.data = data

    def needs_migration(self):
        return self.schema_version < self.CURRENT_VERSION


old = ProductDoc({"price": 19.99})
new = ProductDoc({"price_cents": 1999, "schema_version": 2})
print(old.schema_version, old.needs_migration())
print(new.schema_version, new.needs_migration())

A Versioned Beanie Document

Here is the Beanie model. Note three things:

  • schema_version defaults to the current number for new documents.
  • The old price field is kept as Optional so legacy docs still load.
  • New fields are also Optional so a half-migrated collection never crashes a read.

Keeping deprecated fields Optional instead of deleting them outright is the key to zero-downtime: code must read both shapes for the whole transition window.

from typing import Optional
from beanie import Document


class Product(Document):
    schema_version: int = 2
    name: str
    # legacy (v1)
    price: Optional[float] = None
    # current (v2)
    price_cents: Optional[int] = None
    currency: Optional[str] = None

    class Settings:
        name = "products"

Upgrade on Read (Lazy Migration)

The least disruptive strategy is lazy migration: when you load a document, detect the old version and transform it in memory, persisting the upgrade only if you happen to save. This spreads the work across normal traffic with no big batch job.

The pure transform logic is just a function. Test it in isolation before wiring it into Beanie.

def upgrade_product(doc: dict) -> dict:
    version = doc.get("schema_version", 1)
    if version < 2:
        # v1 -> v2: float dollars to integer cents + currency
        if doc.get("price") is not None:
            doc["price_cents"] = round(doc["price"] * 100)
            doc["currency"] = "USD"
        doc.pop("price", None)
        doc["schema_version"] = 2
    return doc


print(upgrade_product({"name": "Pen", "price": 19.99}))
print(upgrade_product({"name": "Pad", "price_cents": 500,
                       "currency": "USD", "schema_version": 2}))

Wiring Lazy Upgrade into FastAPI

In an endpoint, fetch the Beanie document, apply the upgrade if needed, and save it back. Because the upgrade is idempotent (already-v2 docs are untouched), it is safe to run on every read.

This is framework code that depends on Beanie and a running MongoDB, so treat the transform helper as the testable part and keep the I/O thin.

from fastapi import FastAPI, HTTPException

app = FastAPI()


@app.get("/products/{product_id}")
async def get_product(product_id: str):
    product = await Product.get(product_id)
    if product is None:
        raise HTTPException(status_code=404, detail="Not found")
    if product.schema_version < 2 and product.price is not None:
        product.price_cents = round(product.price * 100)
        product.currency = "USD"
        product.price = None
        product.schema_version = 2
        await product.save()
    return product

Eager Migration with a Batch Script

Lazy migration leaves cold documents on the old shape forever. To fully retire field price, run an eager batch migration once: stream every old document, transform it, and write it back. Iterate over a query filter so you only touch unmigrated docs.

Always process in batches and use a filter like schema_version < 2 so a re-run resumes where it left off instead of redoing finished work.

async def migrate_products():
    cursor = Product.find(Product.schema_version < 2)
    migrated = 0
    async for product in cursor:
        if product.price is not None:
            product.price_cents = round(product.price * 100)
            product.currency = "USD"
            product.price = None
        product.schema_version = 2
        await product.save()
        migrated += 1
    print(f"Migrated {migrated} products")

Bulk Update for Speed

Saving documents one by one is fine for thousands of rows, but for millions you want the database to do the work. MongoDB's aggregation-pipeline update can compute the new field server-side in a single command, avoiding a round trip per document.

Beanie exposes this through update with a raw pipeline. Multiplying price by 100 and setting currency happens inside Mongo.

async def bulk_migrate_products():
    await Product.find(Product.schema_version < 2).update(
        [
            {
                "$set": {
                    "price_cents": {
                        "$round": [{"$multiply": ["$price", 100]}, 0]
                    },
                    "currency": "USD",
                    "schema_version": 2,
                }
            },
            {"$unset": "price"},
        ]
    )

Beanie's Built-in Migrations

Beanie ships a migration framework so you don't hand-roll scripts. You write a migration module with a Forward (and optional Backward) class containing functions decorated with @iterative_migration(). Beanie records applied migrations in a migrations collection, just like Alembic does for SQL.

  • Run forward: beanie migrate -uri ... -db ... -p ./migrations
  • Each function receives the old and new document instances.
  • State is tracked so migrations apply exactly once.

This gives you ordered, repeatable, version-controlled schema changes.

from beanie import Document, iterative_migration


class OldProduct(Document):
    price: float

    class Settings:
        name = "products"


class NewProduct(Document):
    price_cents: int
    currency: str

    class Settings:
        name = "products"


class Forward:
    @iterative_migration()
    async def split_price(self, input_document: OldProduct,
                          output_document: NewProduct):
        output_document.price_cents = round(input_document.price * 100)
        output_document.currency = "USD"

Renaming and Removing Fields Safely

Two changes look harmless but cause outages if rushed:

  • Renaming a field: never rename in one step. Add the new field, dual-write both, backfill old docs, then drop the old field in a later release.
  • Removing a field: deploy code that stops reading it first, then run a migration to $unset it.

The rule of thumb is the expand-and-contract pattern: expand the schema to support old and new at once, migrate the data, then contract by removing the old shape once no running code depends on it.

A Default-Backfill Helper

A common evolution is adding a brand-new field that older documents lack. For nullable convenience you give it an Optional default in the model, but to keep queries simple (e.g. filtering on is_active=True) you backfill a concrete default. This pure helper computes the patch dictionary you'd hand to MongoDB.

def backfill_defaults(doc: dict, defaults: dict) -> dict:
    patch = {}
    for key, value in defaults.items():
        if key not in doc or doc[key] is None:
            patch[key] = value
    return patch


existing = {"name": "Widget", "price_cents": 1999}
defaults = {"is_active": True, "currency": "USD"}
print(backfill_defaults(existing, defaults))
# {'is_active': True, 'currency': 'USD'}

Checkpoint: Choosing a Strategy

Test your understanding of zero-downtime migration order.

Recap

You learned how to evolve MongoDB document schemas with Beanie without taking the service down:

  • Version documents with a schema_version field defaulted in the Pydantic model.
  • Keep deprecated fields Optional so mixed-shape collections still load.
  • Lazy migration upgrades documents on read; eager batch or bulk aggregation-pipeline updates fully retire old shapes.
  • Use Beanie's @iterative_migration() framework for ordered, tracked, version-controlled migrations.
  • Apply expand-and-contract: support old + new, migrate data, then remove the old shape only after no code depends on it.

These patterns let your data model grow alongside new features while users keep hitting the API uninterrupted.

常见问题解答

「模式演进与文档迁移」课时是免费的吗?

是的 — 「模式演进与文档迁移」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 FastAPI Backend Development Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。

「模式演进与文档迁移」这节课中我会学到什么?

为文档模式进行版本管理,并随着数据模型增长迁移现有集合而无需停机。 你通过在浏览器中直接运行的动手代码来练习 FastAPI Backend Development Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 FastAPI Backend Development Bootcamp 需要有经验吗?

无需任何先前经验。CoddyKit 上的 FastAPI Backend Development Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「模式演进与文档迁移」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 FastAPI Backend Development Bootcamp 课中编写并运行代码吗?

能。每节 FastAPI Backend Development Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用 Motor 异步访问 MongoDB
  2. 使用 Beanie ODM 建模文档
  3. 聚合流水线与复杂查询
  4. 模式演进与文档迁移
← 返回 FastAPI Backend Development Bootcamp