ORM 框架
什么是 ORM 和 ODM
ORM(Object-Relational Mapper) 将对象映射到关系数据库表,如 MySql、Oracle 等。
ODM(Object-Document Mapper) 则是将对象映射到文档,如 MongoDB。
为什么要用 ORM
在我们对数据库进行操作的时候,如关系型数据库,总是在代码中去编写 SQL,ORM 提供给我们另外一种选择,即通过面向对象编程来操作数据库。举例来说,下面是一行 SQL 语句:
SELECT id, first_name, last_name, phone, birth_date, sex
FROM persons
WHERE id = 10
程序直接运行 SQL,操作数据库的写法如下:
res = db.execSql(sql);
name = res[0]["FIRST_NAME"];
改成 ORM 的写法示例如下:
p = Person.get(10);
name = p.first_name;
一比较就可以发现,ORM 使用对象封装了数据库操作,因此可以不碰 SQL 语言,不用关心底层数据库。总结起来,ORM 有下面这些优点:
- 数据模型都在一个地方定义,更容易更新和维护,也利于重用代码。
- ORM 有现成的工具,很多功能都可以自动完成,比如数据消毒、预处理、事务等等。
- 它迫使你使用 MVC 架构,ORM 就是天然的 Model,最终使代码更清晰。
- 基于 ORM 的业务代码比较简单,代码量少,语义性好,容易理解。
- 你不必编写性能不佳的 SQL。
但是,ORM 也有很突出的缺点:
- ORM 库不是轻量级工具,需要花很多精力学习和设置。
- 对于复杂的查询,ORM 要么是无法表达,要么是性能不如原生的 SQL。
- ORM 抽象掉了数据库层,开发者无法了解底层的数据库操作,也无法定制一些特殊的 SQL。
摘自阮一峰 ORM 实例教程 👈
常用的 ORM/ODM 框架对比
一、Mongoose
目前比较常见的 MongoDB ODM 框架:
官网:https://mongoosejs.com/
数据库:仅支持 MongoDB
编程风格:
支持 Promise/async/await
基于 JS 内置类型的 Schema 声明
基于链式构造的 Query Builder 查询
周边技术:[Typegoose](https://www.npmjs.com/package/typegoose) 可以增加 TypeScript 支持,支持使用 Reflect Metadata 自动映射 TS 类型标注
热度:周频持续更新,NPM 周下载 70W+
较老牌的 Node.js ORM 框架,相对简易:
官网:http://docs.sequelizejs.com/
数据库:支持关系型数据库(MySQL/MSSQL/PostgreSQL/SQLite)
编程风格:
支持 Promise/async/await
基于自带的一套类型枚举声明
基于 JSON 对象的查询方式
基于自带的一套操作符描述
热度:月频持续更新,NPM 周下载 20W+
Sequelize 之后出现的 ORM 框架,风格与 Sequelize 较相似:
官网:http://bookshelfjs.org/
数据库:支持关系型数据库
编程风格:
基本上是 Eloquent ORM 的 JS 版本
支持 Promise/async/await
支持基于链式构造的 Query Builder 查询
热度:近半年未更新,NPM 周下载 1.7W
四、TypeORM
基于 Decorator 的 ORM 框架,对 TypeScript 支持较好,同时支持在 JavaScript 中通过手动声明使用,以及 JSON 方式的 Entity 配置声明:
官网:https://github.com/typeorm/typeorm/
数据库:支持关系型数据库,Beta 支持 MongoDB
编程风格:
基本上是 Hibernate 的 JS 版本
支持 Promise/async/await
支持基于链式构造的 Query Builder 查询
支持 CLI 工具
热度:周频持续更新,NPM 周下载 2.8W
Mongoose 驱动器
连接 Connection
mongoose.connect('mongodb://localhost/myblog');
也可以传递配置选项,具体可查看这里
mongoose.connect(uri, options);
模式 Schema
Mongoose 的一切都源于 Schema。每个 schema 映射到 MongoDB 的集合(collection)和定义该集合中文档的形式。
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var blogSchema = new Schema({
title: String, // String 为 schemaTypes,即内置的数据类型
author: String,
comments: [{ body: String, date: Date }],
date: { type: Date, default: Date.now },
array: [],
ofString: [String], // 数组
ofNumber: [Number],
mixed: Schema.Types.Mixed, // 混合类型,等价于空对象 {},但是 Mongoose 会失去自动检测和保存这些变化的能力,除非手动调用 markModified 方法
_someId: Schema.Types.ObjectId // 主键。每个 Schema 都会默认配置这个属性,属性名为 _id,自定义后会覆盖该属性
});
Schema 有几个可配置的选项,可以直接传递给构造函数或设置该配置,具体选项可参考这里 Options:
new Schema({..}, options);
// 等价于
var schema = new Schema({..});
schema.set(option, value);
更多 schemaTypes 可查询这里 👈
模型 Model
schema 不具备操作数据库的能力,必须通过它创建模型:
var Blog = mongoose.model('Blog', blogSchema);
实例方法
模型的实例是文档(Documents),具有很多内置的方法,具体可查看这里,也可以自定义:
// define a schema
var animalSchema = new Schema({ name: String, type: String });
// assign a function to the "methods" object of our animalSchema
animalSchema.methods.findSimilarTypes = function(cb) {
return this.model('Animal').find({ type: this.type }, cb);
};
var Animal = mongoose.model('Animal', animalSchema);
var dog = new Animal({ type: 'dog' });
dog.findSimilarTypes(function(err, dogs) {
console.log(dogs); // woof
});
静态方法
// assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function(name, cb) {
return this.find({ name: new RegExp(name, 'i') }, cb);
};
var Animal = mongoose.model('Animal', animalSchema);
Animal.findByName('fido', function(err, animals) {
console.log(animals);
});
虚拟属性
虚拟属性是文档属性,可以获取和设置但不保存到 MongoDB,用于格式化或组合字段。举个栗子:
// define a schema
var personSchema = new Schema({
name: {
first: String,
last: String
}
});
// compile our model
var Person = mongoose.model('Person', personSchema);
// create a document
var bad = new Person({
name: { first: 'Walter', last: 'White' }
});
// 打印全名
console.log(bad.name.first + ' ' + bad.name.last); // Walter White
打印全名时需要拼接字符串,此时可以通过虚拟属性来实现:
personSchema.virtual('name.full').get(function() {
return this.name.first + ' ' + this.name.last;
});
console.log('%s is insane', bad.name.full); // Walter White is insane
当然通过全名来拆分亦可:
personSchema.virtual('name.full').set(function(name) {
var split = name.split(' ');
this.name.first = split[0];
this.name.last = split[1];
});
mad.name.full = 'Breaking Bad';
console.log(mad.name.first); // Breaking
console.log(mad.name.last); // Bad
构建文档
创建文档并保存到数据库的两种方式,删除则都是 remove 方法:
var Tank = mongoose.model('Tank', yourSchema);
var small = new Tank({ size: 'small' });
small.save(function(err) {
if (err) return handleError(err);
// saved!
})
// 等价于
Tank.create({ size: 'small' }, function(err, small) {
if (err) return handleError(err);
// saved!
})
查询 Queries
查询的 API 可参考这里。
var Person = mongoose.model('Person', yourSchema);
// find each person with a last name matching 'Ghost', selecting the `name` and `occupation` fields
Person.findOne({ 'name.last': 'Ghost' }, 'name occupation', function (err, person) {
if (err) return handleError(err);
console.log('%s %s is a %s.', person.name.first, person.name.last, person.occupation) // Space Ghost is a talk show host.
})
实际上等同于执行了:
// find each person with a last name matching 'Ghost'
var query = Person.findOne({ 'name.last': 'Ghost' });
// selecting the `name` and `occupation` fields
query.select('name occupation');
// execute the query at a later time
query.exec(function(err, person) {
if (err) return handleError(err);
console.log('%s %s is a %s.', person.name.first, person.name.last, person.occupation) // Space Ghost is a talk show host.
})
同样以下两种写法也等价,可以建立一个查询使用链式语法,而不是指定一个 JSON 对象:
// With a JSON doc
Person.
find({
occupation: /host/,
'name.last': 'Ghost',
age: { $gt: 17, $lt: 66 },
likes: { $in: ['vaporizing', 'talking'] }
}).
limit(10).
sort({ occupation: -1 }).
select({ name: 1, occupation: 1 }).
exec(callback);
// Using query builder
Person.
find({ occupation: /host/ }).
where('name.last').equals('Ghost').
where('age').gt(17).lt(66).
where('likes').in(['vaporizing', 'talking']).
limit(10).
sort('-occupation').
select('name occupation').
exec(callback);
验证 Validation
验证是在 schemaType 中定义的中间件,常用的一些内置验证器有:
var breakfastSchema = new Schema({
eggs: {
type: Number,
min: [6, 'Too few eggs'],
max: 12
},
bacon: {
type: Number,
required: [true, 'Why no bacon?']
},
drink: {
type: String,
enum: ['Coffee', 'Tea']
}
});
var Breakfast = db.model('Breakfast', breakfastSchema);
// 验证
var badBreakfast = new Breakfast({
eggs: 2,
bacon: 0,
drink: 'Milk'
});
var error = badBreakfast.validateSync();
// error.errors 是错误集合,message 则是错误信息属性
assert.equal(error.errors['eggs'].message, 'Too few eggs');
assert.ok(!error.errors['bacon']);
assert.equal(error.errors['drink'].message, '`Milk` is not a valid enum value for path `drink`.');
badBreakfast.bacon = null;
error = badBreakfast.validateSync();
assert.equal(error.errors['bacon'].message, 'Why no bacon?');
还可以自定义验证器:
var userSchema = new Schema({
phone: {
type: String,
validate: {
validator: function(v) {
return /\d{3}-\d{3}-\d{4}/.test(v);
},
message: '{VALUE} is not a valid phone number!'
},
required: [true, 'User phone number required']
}
});
// 验证
var User = db.model('user', userSchema);
var user = new User();
var error;
user.phone = '555.0123';
error = user.validateSync();
assert.equal(error.errors['phone'].message, '555.0123 is not a valid phone number!');
user.phone = '';
error = user.validateSync();
assert.equal(error.errors['phone'].message, 'User phone number required');
user.phone = '201-555-0123';
// Validation succeeds! Phone number is defined
中间件 Middleware
中间件(也称为前置和后置钩子)是异步函数执行过程中传递的控制的函数。支持的主要有两种:
- 文档中间件
- init
- validate
- save
- remove
- 查询中间件
- count
- find
- findOne
- findOneAndRemove
- findOneAndUpdate
- update
前置钩子(pre) 也分为串行(serial)和并行(parallel),后置钩子(post) 则一般用来做监听器。
var schema = new Schema(..);
// 串行中间件是一个接一个的执行,每个中间件调用 next 将执行权移交给下一个中间件
schema.pre('save', function(next) {
// do stuff
next();
});
// `true` means this is a parallel middleware. You **must** specify `true` as the second parameter if you want to use parallel middleware.
schema.pre('save', true, function(next, done) {
// calling next kicks off the next middleware in parallel
next();
setTimeout(done, 100); // 除非完成 done 操作,否则 save 动作不会执行
});
联表 Population
索引 Index
插件 Plugins
未完待续