mongoose vs sequelize vs bookshelf vs waterline
Node.js 后端 ORM 与 ODM 技术选型深度对比
mongoosesequelizebookshelfwaterline类似的npm包:

Node.js 后端 ORM 与 ODM 技术选型深度对比

bookshelfmongoosesequelizewaterline 都是 Node.js 生态中用于简化数据库操作的对象映射工具。mongoose 是 MongoDB 的 ODM(对象文档映射),专注于文档型数据库;sequelize 是功能强大的 SQL ORM,支持 Postgres、MySQL 等关系型数据库;bookshelf 是基于 Knex.js 构建的 SQL ORM,强调链式查询;waterline 则采用适配器模式,旨在统一不同数据库的访问接口,常与 Sails.js 框架配合使用。这些库帮助开发者以面向对象的方式操作数据,处理模型定义、关系关联及数据验证。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
mongoose4,348,34527,4622.05 MB1995 天前MIT
sequelize2,618,20330,3492.91 MB1,0111 天前MIT
bookshelf57,1946,363-2386 年前MIT
waterline28,5185,4081.3 MB33-MIT

Node.js 数据层架构:Bookshelf vs Mongoose vs Sequelize vs Waterline

在 Node.js 后端开发中,选择合适的数据访问层(Data Access Layer)是架构决策的关键一环。bookshelfmongoosesequelizewaterline 代表了四种不同的设计哲学:从专一的 ODM 到通用的 SQL ORM,再到适配器模式。作为架构师,我们需要透过表面的 API 差异,深入理解它们对数据一致性、开发效率及长期维护性的影响。

🗄️ 核心定位:SQL 与 NoSQL 的分野

选择这些库的第一步并非看语法,而是看数据库类型。这决定了你的数据模型是严格的表格关系,还是灵活的文档结构。

mongoose 专为 MongoDB 设计,是 ODM(Object Document Mapper)。它强制你在 JavaScript 对象和 BSON 文档之间建立映射。

// mongoose: 定义 Schema
const userSchema = new mongoose.Schema({
  name: String,
  email: { type: String, unique: true }
});
const User = mongoose.model('User', userSchema);

sequelize 是纯粹的 SQL ORM,支持 PostgreSQL、MySQL 等。它强调表结构和关系完整性。

// sequelize: 定义 Model
const User = sequelize.define('User', {
  name: DataTypes.STRING,
  email: { type: DataTypes.STRING, unique: true }
});

bookshelf 基于 Knex.js,专注于 SQL 数据库。它更像一个带有模型概念的查询构建器。

// bookshelf: 定义 Model
const User = bookshelf.Model.extend({
  tableName: 'users',
  hasTimestamps: true
});

waterline 采用适配器模式,理论上支持多种数据库(SQL 和 NoSQL),通过配置切换底层实现。

// waterline: 定义 Collection (在 Sails.js 中)
// models/User.js
module.exports = {
  attributes: {
    name: 'string',
    email: { type: 'string', unique: true }
  }
};

🔍 查询语法:链式调用 vs 对象配置

查询数据的体验直接影响日常开发效率。这四个库在“如何表达查询意图”上有着显著差异。

mongoose 使用链式调用构建查询,支持回调、Promise 和 async/await,语法接近 MongoDB 原生驱动。

// mongoose: 链式查询
const user = await User.findOne({ email: 'test@example.com' })
  .select('-password')
  .populate('posts')
  .exec();

sequelize 提供两种风格:基于对象的查找配置(推荐)和链式方法。对象配置更易于阅读和维护。

// sequelize: 对象配置查询
const user = await User.findOne({
  where: { email: 'test@example.com' },
  attributes: { exclude: ['password'] },
  include: [{ model: Post }]
});

bookshelf 深度依赖 Knex.js 的链式风格,对于熟悉 SQL 的开发者来说非常直观。

// bookshelf: 基于 Knex 的链式查询
const user = await User
  .where({ email: 'test@example.com' })
  .query('select', 'name', 'email')
  .fetch({ withRelated: ['posts'] });

waterline 使用类似 NoSQL 的语法,即使底层是 SQL。它试图统一不同数据库的查询方式。

// waterline: 统一查询语法
const user = await User.findOne({
  where: { email: 'test@example.com' },
  select: ['name', 'email'],
  populate: 'posts'
});

🔗 关系处理:显式关联 vs 隐式引用

处理表连接(Join)或文档引用(Populate)是 ORM 的核心价值所在。

