0Pricing
MongoDB Academy · 课时

多态模式与模式版本控制模式

学习者将使用类型判别字段设计一个可存放不同结构文档的集合,并为模式添加版本,以支持渐进式迁移。

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

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

Documents With Different Shapes

One of MongoDB's biggest advantages over SQL is that documents in the same collection do not need to share the same fields. This enables the Polymorphic Pattern — storing documents of fundamentally different types in a single collection. Think of a vehicles collection that holds cars, trucks, and motorcycles: all have make and year, but only cars have numberOfDoors and only motorcycles have hasSidecar.

// Polymorphic documents in one 'vehicles' collection
{ _id: ObjectId(), type: 'car',        make: 'Toyota', year: 2022, numberOfDoors: 4, fuelType: 'hybrid' }
{ _id: ObjectId(), type: 'truck',      make: 'Ford',   year: 2021, payloadKg: 1200, hasTrailerHitch: true }
{ _id: ObjectId(), type: 'motorcycle', make: 'Honda',  year: 2023, engineCC: 750, hasSidecar: false }

The Type Discriminator Field

The key to the Polymorphic Pattern is a type discriminator field — a field (commonly named type, kind, or _type) that identifies which subtype a document represents. All application code and indexes can use this field to dispatch behaviour correctly. Index the discriminator field so queries filtering by type execute as IXSCAN rather than COLLSCAN.

// Index the type discriminator
db.vehicles.createIndex({ type: 1 })

// Query only cars
db.vehicles.find({ type: 'car', fuelType: 'hybrid' })

// Query all vehicles regardless of type
db.vehicles.find({ make: 'Toyota' })

Polymorphism in Application Code

Application code handles polymorphic documents with a factory or strategy pattern: inspect the type field and delegate to the appropriate handler class or function. In Node.js/Mongoose, you can use discriminators — a built-in Mongoose feature that defines sub-schemas for each document type within a single collection, automatically setting and reading the discriminator key.

// Mongoose discriminators
const vehicleSchema = new mongoose.Schema({ make: String, year: Number })
const Vehicle = mongoose.model('Vehicle', vehicleSchema)

const Car = Vehicle.discriminator('car', new mongoose.Schema({
  numberOfDoors: Number,
  fuelType: String
}))

const Motorcycle = Vehicle.discriminator('motorcycle', new mongoose.Schema({
  engineCC: Number,
  hasSidecar: Boolean
}))

// Mongoose automatically sets __t discriminator field
await Car.create({ make: 'Toyota', year: 2022, numberOfDoors: 4, fuelType: 'hybrid' })

When to Use Polymorphic vs Separate Collections

Use the Polymorphic Pattern when different subtypes share most of their fields and are queried together frequently. A single vehicles collection makes it easy to ask 'show all Toyota vehicles regardless of type'. Use separate collections when subtypes are almost entirely different, rarely queried together, or have vastly different indexing needs. Polymorphism trades simpler cross-type queries for slightly more complex per-type logic.

The Schema Versioning Pattern

Applications evolve and schemas change, but you cannot stop the world to migrate every document at once. The Schema Versioning Pattern adds a schema_version field to every document. Old documents have version 1 (or no version field, treated as v1), new documents have version 2. Application code checks the version and applies the appropriate transformation, enabling a gradual zero-downtime migration.

// Version 1 document (old shape)
{ _id: ObjectId(), name: 'Alice Smith', phone: '555-1234', schema_version: 1 }

// Version 2 document (new shape — phone normalised)
{
  _id: ObjectId(),
  name: 'Bob Jones',
  contact: {
    phone: '+15551234',  // normalised E.164 format
    email: 'bob@example.com'
  },
  schema_version: 2
}

Reading With Version Awareness

When reading documents, check schema_version and handle each version appropriately in a transform layer. Version-agnostic code calls the transform function and always receives a canonical object. This decouples the application logic from the stored document shape and gives you time to migrate documents in the background without a hard cutover.

function normaliseUser(doc) {
  if (!doc.schema_version || doc.schema_version === 1) {
    // Upgrade v1 shape to canonical v2 shape in memory
    return {
      ...doc,
      contact: { phone: doc.phone, email: null },
      schema_version: 2
    }
  }
  return doc  // already v2
}

const rawDoc = await db.collection('users').findOne({ _id: userId })
const user = normaliseUser(rawDoc)
console.log(user.contact.phone)  // works for both v1 and v2 docs

Lazy Migration: Upgrade on Write

Lazy migration upgrades documents to the new schema as they are naturally accessed. When a document is read and transformed to v2 in memory, write the v2 shape back to the database. Over time, all active documents migrate without a bulk script. Inactive documents can be migrated by a background job. This approach is low-risk — no single large migration to coordinate or roll back.

async function getAndUpgradeUser(userId) {
  const doc = await db.collection('users').findOne({ _id: userId })
  const user = normaliseUser(doc)

  // If doc was v1, write v2 shape back
  if (!doc.schema_version || doc.schema_version < 2) {
    await db.collection('users').replaceOne(
      { _id: userId },
      { ...user, schema_version: 2 }
    )
  }

  return user
}

Bulk Background Migration Script

For faster migration, run a background script that iterates over all v1 documents in batches using a cursor, transforms them, and writes back using bulkWrite(). Process documents in batches (e.g., 1,000 at a time) to avoid overwhelming the server. Run during off-peak hours and include a delay between batches to throttle impact on production traffic.

async function migrateUsers() {
  const cursor = db.collection('users').find(
    { schema_version: { $lt: 2 } },
    { batchSize: 1000 }
  )

  let batch = []
  for await (const doc of cursor) {
    const upgraded = normaliseUser(doc)
    batch.push({
      replaceOne: {
        filter: { _id: doc._id },
        replacement: { ...upgraded, schema_version: 2 }
      }
    })
    if (batch.length === 1000) {
      await db.collection('users').bulkWrite(batch)
      batch = []
    }
  }
  if (batch.length > 0) await db.collection('users').bulkWrite(batch)
}

Combining Polymorphic and Schema Versioning

These two patterns can work together. A polymorphic collection might have both a type discriminator (for subtype dispatch) and a schema_version (for evolutionary migration within each subtype). When documents of type 'car' gain a new required field in v2, the version field tracks which cars have been migrated and which still carry the old shape.

// A polymorphic, versioned document
{
  _id: ObjectId(),
  type: 'car',
  schema_version: 2,
  make: 'Toyota',
  year: 2022,
  numberOfDoors: 4,
  // v2 added fuelType
  fuelType: 'hybrid'
}

Indexing Across Polymorphic Subtypes

Indexes in a polymorphic collection apply across all document types. A partial index (using partialFilterExpression) lets you index a field only for documents of a specific type — for example, indexing numberOfDoors only for car documents. This avoids indexing null/missing values for types that do not have the field, keeping the index small and efficient.

// Partial index: index numberOfDoors only for cars
db.vehicles.createIndex(
  { numberOfDoors: 1 },
  {
    partialFilterExpression: { type: 'car' },
    name: 'car_doors_idx'
  }
)

// Index engineCC only for motorcycles
db.vehicles.createIndex(
  { engineCC: 1 },
  { partialFilterExpression: { type: 'motorcycle' } }
)

Anti-Pattern: Ignoring Schema Version

A common mistake is to update the schema without tracking versions, assuming all documents will be migrated before the new code deploys. In practice, migrations are rarely 100% complete at deploy time. Code that blindly reads a field that may not exist in older documents will throw null pointer errors or produce incorrect results. Always include a version check or a safe fallback when reading potentially-missing fields.

// DANGEROUS: assumes all docs have contact.email
const email = user.contact.email  // TypeError if doc is v1

// SAFE: optional chaining with fallback
const email = user.contact?.email ?? user.email ?? null

Quick Check

Test your understanding of MongoDB & NoSQL Databases concepts from this lesson.

Lesson Recap

In this lesson you learned: the Polymorphic Pattern stores different document subtypes in one collection using a type discriminator field, enabling efficient cross-type queries and Mongoose discriminator support, the Schema Versioning Pattern tracks document shape evolution with a version field, enabling lazy or background migration without downtime, and partial indexes make polymorphic collections efficient by indexing fields only for the types that have them. Next up we cover the Outlier and Tree Structure Patterns.

常见问题解答

「多态模式与模式版本控制模式」课时是免费的吗?

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

「多态模式与模式版本控制模式」这节课中我会学到什么?

学习者将使用类型判别字段设计一个可存放不同结构文档的集合,并为模式添加版本,以支持渐进式迁移。 你通过在浏览器中直接运行的动手代码来练习 MongoDB Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 MongoDB Academy 需要有经验吗?

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

「多态模式与模式版本控制模式」课时需要多长时间?

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

我能在这节 MongoDB Academy 课中编写并运行代码吗?

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

此课程中的所有课时

  1. 桶模式与计算模式
  2. 扩展引用模式与子集模式
  3. 多态模式与模式版本控制模式
  4. 异常值模式与树结构模式
← 返回 MongoDB Academy