Express.js 中的 Mongoose 鉴别器入门

2025-06-08

Express.js 中的 Mongoose 鉴别器入门

我最近开始用 Express 重写我的 Rails 业余项目what.pm。原因之一是我想提高 JavaScript 水平,另一个原因是 Rails 感觉很神奇,我不喜欢那种用起来不知所措的感觉(“它能用,但我不知道为什么”)。这未必是坏事,而且可以通过深入研究 Rails 的底层机制来解决,但说实话,我对此不感兴趣。

因此,在这次重写中,我想更深入地研究数据存储,不再依赖幕后魔法。这意味着我需要一个合适的数据模型。为了提高灵活性,我想要一个 NoSQL 数据库(以后可能需要添加不同的集合类型!)。我选择了 MongoDB,因为它与 Node 完美兼容,也因为我想尝试 MongooseJS(看了文档,它似乎提供了一个易于理解的抽象层,而且剧透预警:它非常简洁)。

免责声明

我在学习的同时撰写了这篇文章,对文中提到的任何概念的理解可能存在错误。如果你认为我理解有误,请告诉我😃

问题

想象一下,您正在追踪某一年看了哪些电影、书籍和电视节目。这三样东西有几个共同点:它们都有标题和发行日期。然而,它们也彼此不同:一本书有作者,而一部电影有导演。电视节目既没有作者,也没有导演,但是它有季数。那么,您将如何设置您的 Mongoose 模式?您可以轻松地为每种模式(书籍、电影和电视节目)创建三种不同的模式。但是,您会重复自己 — — 在每个模式中,您都会有相同的标题字段和发行日期字段。如果您想添加所有这三种模式都有的另一个字段 — — 例如是否重新观看/重读(“重做”) — — 您必须将该新字段添加到三个不同的文件中。

如果可以扩展某种“基础”架构,并让电影、书籍和电视节目继承自该架构,会怎么样?我不知道该怎么做,但幸运的是,一位同事建议我研究一下 Mongoose 鉴别器。可惜的是,相关文档比较零散,我找不到任何 Express.js 相关的教程/博客文章,所以就尝试写这篇文章来解决这个问题。希望这篇文章能帮助那些想在 Express 应用中集成 Mongoose 鉴别器的人 :)

非 DRY 方式

为了清楚起见,如果没有鉴别器,我们的模式看起来应该是这样的:

> models/book.js

// Define our Book schema
const BookSchema = new mongoose.Schema(
  {
    title: { type: String, required: true },
    author: { type: String, required: true },
    release_date: { type: Date, required: true },
  }
);

// Create a model from our schema
module.exports = mongoose.model('Book', BookSchema);
Enter fullscreen mode Exit fullscreen mode
> models/movie.js

const MovieSchema = new mongoose.Schema(
  {
    title: { type: String, required: true },
    director: { type: String, required: true },
    release_date: { type: Date, required: true },
  }
);

module.exports = mongoose.model('Movie', MovieSchema);
Enter fullscreen mode Exit fullscreen mode
> models/tvshow.js

const Tvshow = new mongoose.Schema(
  {
    title: { type: String, required: true },
    season: { type: Number, required: true },
    release_date: { type: Date, required: true },
  }
);

module.exports = mongoose.model('Tvshow', TvshowSchema);
Enter fullscreen mode Exit fullscreen mode

这没什么问题!但是,就像我之前提到的,如果我们想添加一个新属性,可以这样写:

// signals whether I've already seen or read the item in question
redo: { type: Boolean, required: false } 
Enter fullscreen mode Exit fullscreen mode

我们得在三个单独的文件中添加三次😖。所以,我们来试试别的办法。

我们将创建一个名为 的“主”模式Base,并将 设置为BookMovieTvshow从其继承。以下是我们想要在伪代码中实现的功能:

Base:
    title: { type: String, required: true },
    date_released: { type: Date, required: true },
    redo: { type: Boolean, required: false },

Book:
    Inherit everything from Base, and add the following just for this schema:
    author: { type: String, required: true }

Movie:
    Inherit everything from Base, and add the following just for this schema:
    director: { type: String, required: true }

TV Show:
    Inherit everything from Base, and add the following just for this schema:
    season: { type: Number, required: true }
Enter fullscreen mode Exit fullscreen mode

那么,我们该如何为子模式(书籍、电影、电视节目)赋予这些Base选项呢?换句话说,我们该如何扩展我们的Base?那就来谈谈鉴别器吧。鉴别器是一个函数,model返回一个模型,该模型的模式是基础模式和鉴别器模式的并集。所以,基本上,鉴别器允许我们指定一个键,例如kinditemtype。有了这个键,我们可以将不同的实体(书籍、电影、电视节目……)存储在一个集合中,并且仍然能够区分(badum tsss)这些实体。

那么,让我们设置一下基础架构。同样,其他架构将以此为基础进行扩展。

const baseOptions = {
  discriminatorKey: 'itemtype', // our discriminator key, could be anything
  collection: 'items', // the name of our collection
};

// Our Base schema: these properties will be shared with our "real" schemas
const Base = mongoose.model('Base', new mongoose.Schema({
      title: { type: String, required: true },
      date_added: { type: Date, required: true },
      redo: { type: Boolean, required: false },
    }, baseOptions,
  ),
);

module.exports = mongoose.model('Base');
Enter fullscreen mode Exit fullscreen mode

然后我们可以book.js像这样编辑:

> models/book.js

const Base = require('./base'); // we have to make sure our Book schema is aware of the Base schema

const Book = Base.discriminator('Book', new mongoose.Schema({
    author: { type: String, required: true },
  }),
);

module.exports = mongoose.model('Book');
Enter fullscreen mode Exit fullscreen mode

使用Base.discriminator(),我们告诉 Mongoose 我们想要获取 的属性Base,并添加另一个author属性,仅用于我们的 Book 模式。让我们对 做同样的事情models/movie.js

> models/movie.js

const Base = require('./base');

const Movie = Base.discriminator('Movie', new mongoose.Schema({
    director: { type: String, required: true },
  }),
);

module.exports = mongoose.model('Movie');
Enter fullscreen mode Exit fullscreen mode

tvshow.js

> models/tvshow.js

const Base = require('./base');

const Tvshow = Base.discriminator('Tvshow', new mongoose.Schema({
    season: { type: Number, required: true },
  }),
);

module.exports = mongoose.model('Tvshow');
Enter fullscreen mode Exit fullscreen mode

现在,如果我们为我们的收藏品创建一本新书,新的 Book 实例将显示在我们的 MongoDB 收藏品中,如下所示:

{
    "_id": {
        "$oid": "unique object ID"
    },
    "itemtype": "Book", 
    "author": "Book Author 1",
    "title": "Book Title 1",
    "date_added": {
        "$date": "2018-02-01T00:00:00.000Z"
    },
    "redo": false,
}
Enter fullscreen mode Exit fullscreen mode

很酷吧?现在我们来获取一些数据。下面的示例将返回我们馆藏的书籍数量,以及所有电视节目及其标题和季数:

> controllers/someController.js

const Book = require('../models/book');
const Tvshow = require('../models/tvshow');
const async = require('async');

exports.a_bunch_of_stuff = function(req, res) {
    async.parallel({
        book_count: function (callback) {
            Book.count(callback);
        },
        tvshow_all: function(callback) {
            Tvshow.find({}, 'title season', callback)
        },
    }, function(err, results) {
        res.render('index', { error: err, data: results });
    });
};

Enter fullscreen mode Exit fullscreen mode

总结

通过使用鉴别器,我们有四个包含 DRY 代码的小文件,而不是三个包含大量相同代码的较大模型文件 😎 现在,每当我想添加跨模式共享的新属性时,我只需编辑Base。如果我想添加新模型(也许我应该开始跟踪我去过的音乐会!),我可以在需要时轻松扩展现有属性。

鏂囩珷鏉ユ簮锛�https://dev.to/helenasometimes/getting-started-with-mongoose-discriminators-in-expressjs--22m9
PREV
你的投资组合中应该解决的 5 个问题
NEXT
关于开源开发者,我学到的 5 件事