bookshelf、mongoose、sequelize 和 waterline 都是 Node.js 生态中用于简化数据库操作的对象映射工具。mongoose 是 MongoDB 的 ODM(对象文档映射),专注于文档型数据库;sequelize 是功能强大的 SQL ORM,支持 Postgres、MySQL 等关系型数据库;bookshelf 是基于 Knex.js 构建的 SQL ORM,强调链式查询;waterline 则采用适配器模式,旨在统一不同数据库的访问接口,常与 Sails.js 框架配合使用。这些库帮助开发者以面向对象的方式操作数据,处理模型定义、关系关联及数据验证。
在 Node.js 后端开发中,选择合适的数据访问层(Data Access Layer)是架构决策的关键一环。bookshelf、mongoose、sequelize 和 waterline 代表了四种不同的设计哲学:从专一的 ODM 到通用的 SQL ORM,再到适配器模式。作为架构师,我们需要透过表面的 API 差异,深入理解它们对数据一致性、开发效率及长期维护性的影响。
选择这些库的第一步并非看语法,而是看数据库类型。这决定了你的数据模型是严格的表格关系,还是灵活的文档结构。
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 }
}
};
查询数据的体验直接影响日常开发效率。这四个库在“如何表达查询意图”上有着显著差异。
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'
});
处理表连接(Join)或文档引用(Populate)是 ORM 的核心价值所在。
mongoose 通过 ref 字段定义引用,使用 populate 在查询时自动填充关联数据。
// mongoose: 定义关联
const PostSchema = new Schema({
author: { type: Schema.Types.ObjectId, ref: 'User' }
});
// 查询时填充
Post.find().populate('author');
sequelize 在模型定义时明确声明 belongsTo 或 hasMany,查询时使用 include 进行 SQL JOIN。
// sequelize: 定义关联
User.hasMany(Post);
Post.belongsTo(User);
// 查询时包含
User.findAll({ include: [Post] });
bookshelf 使用 hasMany 或 belongsTo 方法定义关系,获取数据时需显式调用 load 或 withRelated。
// 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');
生产环境必须处理数据库架构变更(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 配置自动管理架构(如 alter、drop),但在独立使用时迁移管理较为复杂。
// waterline: Sails 配置迁移策略
// config/models.js
module.exports.models = {
migrate: 'alter' // auto, alter, drop, safe
};
这是架构决策中最容易被忽视但最致命的一点。
mongoose 和 sequelize 目前处于活跃维护状态,社区庞大,文档更新及时,是生产环境的安全选择。bookshelf 的更新频率显著降低,社区讨论热度下降。虽然稳定,但新特性支持(如 TypeScript 类型定义)滞后。不建议在新项目中作为首选,除非团队对 Knex 有极强依赖。waterline 主要作为 Sails.js 的一部分存在,独立使用场景较少。其迭代速度较慢,且生态绑定严重。若不使用 Sails.js 框架,强烈建议避免使用。| 特性 | mongoose | sequelize | bookshelf | waterline |
|---|---|---|---|---|
| 数据库 | MongoDB | SQL (PG/MySQL) | SQL (via Knex) | 多数据库适配 |
| 类型支持 | 良好 (TS) | 优秀 (TS) | 一般 | 一般 |
| 迁移工具 | 需第三方 | 内置强大 CLI | 依赖 Knex | Sails 内置 |
| 维护状态 | 🟢 活跃 | 🟢 活跃 | 🟡 缓慢 | 🟡 缓慢 |
| 推荐场景 | NoSQL 项目 | 企业级 SQL 项目 | 遗留 Knex 项目 | Sails.js 项目 |
最终结论:
对于现代 Node.js 架构,sequelize(SQL 场景)和 mongoose(NoSQL 场景)是唯二推荐用于新生产环境的库。它们提供了必要的稳定性、类型支持和社区保障。
bookshelf 和 waterline 属于特定历史时期的优秀工具,但在当前的微服务和 Serverless 架构趋势下,它们的抽象层显得过于厚重或维护不足。除非你正在维护基于这些技术栈的遗留系统,否则应将技术债务控制在最小范围,优先选择更活跃的替代方案。
仅在你维护遗留系统或极度依赖 Knex.js 查询构建器时考虑 bookshelf。由于社区活跃度下降且缺乏新特性,新项目不建议使用。它适合那些已经深度集成 Knex 且不需要 ORM 高级特性的轻量级 SQL 项目。
如果你的项目使用 MongoDB,mongoose 是不二之选。它提供了丰富的 Schema 定义、中间件钩子和强大的聚合管道支持。适合需要灵活文档结构、快速迭代且不需要严格事务关系的场景,如内容管理系统或实时分析应用。
如果你需要操作关系型数据库(如 PostgreSQL 或 MySQL),且看重类型安全、迁移管理和复杂事务处理,选择 sequelize。它拥有成熟的生态系统和详细的文档,适合企业级应用和对数据一致性要求较高的场景。
只有在使用 Sails.js 框架或需要一套代码同时适配多种不同数据库(如同时支持 MySQL 和 MongoDB)的特定场景下才考虑 waterline。由于其更新缓慢且生态绑定较强,通用 Node.js 项目通常不推荐作为首选。
Bookshelf is a JavaScript ORM for Node.js, built on the Knex SQL query builder. It features both Promise-based and traditional callback interfaces, transaction support, eager/nested-eager relation loading, polymorphic associations, and support for one-to-one, one-to-many, and many-to-many relations.
It is designed to work with PostgreSQL, MySQL, and SQLite3.
Website and documentation. The project is hosted on GitHub, and has a comprehensive test suite.
Bookshelf aims to provide a simple library for common tasks when querying databases in JavaScript, and forming relations between these objects, taking a lot of ideas from the Data Mapper Pattern.
With a concise, literate codebase, Bookshelf is simple to read, understand, and extend. It doesn't force you to use any specific validation scheme, and provides flexible, efficient relation/nested-relation loading and first-class transaction support.
It's a lean object-relational mapper, allowing you to drop down to the raw Knex interface whenever you need a custom query that doesn't quite fit with the stock conventions.
You'll need to install a copy of Knex, and either mysql, pg, or sqlite3 from npm.
$ npm install knex
$ npm install bookshelf
# Then add one of the following:
$ npm install pg
$ npm install mysql
$ npm install sqlite3
The Bookshelf library is initialized by passing an initialized Knex client instance. The Knex documentation provides a number of examples for different databases.
// Setting up the database connection
const knex = require('knex')({
client: 'mysql',
connection: {
host : '127.0.0.1',
user : 'your_database_user',
password : 'your_database_password',
database : 'myapp_test',
charset : 'utf8'
}
})
const bookshelf = require('bookshelf')(knex)
// Defining models
const User = bookshelf.model('User', {
tableName: 'users'
})
This initialization should likely only ever happen once in your application. As it creates a connection pool for the current database, you should use the bookshelf instance returned throughout your library. You'll need to store this instance created by the initialize somewhere in the application so you can reference it. A common pattern to follow is to initialize the client in a module so you can easily reference it later:
// In a file named, e.g. bookshelf.js
const knex = require('knex')(dbConfig)
module.exports = require('bookshelf')(knex)
// elsewhere, to use the bookshelf client:
const bookshelf = require('./bookshelf')
const Post = bookshelf.model('Post', {
// ...
})
Here is an example to get you started:
const knex = require('knex')({
client: 'mysql',
connection: process.env.MYSQL_DATABASE_CONNECTION
})
const bookshelf = require('bookshelf')(knex)
const User = bookshelf.model('User', {
tableName: 'users',
posts() {
return this.hasMany(Posts)
}
})
const Post = bookshelf.model('Post', {
tableName: 'posts',
tags() {
return this.belongsToMany(Tag)
}
})
const Tag = bookshelf.model('Tag', {
tableName: 'tags'
})
new User({id: 1}).fetch({withRelated: ['posts.tags']}).then((user) => {
console.log(user.related('posts').toJSON())
}).catch((error) => {
console.error(error)
})
.set() on a model.Model, adding timestamps, attribute validation and some native CRUD methods.Have questions about the library? Come join us in the #bookshelf freenode IRC channel for support on knex.js and bookshelf.js, or post an issue on Stack Overflow.
If you want to contribute to Bookshelf you'll usually want to report an issue or submit a pull-request. For this purpose the online repository is available on GitHub.
For further help setting up your local development environment or learning how you can contribute to Bookshelf you should read the Contributing document available on GitHub.
Yes, you can call .asCallback(function(err, resp) { on any database operation method and use the standard (err, result) style callback interface if you prefer.
Make sure to check that the type is correct for the initial parameters passed to the initial model being fetched. For example new Model({id: '1'}).load([relations...]) will not return the same as new Model({id: 1}).load([relations...]) - notice that the id is a string in one case and a number in the other. This can be a common mistake if retrieving the id from a url parameter.
This is only an issue if you're eager loading data with load without first fetching the original model. new Model({id: '1'}).fetch({withRelated: [relations...]}) should work just fine.
The issue here is that Knex, the database abstraction layer used by Bookshelf, uses connection pooling and thus keeps the database connection open. If you want your process to exit after your script has finished, you will have to call .destroy(cb) on the knex property of your Bookshelf instance or on the Knex instance passed during initialization. More information about connection pooling can be found over at the Knex docs.
If you pass debug: true in the options object to your knex initialize call, you can see all of the query calls being made. You can also pass that same option to all methods that access the database, like model.fetch() or model.destroy(). Examples:
// Turning on debug mode for all queries
const knex = require('knex')({
debug: true,
client: 'mysql',
connection: process.env.MYSQL_DATABASE_CONNECTION
})
const bookshelf = require('bookshelf')(knex)
// Debugging a single query
new User({id: 1}).fetch({debug: true, withRelated: ['posts.tags']}).then(user => {
// ...
})
Sometimes you need to dive a bit further into the various calls and see what all is going on behind the scenes. You can use node-inspector, which allows you to debug code with debugger statements like you would in the browser.
Bookshelf uses its own copy of the bluebird Promise library. You can read up here for more on debugging Promises.
Adding the following block at the start of your application code will catch any errors not otherwise caught in the normal Promise chain handlers, which is very helpful in debugging:
process.stderr.on('data', (data) => {
console.log(data)
})
See the CONTRIBUTING document on GitHub.
While it primarily targets Node.js, all dependencies are browser compatible, and it could be adapted to work with other javascript environments supporting a sqlite3 database, by providing a custom Knex adapter. No such adapter exists though.
We found the following projects using Bookshelf, but there can be more: