-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve support for partially denormalized subdocs, including populating denormalized subdocs #10856
Comments
Current workaround I've found: FooSchema
.virtual('$bar', {
ref: 'Bar',
localField: 'bar._id',
foreignField: '_id',
justOne: true,
})
FooSchema.options.toJSON = {
transform(doc, ret) {
if (ret.$bar) {
ret.bar = ret.$bar
delete ret.$bar
}
},
} But this feels kind of hacky |
I think this comes down to two separate issues:
const mongoose = require('mongoose')
const { Schema } = mongoose
//Bar model, has a name property and some other properties that we are interested in
const BarSchema = new Schema({
name: String,
more: String,
another: Number,
})
mongoose.model('Bar', BarSchema)
//Denormalised Bar schema with just the name, for use on the Foo model
const BarNameSchema = new Schema({
_id: {
type: Schema.Types.ObjectId,
ref: 'Bar',
},
name: String,
})
BarNameSchema.virtual('more').get(function() { return this.$locals.more }).set(function(v) { this.$locals.more = v })
BarNameSchema.virtual('another').get(function() { return this.$locals.another }).set(function(v) { this.$locals.another = v })
//Foo model, which contains denormalized bar data (just the name
const FooSchema = new Schema({
something: String,
bar: BarNameSchema,
})
mongoose.model('Foo', FooSchema)
;(async function() {
await mongoose.connect('mongodb://localhost:27017/test')
const barId = new mongoose.Types.ObjectId()
await mongoose.model('Bar').create({ _id: barId, name: 'test', more: 'foo', another: 42 })
const { _id } = await mongoose.model('Foo').create({ something: 'test', bar: { _id: barId, name: 'test' } })
const foo = await mongoose.model('Foo').findOne({_id})
const bar = await mongoose.model('Bar').findOne({_id: foo.bar._id})
foo.bar = bar
// includes `more` and `another`
console.log(foo.bar.toObject({ virtuals: true }))
})()
What do you think @adamreisnz ? |
Hi @vkarpov15 thanks for looking into this.
Ideally, it'd be great if we can work towards better support for denormalised subdocs. The way I would imagine it working is that you could simply populate/inflate the denormalized sub document with additional properties from the original doc, without having to define virtuals or use differently named helper properties. For example, if we have a simple document like this, which has some denormalized data: const doc = {
_id: 1,
name: 'Test',
denormalized: {
_id: 2,
foo: 'Foo'
}
} It'd be great if we can utilise population to "fill this up" like so: await mongoose.model('Something').populate(doc, {
path: 'denormalized',
select: 'foo bar baz',
}) Which would result in: const doc = {
_id: 1,
name: 'Test',
denormalized: {
_id: 2,
foo: 'Foo',
bar: 'Bar',
baz: 'Baz,
}
} Even though So the additional populated properties are appended, and basically the denormalized sub doc schema is replaced with a copy of the full document. I imagine this might be the simplest implementation, rather than trying to append the additional properties onto the schema in real time. Looking at it another way, essentially I am asking for a way to populate something which is already an object (of denormalized data) rather than just an Does that make sense? |
This does make sense. I'm thinking the syntax will look like this: const FooSchema = new Schema({
something: String,
other: Number,
bar: {
type: BarNameSchema,
ref: 'Bar'
}
}) So if you put a The tricky part is what happens if you overwrite certain properties of |
I like that syntax. Yes I figured there'd be some edge cases to figure out. I am inclined to say that an update should not update the original model, only the denormalized schema (and only the properties that are on that schema). After all, you're calling But happy to get more feedback on that. |
@vkarpov15 I'm running into the following error while trying out this new feature. Is that something on our end I'm doing wrong?
Unfortunately this is all I get out of the stack trace, so not 100% sure whether this is triggered when saving a new document but I'll try to dig deeper. |
Does this also work when we're creating a new document as such: const FooSchema = new Schema({
something: String,
other: Number,
bar: {
type: BarNameSchema,
ref: 'Bar'
}
})
const data = {
something: 'test',
other: 2,
bar: {
//fully populated bar item here
}
}
const foo = new Foo(data) It appears any data not present in the BarNameSchema is stripped when we create a new foo object like this. Also setting it manually after document creation appears to have no effect, e.g.:
Is this expected? |
Here is a full test script to see the above 2 cases in action (nothing in here is throwing an error though, so I'm still digging into that): const mongoose = require('mongoose')
const {Schema} = mongoose
const uri = `mongodb://localhost:27017/test`
console.log(`Mongoose version: ${mongoose.version}`)
//Bar model, has a name property and some other properties that we are interested in
const BarSchema = new Schema({
name: String,
more: String,
another: Number,
})
const Bar = mongoose.model('Bar', BarSchema)
//Denormalised Bar schema with just the name, for use on the Foo model
const BarNameSchema = new Schema({
_id: {
type: Schema.Types.ObjectId,
ref: 'Bar',
},
name: String,
})
//Foo model, which contains denormalized bar data (just the name)
const FooSchema = new Schema({
something: String,
other: Number,
bar: {
type: BarNameSchema,
ref: 'Bar',
},
})
const Foo = mongoose.model('Foo', FooSchema)
//Now for some routes I want to be able to load the full `Bar` model, and set it on the `bar` property of a loaded `foo` document
async function test() {
//Connect to database
await mongoose.connect(uri)
const bar = await Bar.create({
name: 'I am Bar',
more: 'With more data',
another: 2,
})
const foo = await Foo.create({
something: 'I am Foo',
other: 1,
bar,
})
console.log(foo)
const fooLookup = await Foo
.findOne({_id: foo._id})
.populate({
path: 'bar',
select: 'name more another',
})
console.log(fooLookup)
const bar2 = await Bar.create({
name: 'I am another Bar',
more: 'With even more data',
another: 3,
})
const foo2 = await Foo.create({
something: 'I am another Foo',
other: 4,
})
foo2.bar = bar2
console.log(foo2)
}
test() Output is:
So neither setting via Thoughts? |
@adamreisnz I got the Re: the |
@vkarpov15 I think I can confirm that all cases are working now, thanks! 👍🏼 |
Thanks @adamreisnz , let me know if you run into any related issues 👍 |
Do you want to request a feature or report a bug?
Request a feature
What is the current behavior?
When a schema is set as
strict
, it is not possible to append different fields to the model OR save those fields in the database.When a schema is set as not
strict
, it is possible to append different fields AND save those fields in the databaseWhat is the proposed behavior?
It would be nice if these two features could be split into two distinct features/flags. I have a use case where I would like to append/set additional properties to a model, and have this returned to the client when using
toJSON
, but I don't want these fields to be saved to the database.Currently, this doesn't seem possible unless I disable strict for the schema altogether, and create a manual pre-save hook to clean up data that I don't want when saving. Needless to say this adds overhead and is prone to bugs if new fields are added.
Use case
The main use case is being able to populate denormalised data easily, which currently is not possible if using
strict
schemas.For example:
So ideally, these two behaviours of
strict
could be separated somehow into a separate flag/setting. However, I am also open to other suggestions that would solve our problem.To be honest this has been a pet peeve of mine for a few years of working with Mongoose now. It's so easy to populate data, and it's so easy to make and manage denormalized data in your database, but it seems the combination of the two still causes unnecessary friction.
Maybe there's a use case for defining a new schema type, which is a "Denormalized" type, which can refer to another model and which will store a select number of fields alongside the parent document, but which can also easily be populated with additional fields for output as JSON/object, while not saving those fields in the database. 🤔
The text was updated successfully, but these errors were encountered: