다형성 및 스키마 버전 관리 패턴
학습자는 형식 판별자를 사용하여 서로 다른 구조의 문서를 하나의 컬렉션에 저장하도록 설계하고, 점진적인 마이그레이션을 위해 스키마의 버전을 관리합니다.
다형성 및 스키마 버전 관리 패턴은(는) CoddyKit의 무료 MongoDB Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 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 docsLazy 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 ?? nullQuick 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.
자주 묻는 질문
“다형성 및 스키마 버전 관리 패턴” 강의는 무료인가요?
네 — “다형성 및 스키마 버전 관리 패턴” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 MongoDB Academy 강의 전체를 잠금 해제할 수 있습니다. MongoDB Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“다형성 및 스키마 버전 관리 패턴”에서 뭘 배우나요?
학습자는 형식 판별자를 사용하여 서로 다른 구조의 문서를 하나의 컬렉션에 저장하도록 설계하고, 점진적인 마이그레이션을 위해 스키마의 버전을 관리합니다. 브라우저에서 직접 실행하는 실습 코드로 MongoDB Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
MongoDB Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 MongoDB Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“다형성 및 스키마 버전 관리 패턴” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 MongoDB Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 MongoDB Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 버킷 및 계산 패턴
- 확장 참조 및 부분집합 패턴
- 다형성 및 스키마 버전 관리 패턴
- 이상치 및 트리 구조 패턴