mongoose 通过 ref 字段定义引用,使用 populate 在查询时自动填充关联数据。

// mongoose: 定义关联
const PostSchema = new Schema({
  author: { type: Schema.Types.ObjectId, ref: 'User' }
});
// 查询时填充
Post.find().populate('author');

sequelize 在模型定义时明确声明 belongsTohasMany,查询时使用 include 进行 SQL JOIN。

// sequelize: 定义关联
User.hasMany(Post);
Post.belongsTo(User);
// 查询时包含
User.findAll({ include: [Post] });

bookshelf 使用 hasManybelongsTo 方法定义关系,获取数据时需显式调用 loadwithRelated

// bookshelf: 定义关联
class User extends bookshelf.Model {
  posts() { return this.hasMany(Post); }
}
// 加载关联
User.forge({ id: 1 }).fetch({ withRelated: ['posts'] });

waterline 使用 collection 属性定义关联,语法简洁,底层自动处理 JOIN 或多次查询。

// waterline: 定义关联
// models/Post.js
module.exports = {
  attributes: {
    author: { model: 'User' }
  }
};
// 查询时填充
Post.find().populate('author');

🛠️ 迁移与生命周期:内置工具 vs 外部依赖

生产环境必须处理数据库架构变更(Migration)和数据验证(Validation)。

sequelize 内置了强大的 CLI 工具,支持生成迁移文件和种子数据,是四个库中迁移管理最完善的。

// sequelize: 运行迁移
// npx sequelize-cli db:migrate
// 迁移文件示例
module.exports = {
  up: (queryInterface, Sequelize) => queryInterface.addColumn('Users', 'age', Sequelize.INTEGER),
  down: (queryInterface, Sequelize) => queryInterface.removeColumn('Users', 'age')
};

mongoose 没有内置迁移工具,通常依赖第三方库(如 mongoose-migrate)或自定义脚本。验证逻辑在 Schema 中定义。

// mongoose: Schema 内验证
const userSchema = new Schema({
  age: { type: Number, min: 18 }
});
// 保存时自动验证
await new User({ age: 15 }).save(); // 抛出验证错误

bookshelf 本身不包含迁移功能,完全依赖 Knex.js 的迁移系统。验证需配合外部库(如 bookshelf-validator)。

// bookshelf: 依赖 Knex 迁移
// knex migrate:make create_users
// 验证需手动实现或引入插件

waterline 在 Sails.js 环境中通过 migrate 配置自动管理架构(如 alterdrop),但在独立使用时迁移管理较为复杂。

// waterline: Sails 配置迁移策略
// config/models.js
module.exports.models = {
  migrate: 'alter' // auto, alter, drop, safe
};

⚠️ 维护状态与风险提示

这是架构决策中最容易被忽视但最致命的一点。

  • mongoosesequelize 目前处于活跃维护状态,社区庞大,文档更新及时,是生产环境的安全选择。
  • bookshelf 的更新频率显著降低,社区讨论热度下降。虽然稳定,但新特性支持(如 TypeScript 类型定义)滞后。不建议在新项目中作为首选,除非团队对 Knex 有极强依赖。
  • waterline 主要作为 Sails.js 的一部分存在,独立使用场景较少。其迭代速度较慢,且生态绑定严重。若不使用 Sails.js 框架,强烈建议避免使用

💡 架构师建议

特性mongoosesequelizebookshelfwaterline
数据库MongoDBSQL (PG/MySQL)SQL (via Knex)多数据库适配
类型支持良好 (TS)优秀 (TS)一般一般
迁移工具需第三方内置强大 CLI依赖 KnexSails 内置
维护状态🟢 活跃🟢 活跃🟡 缓慢🟡 缓慢
推荐场景NoSQL 项目企业级 SQL 项目遗留 Knex 项目Sails.js 项目

最终结论

对于现代 Node.js 架构,sequelize(SQL 场景)和 mongoose(NoSQL 场景)是唯二推荐用于新生产环境的库。它们提供了必要的稳定性、类型支持和社区保障。

bookshelfwaterline 属于特定历史时期的优秀工具,但在当前的微服务和 Serverless 架构趋势下,它们的抽象层显得过于厚重或维护不足。除非你正在维护基于这些技术栈的遗留系统,否则应将技术债务控制在最小范围,优先选择更活跃的替代方案。

如何选择: mongoose vs sequelize vs bookshelf vs waterline

  • mongoose:

    如果你的项目使用 MongoDB,mongoose 是不二之选。它提供了丰富的 Schema 定义、中间件钩子和强大的聚合管道支持。适合需要灵活文档结构、快速迭代且不需要严格事务关系的场景,如内容管理系统或实时分析应用。

  • sequelize:

    如果你需要操作关系型数据库(如 PostgreSQL 或 MySQL),且看重类型安全、迁移管理和复杂事务处理,选择 sequelize。它拥有成熟的生态系统和详细的文档,适合企业级应用和对数据一致性要求较高的场景。

  • bookshelf:

    仅在你维护遗留系统或极度依赖 Knex.js 查询构建器时考虑 bookshelf。由于社区活跃度下降且缺乏新特性,新项目不建议使用。它适合那些已经深度集成 Knex 且不需要 ORM 高级特性的轻量级 SQL 项目。

  • waterline:

    只有在使用 Sails.js 框架或需要一套代码同时适配多种不同数据库(如同时支持 MySQL 和 MongoDB)的特定场景下才考虑 waterline。由于其更新缓慢且生态绑定较强,通用 Node.js 项目通常不推荐作为首选。

mongoose的README

Mongoose

Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment. Mongoose supports Node.js and Deno (alpha).

Build Status NPM version Deno version Deno popularity

npm

Documentation

The official documentation website is mongoosejs.com.

Mongoose 9.0.0 was released on November 21, 2025. You can find more details on backwards breaking changes in 9.0.0 on our docs site.

Support

Plugins

Check out the plugins search site to see hundreds of related modules from the community. Next, learn how to write your own plugin from the docs or this blog post.

Contributors

Pull requests are always welcome! Please base pull requests against the master branch and follow the contributing guide.

If your pull requests makes documentation changes, please do not modify any .html files. The .html files are compiled code, so please make your changes in docs/*.pug, lib/*.js, or test/docs/*.js.

View all 400+ contributors.

Installation

First install Node.js and MongoDB. Then:

Then install the mongoose package using your preferred package manager:

Using npm

npm install mongoose

Using pnpm

pnpm add mongoose

Using Yarn

yarn add mongoose

Using Bun

bun add mongoose

Mongoose 6.8.0 also includes alpha support for Deno.

Importing

// Using Node.js `require()`
const mongoose = require('mongoose');

// Using ES6 imports
import mongoose from 'mongoose';

Or, using Deno's createRequire() for CommonJS support as follows.

import { createRequire } from 'https://deno.land/std@0.177.0/node/module.ts';
const require = createRequire(import.meta.url);

const mongoose = require('mongoose');

mongoose.connect('mongodb://127.0.0.1:27017/test')
  .then(() => console.log('Connected!'));

You can then run the above script using the following.

deno run --allow-net --allow-read --allow-sys --allow-env mongoose-test.js

Mongoose for Enterprise

Available as part of the Tidelift Subscription

The maintainers of mongoose and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. Learn more.

Overview

Connecting to MongoDB

First, we need to define a connection. If your app uses only one database, you should use mongoose.connect. If you need to create additional connections, use mongoose.createConnection.

Both connect and createConnection take a mongodb:// URI, or the parameters host, database, port, options.

await mongoose.connect('mongodb://127.0.0.1/my_database');

Once connected, the open event is fired on the Connection instance. If you're using mongoose.connect, the Connection is mongoose.connection. Otherwise, mongoose.createConnection return value is a Connection.

Note: If the local connection fails then try using 127.0.0.1 instead of localhost. Sometimes issues may arise when the local hostname has been changed.

Important! Mongoose buffers all the commands until it's connected to the database. This means that you don't have to wait until it connects to MongoDB in order to define models, run queries, etc.

Defining a Model

Models are defined through the Schema interface.

const Schema = mongoose.Schema;
const ObjectId = Schema.ObjectId;

const BlogPost = new Schema({
  author: ObjectId,
  title: String,
  body: String,
  date: Date
});

Aside from defining the structure of your documents and the types of data you're storing, a Schema handles the definition of:

The following example shows some of these features:

const Comment = new Schema({
  name: { type: String, default: 'hahaha' },
  age: { type: Number, min: 18, index: true },
  bio: { type: String, match: /[a-z]/ },
  date: { type: Date, default: Date.now },
  buff: Buffer
});

// a setter
Comment.path('name').set(function(v) {
  return capitalize(v);
});

// middleware
Comment.pre('save', function(next) {
  notify(this.get('email'));
  next();
});

Take a look at the example in examples/schema/schema.js for an end-to-end example of a typical setup.

Accessing a Model

Once we define a model through mongoose.model('ModelName', mySchema), we can access it through the same function

const MyModel = mongoose.model('ModelName');

Or just do it all at once

const MyModel = mongoose.model('ModelName', mySchema);

The first argument is the singular name of the collection your model is for. Mongoose automatically looks for the plural version of your model name. For example, if you use

const MyModel = mongoose.model('Ticket', mySchema);

Then MyModel will use the tickets collection, not the ticket collection. For more details read the model docs.

Once we have our model, we can then instantiate it, and save it:

const instance = new MyModel();
instance.my.key = 'hello';
await instance.save();

Or we can find documents from the same collection

await MyModel.find({});

You can also findOne, findById, update, etc.

const instance = await MyModel.findOne({ /* ... */ });
console.log(instance.my.key); // 'hello'

For more details check out the docs.

Important! If you opened a separate connection using mongoose.createConnection() but attempt to access the model through mongoose.model('ModelName') it will not work as expected since it is not hooked up to an active db connection. In this case access your model through the connection you created:

const conn = mongoose.createConnection('your connection string');
const MyModel = conn.model('ModelName', schema);
const m = new MyModel();
await m.save(); // works

vs

const conn = mongoose.createConnection('your connection string');
const MyModel = mongoose.model('ModelName', schema);
const m = new MyModel();
await m.save(); // does not work b/c the default connection object was never connected

Embedded Documents

In the first example snippet, we defined a key in the Schema that looks like:

comments: [Comment]

Where Comment is a Schema we created. This means that creating embedded documents is as simple as:

// retrieve my model
const BlogPost = mongoose.model('BlogPost');

// create a blog post
const post = new BlogPost();

// create a comment
post.comments.push({ title: 'My comment' });

await post.save();

The same goes for removing them:

const post = await BlogPost.findById(myId);
post.comments[0].deleteOne();
await post.save();

Embedded documents enjoy all the same features as your models. Defaults, validators, middleware.

Middleware

See the docs page.

Intercepting and mutating method arguments

You can intercept method arguments via middleware.

For example, this would allow you to broadcast changes about your Documents every time someone sets a path in your Document to a new value:

schema.pre('set', function(next, path, val, typel) {
  // `this` is the current Document
  this.emit('set', path, val);

  // Pass control to the next pre
  next();
});

Moreover, you can mutate the incoming method arguments so that subsequent middleware see different values for those arguments. To do so, just pass the new values to next:

schema.pre(method, function firstPre(next, methodArg1, methodArg2) {
  // Mutate methodArg1
  next('altered-' + methodArg1.toString(), methodArg2);
});

// pre declaration is chainable
schema.pre(method, function secondPre(next, methodArg1, methodArg2) {
  console.log(methodArg1);
  // => 'altered-originalValOfMethodArg1'

  console.log(methodArg2);
  // => 'originalValOfMethodArg2'

  // Passing no arguments to `next` automatically passes along the current argument values
  // i.e., the following `next()` is equivalent to `next(methodArg1, methodArg2)`
  // and also equivalent to, with the example method arg
  // values, `next('altered-originalValOfMethodArg1', 'originalValOfMethodArg2')`
  next();
});

Schema gotcha

type, when used in a schema has special meaning within Mongoose. If your schema requires using type as a nested property you must use object notation:

new Schema({
  broken: { type: Boolean },
  asset: {
    name: String,
    type: String // uh oh, it broke. asset will be interpreted as String
  }
});

new Schema({
  works: { type: Boolean },
  asset: {
    name: String,
    type: { type: String } // works. asset is an object with a type property
  }
});

Driver Access

Mongoose is built on top of the official MongoDB Node.js driver. Each mongoose model keeps a reference to a native MongoDB driver collection. The collection object can be accessed using YourModel.collection. However, using the collection object directly bypasses all mongoose features, including hooks, validation, etc. The one notable exception that YourModel.collection still buffers commands. As such, YourModel.collection.find() will not return a cursor.

API Docs

Mongoose API documentation, generated using dox and acquit.

Related Projects

MongoDB Runners

Unofficial CLIs

Data Seeding

Express Session Stores

License

Copyright (c) 2010 LearnBoost <dev@learnboost.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.