鉴别器 (Discriminators)

优质
小牛编辑
140浏览
2023-12-01

Discriminator 是一种 schema 继承机制。 他允许你在相同的底层 MongoDB collection 上 使用部分重叠的 schema 建立多个 model。

假设你要在单个 collection 中记录多种 event, 每个 event 都有时间戳字段,但是 click 事件还有 URL 字段, 这时你可以用 model.discriminator() 实现上述要求。 此函数接受 2 个参数,model 名称和 discriminator schema, 返回的 model 结合了原 model 的 schema 和 discriminator schema。


    var options = {discriminatorKey: 'kind'};

    var eventSchema = new mongoose.Schema({time: Date}, options);
    var Event = mongoose.model('Event', eventSchema);

    // ClickedLinkEvent 是一个有 URL 的特别 event
    var ClickedLinkEvent = Event.discriminator('ClickedLink',
      new mongoose.Schema({url: String}, options));

    // 当你创建通用 event,他将没有 URL 字段...
    var genericEvent = new Event({time: Date.now(), url: 'google.com'});
    assert.ok(!genericEvent.url);

    // 但是 ClickedLinkEvent 可以有
    var clickedEvent =
      new ClickedLinkEvent({time: Date.now(), url: 'google.com'});
    assert.ok(clickedEvent.url);
  

现在假设你要创建另一个 discriminator,记录用户注册 event。 SignedUpEvent 实例将跟 通用 events 和 ClickedLinkEvent 实例 一样储存在同一个 collection。


    var event1 = new Event({time: Date.now()});
    var event2 = new ClickedLinkEvent({time: Date.now(), url: 'google.com'});
    var event3 = new SignedUpEvent({time: Date.now(), user: 'testuser'});

    var save = function (doc, callback) {
      doc.save(function (error, doc) {
        callback(error, doc);
      });
    };

    async.map([event1, event2, event3], save, function (error) {

      Event.count({}, function (error, count) {
        assert.equal(count, 3);
      });
    });
  

Mongoose 通过 'discriminator key' 识别两个不同的 discriminator, 这个值默认是 __t 。Mongoose 自动在你的 schema 添加 __t 字段, 记录你的 document 是哪个 discriminator 的实例。


    var event1 = new Event({time: Date.now()});
    var event2 = new ClickedLinkEvent({time: Date.now(), url: 'google.com'});
    var event3 = new SignedUpEvent({time: Date.now(), user: 'testuser'});

    assert.ok(!event1.__t);
    assert.equal(event2.__t, 'ClickedLink');
    assert.equal(event3.__t, 'SignedUp');
  

Discriminator model 的特别之处在于:他们会把 discriminator key 附到 query 上。换句话说,find(), count(), aggregate() 等方法 都能适配 discriminators。


    var event1 = new Event({time: Date.now()});
    var event2 = new ClickedLinkEvent({time: Date.now(), url: 'google.com'});
    var event3 = new SignedUpEvent({time: Date.now(), user: 'testuser'});

    var save = function (doc, callback) {
      doc.save(function (error, doc) {
        callback(error, doc);
      });
    };

    async.map([event1, event2, event3], save, function (error) {

      ClickedLinkEvent.find({}, function (error, docs) {
        assert.equal(docs.length, 1);
        assert.equal(docs[0]._id.toString(), event2._id.toString());
        assert.equal(docs[0].url, 'google.com');
      });
    });
  

Discriminator 会继承他的基础 schema 的 pre 和 post 中间件。 不过,你也可以为 discriminator 添加中间件,这不回影响到基础 schema。


    var options = {discriminatorKey: 'kind'};

    var eventSchema = new mongoose.Schema({time: Date}, options);
    var eventSchemaCalls = 0;
    eventSchema.pre('validate', function (next) {
      ++eventSchemaCalls;
      next();
    });
    var Event = mongoose.model('GenericEvent', eventSchema);

    var clickedLinkSchema = new mongoose.Schema({url: String}, options);
    var clickedSchemaCalls = 0;
    clickedLinkSchema.pre('validate', function (next) {
      ++clickedSchemaCalls;
      next();
    });
    var ClickedLinkEvent = Event.discriminator('ClickedLinkEvent',
      clickedLinkSchema);

    var event1 = new ClickedLinkEvent();
    event1.validate(function() {
      assert.equal(eventSchemaCalls, 1);
      assert.equal(clickedSchemaCalls, 1);

      var generic = new Event();
      generic.validate(function() {
        assert.equal(eventSchemaCalls, 2);
        assert.equal(clickedSchemaCalls, 1);
      });
    });
  

Discriminator 的字段是基础 schema 加 discriminator schema , 并且以 discriminator schema 的字段优先。 但有一个例外,_id 字段。

You can work around this by setting the _id option to false in the discriminator schema as shown below.


    var options = {discriminatorKey: 'kind'};

    // 基础 schema 有字符串格式的 `_id` 字段和 Data 格式的 `time` 字段...
    var eventSchema = new mongoose.Schema({_id: String, time: Date},
      options);
    var Event = mongoose.model('BaseEvent', eventSchema);

    var clickedLinkSchema = new mongoose.Schema({
      url: String,
      time: String
    }, options);
    // 但是 Discriminator schema 有字符串格式的 `time`,并且有
    // 隐式添加的 ObjectId 格式的 `_id`
    assert.ok(clickedLinkSchema.path('_id'));
    assert.equal(clickedLinkSchema.path('_id').instance, 'ObjectID');
    var ClickedLinkEvent = Event.discriminator('ChildEventBad',
      clickedLinkSchema);

    var event1 = new ClickedLinkEvent({ _id: 'custom id', time: '4pm' });
    // 问题来了,clickedLinkSchema 重写了 `time` 路径,但是**没有**
    // 重写 `_id` 路径,因为已经隐式添加(没看懂)
    assert.ok(typeof event1._id === 'string');
    assert.ok(typeof event1.time === 'string');
  

当你使用 Model.create(),Mongoose 会自动帮你适配 discriminator key ~


    var Schema = mongoose.Schema;
    var shapeSchema = new Schema({
      name: String
    }, { discriminatorKey: 'kind' });

    var Shape = db.model('Shape', shapeSchema);

    var Circle = Shape.discriminator('Circle',
      new Schema({ radius: Number }));
    var Square = Shape.discriminator('Square',
      new Schema({ side: Number }));

    var shapes = [
      { name: 'Test' },
      { kind: 'Circle', radius: 5 },
      { kind: 'Square', side: 10 }
    ];
    Shape.create(shapes, function(error, shapes) {
      assert.ifError(error);
      // 重点看这里
      assert.ok(shapes[0] instanceof Shape);
      assert.ok(shapes[1] instanceof Circle);
      assert.equal(shapes[1].radius, 5);
      assert.ok(shapes[2] instanceof Square);
      assert.equal(shapes[2].side, 10);
    });
  

你也可以为嵌套文档数组定义 discriminator。 嵌套 discriminator 的特点是:不同 discriminator 类型储存在相同的文档而不是同一个 mongoDB collection。 换句话说,嵌套 discriminator 让你 在同一个数组储存符合不同 schema 的子文档。

最佳实践:确保你声明了钩子再使用他们。 你不应当在调用 discriminator() 之后调用 pre()post()


    var eventSchema = new Schema({ message: String },
      { discriminatorKey: 'kind', _id: false });

    var batchSchema = new Schema({ events: [eventSchema] });

    // `batchSchema.path('events')` gets the mongoose `DocumentArray`
    var docArray = batchSchema.path('events');

    // 这个 `events` 数组可以包含 2 种不同的 event 类型,
    // 'clicked' event that requires an element id that was clicked...
    var clickedSchema = new Schema({
      element: {
        type: String,
        required: true
      }
    }, { _id: false });
    // 确定在调用 `discriminator()` **之前**
    // 对 `eventSchema` 和 `clickedSchema` 赋予钩子
    var Clicked = docArray.discriminator('Clicked', clickedSchema);

    // ... and a 'purchased' event that requires the product that was purchased.
    var Purchased = docArray.discriminator('Purchased', new Schema({
      product: {
        type: String,
        required: true
      }
    }, { _id: false }));

    var Batch = db.model('EventBatch', batchSchema);

    // Create a new batch of events with different kinds
    var batch = {
      events: [
        { kind: 'Clicked', element: '#hero', message: 'hello' },
        { kind: 'Purchased', product: 'action-figure-1', message: 'world' }
      ]
    };

    Batch.create(batch).
      then(function(doc) {
        assert.equal(doc.events.length, 2);

        assert.equal(doc.events[0].element, '#hero');
        assert.equal(doc.events[0].message, 'hello');
        assert.ok(doc.events[0] instanceof Clicked);

        assert.equal(doc.events[1].product, 'action-figure-1');
        assert.equal(doc.events[1].message, 'world');
        assert.ok(doc.events[1] instanceof Purchased);

        doc.events.push({ kind: 'Purchased', product: 'action-figure-2' });
        return doc.save();
      }).
      then(function(doc) {
        assert.equal(doc.events.length, 3);

        assert.equal(doc.events[2].product, 'action-figure-2');
        assert.ok(doc.events[2] instanceof Purchased);

        done();
      }).
      catch(done);
  

检索嵌套 discriminator


    var singleEventSchema = new Schema({ message: String },
      { discriminatorKey: 'kind', _id: false });

    var eventListSchema = new Schema({ events: [singleEventSchema] });

    var subEventSchema = new Schema({
       sub_events: [singleEventSchema]
    }, { _id: false });

    var SubEvent = subEventSchema.path('sub_events').discriminator('SubEvent', subEventSchema)
    eventListSchema.path('events').discriminator('SubEvent', subEventSchema);

    var Eventlist = db.model('EventList', eventListSchema);

    // Create a new batch of events with different kinds
    var list = {
      events: [
        { kind: 'SubEvent', sub_events: [{kind:'SubEvent', sub_events:[], message:'test1'}], message: 'hello' },
        { kind: 'SubEvent', sub_events: [{kind:'SubEvent', sub_events:[{kind:'SubEvent', sub_events:[], message:'test3'}], message:'test2'}], message: 'world' }
      ]
    };

    Eventlist.create(list).
      then(function(doc) {
        assert.equal(doc.events.length, 2);

        assert.equal(doc.events[0].sub_events[0].message, 'test1');
        assert.equal(doc.events[0].message, 'hello');
        assert.ok(doc.events[0].sub_events[0] instanceof SubEvent);

        assert.equal(doc.events[1].sub_events[0].sub_events[0].message, 'test3');
        assert.equal(doc.events[1].message, 'world');
        assert.ok(doc.events[1].sub_events[0].sub_events[0] instanceof SubEvent);

        doc.events.push({kind:'SubEvent', sub_events:[{kind:'SubEvent', sub_events:[], message:'test4'}], message:'pushed'});
        return doc.save();
      }).
      then(function(doc) {
        assert.equal(doc.events.length, 3);

        assert.equal(doc.events[2].message, 'pushed');
        assert.ok(doc.events[2].sub_events[0] instanceof SubEvent);

        done();
      }).
      catch(done);