Mongoose创建含关联对象的文档报错,如何自动创建URL子文档?
Problem Statement
I've defined two Mongoose models:
DocumentandURLwith the following schemas:const DocumentSchema = new Schema({ text: String, path: String, urls: [{ type: Schema.Types.ObjectId, ref: 'URL' }], }); const URLSchema = new Schema({ url: String, display_url: String, ref: Boolean, });I want to batch create multiple
Documentdocuments using an array like this:const documents = [{ ..., urls: [ ... ] }, ...]; Document.create(documents, (err, docs) => { ... });But I get an error:
urls: Cast to Array failed for value. I guess this is because the URLs I want to add haven't been saved to the database yet, so they don't have corresponding IDs. Is there a way to adjust the Schema configuration so that I can automatically create associated URLs when creating a Document without creating them in advance?
Solution
Absolutely! You don't have to pre-create your URL documents. There are two straightforward approaches to handle this depending on whether you need standalone URL records or not:
Approach 1: Use a Pre-Save Middleware (For Standalone URL Documents)
If you need to manage URLs independently (like querying all URLs or updating them separately from Documents), use a pre('save') hook on the Document schema. This middleware will automatically create all associated URLs before saving the Document, then replace the URL objects in the urls array with their new database IDs.
First, ensure both models are properly defined:
const mongoose = require('mongoose'); const { Schema } = mongoose; // Define URL model first const URLSchema = new Schema({ url: String, display_url: String, ref: Boolean, }); const URL = mongoose.model('URL', URLSchema); // Define Document model const DocumentSchema = new Schema({ text: String, path: String, urls: [{ type: Schema.Types.ObjectId, ref: 'URL' }], });Add the pre-save middleware to handle URL creation:
DocumentSchema.pre('save', async function(next) { // Skip if there are no URLs to process if (!this.urls || this.urls.length === 0) return next(); // Separate URL objects (not yet saved) from existing IDs const newUrlObjects = this.urls.filter(item => typeof item === 'object' && !item._id); if (newUrlObjects.length === 0) return next(); // Bulk create the new URL documents const createdUrls = await URL.create(newUrlObjects); const newUrlIds = createdUrls.map(url => url._id); // Replace the unsaved URL objects with their new IDs, keeping existing IDs intact this.urls = this.urls.map(item => { if (typeof item === 'object' && !item._id) { return newUrlIds.shift(); } return item; }); next(); }); const Document = mongoose.model('Document', DocumentSchema);Now you can batch create Documents with full URL objects in the
urlsarray:const documents = [ { text: 'Project Documentation', path: '/docs/project', urls: [ { url: 'https://github.com', display_url: 'GitHub', ref: true }, { url: 'https://docs.mongodb.com', display_url: 'MongoDB Docs', ref: false } ] }, { text: 'User Guide', path: '/docs/user', urls: [{ url: 'https://support.example.com', display_url: 'Support Portal', ref: true }] } ]; // Use async/await for cleaner error handling try { const createdDocuments = await Document.create(documents); console.log('Successfully created documents and URLs:', createdDocuments); } catch (error) { console.error('Failed to create documents:', error); }
Approach 2: Use Subdocuments (For URLs Tied Directly to Documents)
If URLs don't need to exist independently (they only make sense in the context of a Document), you can define urls as an array of subdocuments. This way, URLs are stored directly inside the Document record, no separate IDs required.
const mongoose = require('mongoose'); const { Schema } = mongoose; // Define URL as a subdocument schema const URLSchema = new Schema({ url: String, display_url: String, ref: Boolean, }); // Define Document schema with URL subdocuments const DocumentSchema = new Schema({ text: String, path: String, urls: [URLSchema], // URLs are embedded directly here }); const Document = mongoose.model('Document', DocumentSchema);
Now batch creation works out of the box with no extra setup:
const documents = [ { text: 'Quick Start Guide', path: '/docs/quickstart', urls: [ { url: 'https://example.com/get-started', display_url: 'Get Started', ref: true }, { url: 'https://example.com/tutorials', display_url: 'Tutorials', ref: false } ] } ]; Document.create(documents) .then(createdDocs => console.log('Created documents:', createdDocs)) .catch(err => console.error('Error creating documents:', err));
Key Takeaways
- Approach 1 is ideal when you need to interact with URL documents separately (e.g., update a URL across multiple Documents).
- Approach 2 is simpler and more performant if URLs are only relevant to their parent Document.
- Always handle async operations with
async/awaitor promises to avoid callback hell and ensure proper error handling.
内容的提问来源于stack exchange,提问作者mdmb

