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
added support for discriminator mapping (closes #1003) #1647
Conversation
ping @whitecolor @pongells just in case you're interested |
At first glance this looks good. Detailed feedback tomorrow. |
Awesome. Yeah, I tried to simplify it and make it feel natural. I did most of this at 1am this morning, so I can probably dig more into the core to find cleaner ways. Looking forward to your opinion :) On Wed, Aug 14, 2013 at 7:35 PM, Aaron Heckmann notifications@github.com
|
Looks nice for the first level docs. Can it work for embedded some docs in some way? |
@whitecolor yeah, I have a use case for that currently, but have done it manually. I based this PR off the other attempts I've seen out there which affect the root level and give you the ability to do "instanceof" on the model/document level which isn't possible on the schema level (unless you create a custom type which casts to objects. That and it seems that most ORMs just do this on the root level. If someone can think of a good way to do both at the same time, I can jump on that.. I was thinking of the following schema level options:
Anyway, I'll wait thinking about that until I get feed back on this current implementation of root level discriminator mapping. |
Looks nice. By the way, I found this plugin which seems to do something similar mongoose-schema-extend. |
@pongells yeah I checked that out and got some of the motivation from it for developing this version. I wanted to be able to do more of a natural way of doing this by having the ability to do the following: var FooSchema = new Schema();
var BarSchema = new Schema();
util.inherits(BarSchema, FooSchema);
var Foo = mongoose.model('Foo', FooSchema);
var Bar = mongoose.model('Bar', BarSchema); // this would create a discriminator type 'Bar' automatically but you can't inherit that way, also, most people may want a base schema to inherit from but not live in the same collection, which is why I demonstrated in my example of this PR by extending Schema to create a "base" schema. The "Model" class already has a .model() method and a prototype.model() method, which could replace the .discriminator() method I created which would create a discriminator type if a Schema is passed into it. It's current implementation just shortcuts fetching of a Model which is weird :P So we could create an extend function in the Schema, but that seems misleading b/c someone may just want to literally extend the schema to copy it to create a separate and unrelated model: i.e) var BaseSchema = new Schema({ createdAt: Date, updatedAt: Date, deletedAt: Date });
var UserSchema = BaseSchema.extend({ firstName: String });
var PostSchema = BaseSchema.extend({ content: String }); So naturally I'd think of extending something as a factory method for inheriting and creating an instance of what I extend.. so an extend method may end up confusing people. Also, the route I took, you don't have to extend anything. In fact all objects can be completely unrelated to each other and live in the same collection. So my current stance is:
Thoughts? |
@@ -154,6 +155,7 @@ Schema.prototype.defaultOptions = function (options) { | |||
, bufferCommands: true | |||
, capped: false // { size, max, autoIndexId } | |||
, versionKey: '__v' | |||
, discriminatorKey: null // discriminator keys are '__t' when left null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets set this to __t
by default and remove the check for existence on https://github.com/LearnBoost/mongoose/pull/1647/files#L0R641
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
silly, that's what I had from the get-go but thought you wouldn't like it since it's not a discriminator schema :P changing now.
It might be nice if
was true. We might be able to do some |
@aheckmann yeah, i agree. i'll look into that later. if you like the direction of this, we can keep this as a WIP obviously to get feedback and perfect this way of doing discriminator types. |
assert.ifError(err); | ||
conversionEvent.save(function(err) { | ||
assert.ifError(err); | ||
BaseEvent.find({}, function(err, docs) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should add .sort('name')
to ensure docs return in consistent order (varies on linux)
Really like the direction of this. I want to play around with it to get a user experience opinion and some more feedback yet. |
done(); | ||
}); | ||
|
||
it('throws error when discriminator when discriminator with taken name is added', function(done) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/when discriminator with/with/
I agree with that statement. Just writing the tests made me like the feeling of the other attempted methods so far. I'd like to hear input. |
@aheckmann is this fine? j/mongoose@31b958a i'm assuming merge gets called on all query preparations see the regression test (j/mongoose@2c2d591) i've added to show that findOneAndUpdate() works |
Alright, I'm going to call it a night for now.. I'm assuming you work in the NY 10gen offices, so you're going to be up early (earlier than us lazy west coasters!). Here's some food for thought: This implementation doesn't force schemes to be extended / inherited... meaning, you can create a "discriminator" and they have absolutely nothing to do with the "base" model's schema. They just share the same collection and that's it. However, most ORMs use discriminator mapping alongside inheritance. For example, using PHP's Doctrine MongoDB ODM (https://github.com/doctrine/mongodb-odm) which I contribute to: <?php
/**
* @Document()
* @DiscriminatorField(fieldName="__t")
* @DiscriminatorMap({"person"="Person", "employee"="Employee"})
*/
class Person
{
// ...
}
/**
* @Document()
*/
class Employee extends Person
{
// ...
} So, should we do the same? I'm starting to think that it makes sense to do so b/c that's the main use-case of it.. and if you don't want to have any shared schema properties, the root Schema can be empty... Then if we want to tackle the model inheritance, the hydrated document will always |
It would be nice if |
West-coasters BTW :) |
haha, I'll try hacking on the instanceof part. but with that said, do you think schemas should always inherit the root schema's properties or keep them unrelated? also, what do you think of the method discriminator? what about "type", "embed" or putting this logic in the "model" method?.. or something else? Here's some sudo-coding of options: 1.) Current implementation (does not inherit base schema) var TransportationSchema = new Schema({ maxSpeed: Number });
var CarSchema = new Schema();
var BusSchema = new Schema();
var Transportation = mongoose.model('Transportation', TransportationSchema);
var Car = Transportation.discriminator('Car', CarSchema);
var Bus = Transportation.discriminator('Car', BusSchema);
// .. or
var Car = Transportation.model('Car', CarSchema);
// .. or
var Bus = Transportation.type('Car', BusSchema);
var ferrari = new Car(); // does not inherit from base schema and does not have maxSpeed field
// .. but users can do:
function BaseSchema() {}
util.inherits(BaseSchema, Schema);
var CarSchema = new BaseSchema();
// another note:
console.log(ferrari instanceof Transportation); // ???
// leads to "sub-option" of 1:
// * my personal opinion is that this only makes sense if schemas are related *
// a.) Should `(ferrari instanceof Transportation` === true if schemas aren't related?
// b.) Should `(ferrari instanceof Transportation` === false if schemas aren't related? 2.) Inherits base schema under the hood: var TransportationSchema = new Schema({ maxSpeed: Number });
var CarSchema = new Schema();
var BusSchema = new Schema();
var Transportation = mongoose.model('Transportation', TransportationSchema);
var Car = Transportation.discriminator('Car', CarSchema);
var Bus = Transportation.discriminator('Car', BusSchema);
// .. or
var Car = Transportation.model('Car', CarSchema);
// .. or
var Bus = Transportation.type('Car', BusSchema);
var ferrari = new Car({ maxSpeed: 340 }); // automatically inherits base schema behind the scenes
console.log(ferrari instanceof Transportation); // true 3.) Move logic to Schema (like https://github.com/briankircho/mongoose-schema-extend) var TransportationSchema = new Schema({ maxSpeed: Number });
var CarSchema = TransportationSchema.extend();
var BusSchema = TransportationSchema.extend();
var Transportation = mongoose.model('Transportation', TransportationSchema);
var Car = Transportation.discriminator('Car', CarSchema);
var Bus = Transportation.discriminator('Car', BusSchema);
// .. or
var Car = Transportation.model('Car', CarSchema);
// .. or
var Bus = Transportation.type('Car', BusSchema);
var ferrari = new Car({ maxSpeed: 340 }); // Schema.prototype.extend does this automatically
console.log(ferrari instanceof Transportation); // true 4.) Combination of extend and others // create extend function or similar that ONLY extends and has nothing to do with discriminator mapping
var TransportationSchema = new Schema({ maxSpeed: Number });
var CarSchema = TransportationSchema.extend();
var BusSchema = TransportationSchema.extend();
// or perhaps add Schema.inherits
var TransportationSchema = new Schema({ maxSpeed: Number });
var CarSchema = new Schema();
Schema.inherits(CarSchema, TransportationSchema);
// or try to make this work: (?? i haven't looked into this yet ??)
util.inherits(CarSchema, TransportationSchema)
// then using a `Transportation.discriminator` creates the discriminator mapping
var Transportation = mongoose.model('Transportation', TransportationSchema);
var Car = Transportation.discriminator('Car', CarSchema);
var Bus = Transportation.discriminator('Car', BusSchema); 4.) Or... just vomiting stuff out (seems kinda cool though) var CarSchema = new Schema();
var BusSchema = new Schema();
var TransportationSchema = new Schema(
CarSchema,
BusSchema,
{ discriminatorKey: '_type', discriminatorMap: { Car: CarSchema, Bus, BusSchema }}
);
var Transportation = mongoose.model('Transportation', TransportationSchema);
var Car = Transportation.type('Car');
var Bus = Transportation.type('Bus'); All the options that have extend / inheritance in the schema (minus auto discriminator mapping) are unrelated to this PR and should be separate. |
I personally like option 2. It seems very straight forward.
I'd like having the ".model" - it makes it clear Car is a model that comes from Transportation. I also think inheriting the super-schema is good.. since Car comes from Transportation, it should have a maxSpeed by default. |
@pongells yeah, I've been leaning towards that also. I haven't implemented the automatic inheritance yet. We're going to have to make sure we adopt the methods, statics, validations, etc, from the base schema. I'm also fond of HOWEVER, I'm not sure if anyone is currently abusing Model.model (see https://github.com/LearnBoost/mongoose/blob/master/lib/model.js#L602) It's basically a shortcut for getting a model from the same connection instance... I'd rather force Model.model to be for discriminator types instead of shortcutting this. For example: var UserSchema = new Schema({ /** ... */ });
var TransportationSchema = new Schema({ /** ... */ });
var CarSchema = new Schema({ /** ... */ });
var UserSchema = mongoose.model('User', UserSchema);
var Transportation = mongoose.model('Transportation', TransportationSchema);
Transportation.model('Car', CarSchema);
var Car = Transportation.model('Car');
var User = Transportation.model('User'); // should throw exception in my opinion If my example above can be implemented, then discriminators would suddenly become very clear as to what's happening. It's clean. Thoughts @aheckmann @pongells ? |
Woah, so atm:
|
Yeah, as long as they share the same connection... lol. |
Well, I'd say I agree with you. However, I am quite new around here.. maybe there is a reason for that Model.model.. By the way, how would this work? var TransportationSchema = new Schema({ maxSpeed: Number });
var CarSchema = new Schema({});
var Transportation = mongoose.model('Transportation', TransportationSchema);
var Car = Transportation.model('Car', CarSchema);
var ParkSchema = new Schema({
transportations: [TransportationSchema]
}) |
Are there any documents on how to use this feature other than this thread? I desperately need schema inheritance, but am not sure where to start. |
Check out one of the latest PRs. On Mon, Nov 25, 2013 at 12:24 PM, Nik Martin notifications@github.com
|
Got it! And I hate jade too. |
Having some troubles with schema inheritance with discriminators and trying to populate using 3.8 - http://stackoverflow.com/questions/20430484/mongoose-schema-inheritance-and-model-populate |
replied on SO for that sweet sweet karma On Fri, Dec 6, 2013 at 12:37 PM, pmgration notifications@github.com wrote:
|
I notice the following error: Uncaught Error: Discriminator options are not customizable (except toJSON & toObject) When settings auto index to false: var ModelSchema = new Schema({...}); Removing the autoIndex:false line prevents the error. |
Troubling facts... Let's say we have done: var Transportation = mongoose.model('Transportation', TransportationSchema),
Car = Transportation.discriminator('Car', CarSchema);
or
This is inconsistent and one would expect at least 1) to work (and possibly 2) |
More troubling facts.... Let's says we have done:
then
|
Is current documentation available for these features...? I am not finding any mention here: http://mongoosejs.com/docs/ |
Unfortunately discriminators aren't really covered in the docs right now @techjeffharris . |
Thanks, @vkarpov15. If you were to use the node.js stability index, about where would the polymorphic schema API be? I would like to implement core-features if the API is relatively stable (in which case, I'll help with the documentation--albeit later this month), but if its still going to be a while, I'll just use the mongoose-schema-extend module in the interim. Thanks for all the hard work, everyone! |
Hi @techjeffharris, I unfortunately am not familiar enough with discriminators right now to make such an estimate. However, I'm reasonably certain that the API won't change in the next major version. |
Still no docs? |
+1 for adding some documentation on this. Seems like a great feature but I don't feel confident using it until there's some docs. |
+1 for documentation too. It is a great feature but the lack of documentation make it (sadly) useless... |
Example in the documentation working nicely: Person Boss |
@hartca, if you are referring to my comment of May 30, you need 2 discriminators to check my point. Add NoBoss, and you will realize that Boss is an instance of NoBoss and vice versa (unless it has been corrected in Mongoose since May 30). |
What if we want to extend more than once ? |
@guiltry Querying Human won't get Woman in that case, sadly. :[ You only really get one level of inheritance because discriminators only hold one value. Maybe there could be a way to have functionality like that by having multiple descriptors, or maybe having paths for the descriptor and using regex. (regex is super slow though) |
Yeah, I've tried it. |
If the discriminator was an array, we could store all of the types that it matches against, so Woman in your example might have a discriminator that would look like ["thing","human","woman"] Then you could query for any |
I have a base schema called User, and two other schemas that inherit from User, called Person and Company. |
For anyone else that might need to do something similar, use the Model.hydrate method. The way to use it is as follows(pseudocode): Now you have casted the user into a person. |
👍 |
Well done, @j! I just get an issue using discriminators: If you have some references in your child model, you can't populate it using the base model: Child: new BaseSchema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
}); This works as expected: Child.find().populate('user'); But this does not populate referenced users: Base.find().populate('user'); |
@ericsaboia can you open up a new issue for that? Makes it much easier to track :) |
@vkarpov15 sure. |
So I gave discriminator mapping a shot. After coding this out, it feels pretty natural and seems to fit well with the current API of mongoose.
Anyway, a quick example of how it works:
Then assuming we have inserted a single document for each event in the order of Base, Impression, and Conversion:
Lastly, if you take a the specific discriminator type model, it will query for only the documents of that type:
Just one thing before moving too far forward: I introduced a new method "discriminator" to be clear of what you're doing. It creates a model in the same connection and returns it and does some other stuff behind the scenes. We could also make the static method Model.model do the same (or alias / remove discriminator method) if the community would rather have the following instead:
Another note, in my example, I'm using util.inherit to simply show that you can have a base model. This is completely optional. You can technically store whatever schema's you want and they don't have to inherit from one another. I'm also +1 for creating an extend method in Schema to shortcut schema inheritance.
Anyway, here you go! :) 🏄
Todos
Off topic:
After this, we can discuss adding "discriminator" style mapping to embedded documents. They are sort of unrelated in the scope of this PR... Unless someone can think of a way to combine the two in a mongoose style fashion :P