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);
> 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);
> 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);
这没什么问题!但是,就像我之前提到的,如果我们想添加一个新属性,可以这样写:
// signals whether I've already seen or read the item in question
redo: { type: Boolean, required: false }
我们得在三个单独的文件中添加三次😖。所以,我们来试试别的办法。
我们将创建一个名为 的“主”模式Base
,并将 设置为Book
,Movie
并Tvshow
从其继承。以下是我们想要在伪代码中实现的功能:
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 }
那么,我们该如何为子模式(书籍、电影、电视节目)赋予这些Base
选项呢?换句话说,我们该如何扩展我们的Base
?那就来谈谈鉴别器吧。鉴别器是一个函数,model
它返回一个模型,该模型的模式是基础模式和鉴别器模式的并集。所以,基本上,鉴别器允许我们指定一个键,例如kind
或itemtype
。有了这个键,我们可以将不同的实体(书籍、电影、电视节目……)存储在一个集合中,并且仍然能够区分(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');
然后我们可以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');
使用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');
和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');
现在,如果我们为我们的收藏品创建一本新书,新的 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,
}
很酷吧?现在我们来获取一些数据。下面的示例将返回我们馆藏的书籍数量,以及所有电视节目及其标题和季数:
> 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 });
});
};
总结
通过使用鉴别器,我们有四个包含 DRY 代码的小文件,而不是三个包含大量相同代码的较大模型文件 😎 现在,每当我想添加跨模式共享的新属性时,我只需编辑Base
。如果我想添加新模型(也许我应该开始跟踪我去过的音乐会!),我可以在需要时轻松扩展现有属性。