如果你觉得Sequelize的文档有点多、杂,不方便看,可以看看这篇。
在使用NodeJS
来关系型操作数据库时,为了方便,通常都会选择一个合适的ORM
(Object Relationship Model)框架。毕竟直接操作SQL
比较繁琐,通过ORM
框架,我们可以使用面向对象的方式来操作表。NodeJS
社区有很多的ORM
框架,我比较喜欢Sequelize
,它功能丰富,可以非常方便的进行连表查询。
这篇文章我们就来看看,Sequelize
是如何在SQL
之上进行抽象、封装,从而提高开发效率的。
安装
这篇文章主要使用MySQL
、Sequelize
、co
来进行介绍。安装非常简单:
1 | $ npm install --save co |
代码模板如下:
1 | var Sequelize = require('sequelize'); |
基本上,Sequelize
的操作都会返回一个Promise
,在co
的框架里面可以直接进行yield
,非常方便。
建立数据库连接
1 | var sequelize = new Sequelize( |
定义单张表
Sequelize
:
1 | var User = sequelize.define( |
SQL
:
1 | CREATE TABLE IF NOT EXISTS `users` ( |
几点说明:
建表
SQL
会自动执行的意思是你主动调用sync
的时候。类似这样:User.sync({force: true});
(加force:true
,会先删掉表后再建表)。我们也可以先定义好表结构,再来定义Sequelize
模型,这时可以不用sync
。两者在定义阶段没有什么关系,直到我们真正开始操作模型时,才会触及到表的操作,但是我们当然还是要尽量保证模型和表的同步(可以借助一些migration
工具)。自动建表功能有风险,使用需谨慎。所有数据类型,请参考文档数据类型。
模型还可以定义虚拟属性、类方法、实例方法,请参考文档:模型定义
其他一些特殊定义如下所示:
1 | var User = sequelize.define( |
单表增删改查
通过Sequelize
获取的模型对象都是一个DAO
(Data Access Object)对象,这些对象会拥有许多操作数据库表的实例对象方法(比如:save
、update
、destroy
等),需要获取“干净”的JSON
对象可以调用get({'plain': true})
。
通过模型的类方法可以获取模型对象(比如:findById
、findAll
等)。
增
Sequelize
:
1 | // 方法1:build后对象只存在于内存中,调用save后才操作db |
SQL
:
1 | INSERT INTO `users` |
Sequelize
会为主键id
设置DEFAULT
值来让数据库产生自增值,还将当前时间设置成了created_at
和updated_at
字段,非常方便。
改
Sequelize
:
1 | // 方法1:操作对象属性(不会操作db),调用save后操作db |
SQL
:
1 | UPDATE `users` |
更新操作时,Sequelize
将将当前时间设置成了updated_at
,非常方便。
如果想限制更新属性的白名单,可以这样写:
1 | // 方法1 |
这样就只会更新nick
字段,而emp_id
会被忽略。这种方法在对表单提交过来的一大推数据中只更新某些属性的时候比较有用。
删
Sequelize
:
1 | yield user.destroy(); |
SQL
:
1 | DELETE FROM `users` WHERE `id` = 1; |
这里有个特殊的地方是,如果我们开启了paranoid
(偏执)模式,destroy
的时候不会执行DELETE
语句,而是执行一个UPDATE
语句将deleted_at
字段设置为当前时间(一开始此字段值为NULL
)。我们可以使用user.destroy({force: true})
来强制删除,从而执行DELETE
语句进行物理删除。
查
查全部
Sequelize
:
1 | var users = yield User.findAll(); |
SQL
:
1 | SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` FROM `users`; |
限制字段
Sequelize
:
1 | var users = yield User.findAll({ |
SQL
:
1 | SELECT `emp_id`, `nick` FROM `users`; |
字段重命名
Sequelize
:
1 | var users = yield User.findAll({ |
SQL
:
1 | SELECT `emp_id`, `nick` AS `user_nick` FROM `users`; |
where子句
Sequelize
的where
配置项基本上完全支持了SQL
的where
子句的功能,非常强大。我们一步步来进行介绍。
基本条件
Sequelize
:
1 | var users = yield User.findAll({ |
SQL
:
1 | SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` |
可以看到,k: v
被转换成了k = v
,同时一个对象的多个k: v
对被转换成了AND
条件,即:k1: v1, k2: v2
转换为k1 = v1 AND k2 = v2
。
这里有2个要点:
- 如果v是
null
,会转换为IS NULL
(因为SQL
没有= NULL
这种语法) - 如果v是数组,会转换为
IN
条件(因为SQL
没有=[1,2,3]
这种语法,况且也没数组这种类型)
操作符
操作符是对某个字段的进一步约束,可以有多个(对同一个字段的多个操作符会被转化为AND
)。
Sequelize
:
1 | var users = yield User.findAll({ |
SQL
:
1 | SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` |
这里我们发现,其实相等条件k: v
这种写法是操作符写法k: {$eq: v}
的简写。而要实现不等条件就必须使用操作符写法k: {$ne: v}
。
条件
上面我们说的条件查询,都是AND
查询,Sequelize
同时也支持OR
、NOT
、甚至多种条件的联合查询。
AND条件
Sequelize
:
1 | var users = yield User.findAll({ |
SQL
:
1 | SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` |
OR条件
Sequelize
:
1 | var users = yield User.findAll({ |
SQL
:
1 | SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` |
NOT条件
Sequelize
:
1 | var users = yield User.findAll({ |
SQL
:
1 | SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` |
转换规则
我们这里做个总结。Sequelize
对where
配置的转换规则的伪代码大概如下:
1 | function translate(where) { |
我们看一个复杂例子,基本上就是按上述流程来进行转换。
Sequelize
:
1 | var users = yield User.findAll({ |
SQL
:
1 | SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` |
排序
Sequelize
:
1 | var users = yield User.findAll({ |
SQL
:
1 | SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` |
分页
Sequelize
:
1 | var countPerPage = 20, currentPage = 5; |
SQL
:
1 | SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` |
其他查询方法
查询一条数据
Sequelize
:
1 | user = yield User.findById(1); |
SQL
:
1 | SELECT `id`, `emp_id`, `nick`, `department`, `created_at`, `updated_at` |
查询并获取数量
Sequelize
:
1 | var result = yield User.findAndCountAll({ |
SQL
:
1 | SELECT count(*) AS `count` FROM `users` AS `user`; |
这个方法会执行2个SQL
,返回的result
对象将包含2个字段:result.count
是数据总数,result.rows
是符合查询条件的所有数据。
批量操作
插入
Sequelize
:
1 | var users = yield User.bulkCreate( |
SQL
:
1 | INSERT INTO `users` |
这里需要注意,返回的users
数组里面每个对象的id
值会是null
。如果需要id
值,可以重新取下数据。
更新
Sequelize
:
1 | var affectedRows = yield User.update( |
SQL
:
1 | UPDATE `users` |
这里返回的affectedRows
其实是一个数组,里面只有一个元素,表示更新的数据条数(看起来像是Sequelize
的一个bug
)。
删除
Sequelize
:
1 | var affectedRows = yield User.destroy({ |
SQL
:
1 | DELETE FROM `users` WHERE `id` IN (2, 3, 4); |
这里返回的affectedRows
是一个数字,表示删除的数据条数。
关系
关系一般有三种:一对一、一对多、多对多。Sequelize
提供了清晰易用的接口来定义关系、进行表间的操作。
当说到关系查询时,一般会需要获取多张表的数据。有建议用连表查询join
的,有不建议的。我的看法是,join
查询这种黑科技在数据量小的情况下可以使用,基本没有什么影响,数据量大的时候,join
的性能可能会是硬伤,应该尽量避免,可以分别根据索引取单表数据然后在应用层对数据进行join
、merge
。当然,查询时一定要分页,不要findAll
。
一对一
模型定义
Sequelize
:
1 | var User = sequelize.define('user', |
SQL
:
1 | CREATE TABLE IF NOT EXISTS `users` ( |
可以看到,这种关系中外键user_id
加在了Account
上。另外,Sequelize
还给我们生成了外键约束。
一般来说,外键约束在有些自己定制的数据库系统里面是禁止的,因为会带来一些性能问题。所以,建表的SQL
一般就去掉约束,同时给外键加一个索引(加速查询),数据的一致性就靠应用层来保证了。
关系操作
增
Sequelize
:
1 | var user = yield User.create({'emp_id': '1'}); |
SQL
:
1 | INSERT INTO `users` |
SQL
执行逻辑是:
- 使用对应的的
user_id
作为外键在accounts
表里插入一条数据。
改
Sequelize
:
1 | var anotherAccount = yield Account.create({'email': 'b'}); |
SQL
:
1 | INSERT INTO `accounts` |
SQL
执行逻辑是:
- 插入一条
account
数据,此时外键user_id
是空的,还没有关联user - 找出当前
user
所关联的account
并将其user_id
置为`NUL(为了保证一对一关系) - 设置新的
acount
的外键user_id
为user
的属性id
,生成关系
删
Sequelize
:
1 | yield user.setAccount(null); |
SQL
:
1 | SELECT `id`, `email`, `created_at`, `updated_at`, `user_id` |
这里的删除实际上只是“切断”关系,并不会真正的物理删除记录。
SQL
执行逻辑是:
- 找出
user
所关联的account
数据 - 将其外键
user_id
设置为NULL
,完成关系的“切断”
查
Sequelize
:
1 | var account = yield user.getAccount(); |
SQL
:
1 | SELECT `id`, `email`, `created_at`, `updated_at`, `user_id` |
这里就是调用user
的getAccount
方法,根据外键来获取对应的account
。
但是其实我们用面向对象的思维来思考应该是获取user
的时候就能通过user.account
的方式来访问account
对象。这可以通过Sequelize
的eager loading
(急加载,和懒加载相反)来实现。
eager loading
的含义是说,取一个模型的时候,同时也把相关的模型数据也给我取过来(我很着急,不能按默认那种取一个模型就取一个模型的方式,我还要更多)。方法如下:
Sequelize
:
1 | var user = yield User.findById(1, { |
SQL
:
1 | SELECT `user`.`id`, `user`.`emp_id`, `user`.`created_at`, `user`.`updated_at`, `account`.`id` AS `account.id`, `account`.`email` AS `account.email`, `account`.`created_at` AS `account.created_at`, `account`.`updated_at` AS `account.updated_at`, `account`.`user_id` AS `account.user_id` |
可以看到,我们对2个表进行了一个外联接,从而在取user
的同时也获取到了account
。
其他补充说明
如果我们重复调用user.createAccount
方法,实际上会在数据库里面生成多条user_id
一样的数据,并不是真正的一对一。
所以,在应用层保证一致性时,就需要我们遵循良好的编码约定。新增就用user.createAccount
,更改就用user.setAccount
。
也可以给user_id
加一个UNIQUE
约束,在数据库层面保证一致性,这时就需要做好try/catch
,发生插入异常的时候能够知道是因为插入了多个account
。
另外,我们上面都是使用user
来对account
进行操作。实际上反向操作也是可以的,这是因为我们定义了Account.belongsTo(User)
。在Sequelize
里面定义关系时,关系的调用方会获得相关的“关系”方法,一般为了两边都能操作,会同时定义双向关系(这里双向关系指的是模型层面,并不会在数据库表中出现两个表都加上外键的情况,请放心)。
一对多
模型定义
Sequelize
:
1 | var User = sequelize.define('user', |
SQL
:
1 | CREATE TABLE IF NOT EXISTS `users` ( |
可以看到这种关系中,外键user_id
加在了多的一端(notes
表)。同时相关的模型也自动获得了一些方法。
关系操作
增
方法1
Sequelize
:
1 | var user = yield User.create({'emp_id': '1'}); |
SQL
:
1 | NSERT INTO `users` |
SQL
执行逻辑:
- 使用
user
的主键id
值作为外键直接在notes
表里插入一条数据。
方法2
Sequelize
:
1 | var user = yield User.create({'emp_id': '1'}); |
SQL
:
1 | INSERT INTO `users` |
SQL
执行逻辑:
- 插入一条
note
数据,此时该条数据的外键user_id
为空 - 使用
user
的属性id
值再更新该条note
数据,设置好外键,完成关系建立
改
Sequelize
:
1 | // 为user增加note1、note2 |
SQL
:
1 | /* 省去了创建语句 */ |
SQL
执行逻辑:
- 根据
user
的属性id
查询所有相关的note
数据 - 将
note1
、note2
的外键user_id
置为NULL
,切断关系 - 将
note3
、note4
的外键user_id
置为user
的属性id
,完成关系建立
这里为啥还要查出所有的note
数据呢?因为我们需要根据传人setNotes
的数组来计算出哪些note
要切断关系、哪些要新增关系,所以就需要查出来进行一个计算集合的“交集”运算。
删
Sequelize
:
1 | var user = yield User.create({'emp_id': '1'}); |
SQL
:
1 | SELECT `id`, `title`, `created_at`, `updated_at`, `user_id` |
实际上,上面说到的“改”已经有“删”的操作了(去掉note1
、note2
的关系)。这里的操作是删掉用户的所有note
数据,直接执行user.setNotes([])
即可。
SQL
执行逻辑:
- 根据
user
的属性id
查出所有相关的note
数据 - 将其外键
user_id
置为NULL
,切断关系
还有一个真正的删除方法,就是removeNote
。如下所示:
Sequelize
:
1 | yield user.removeNote(note); |
SQL
:
1 | UPDATE `notes` |
查
情况1
查询user
的所有满足条件的note
数据。
Sequelize
:
1 | var notes = yield user.getNotes({ |
SQL
:
1 | SELECT `id`, `title`, `created_at`, `updated_at`, `user_id` |
这种方法的SQL
很简单,直接根据user
的id
值来查询满足条件的note
即可。
情况2
查询所有满足条件的note
,同时获取note
属于哪个user
。
Sequelize
:
1 | var notes = yield Note.findAll({ |
SQL
:
1 | SELECT `note`.`id`, `note`.`title`, `note`.`created_at`, `note`.`updated_at`, `note`.`user_id`, |
这种方法,因为获取的主体是note
,所以将notes
去left join
了users
。
情况3
查询所有满足条件的user
,同时获取该user
所有满足条件的note
。
Sequelize
:
1 | var users = yield User.findAll({ |
SQL
:
1 | SELECT `user`.`id`, `user`.`emp_id`, `user`.`created_at`, `user`.`updated_at`, |
这种方法获取的主体是user
,所以将users
去left join
了notes
。
一点补充
关于各种join的区别,可以参考:http://blog.codinghorror.com/a-visual-explanation-of-sql-joins/。
关于eager loading
我想再啰嗦几句。include
里面传递的是去取相关模型,默认是取全部,我们也可以再对这个模型进行一层过滤。像下面这样:
Sequelize
:
1 | // 查询创建时间在今天之前的所有user,同时获取他们note的标题中含有关键字css的所有note |
SQL
:
1 | SELECT `user`.`id`, `user`.`emp_id`, `user`.`created_at`, `user`.`updated_at`, |
注意:当我们对include
的模型加了where
过滤时,会使用inner join
来进行查询,这样保证只有那些拥有标题含有css
关键词note
的用户才会返回。
多对多关系
在多对多关系中,必须要额外一张关系表来将2个表进行关联,这张表可以是单纯的一个关系表,也可以是一个实际的模型(含有自己的额外属性来描述关系)。我比较喜欢用一个模型的方式,这样方便以后做扩展。
模型定义
Sequelize
:
1 | var Note = sequelize.define('note', |
SQL
:
1 | CREATE TABLE IF NOT EXISTS `notes` ( |
可以看到,多对多关系中单独生成了一张关系表,并设置了2个外键tag_id
和note_id
来和tags
和notes
进行关联。关于关系表的命名,我比较喜欢使用动词,因为这张表是用来表示两张表的一种联系,而且这种联系多数时候伴随着一种动作。比如:用户收藏商品(collecting
)、用户购买商品(buying
)、用户加入项目(joining
)等等。
增
方法1
Sequelize
:
1 | var note = yield Note.create({'title': 'note'}); |
SQL
:
1 | INSERT INTO `notes` |
SQL
执行逻辑:
- 在
notes
表插入记录 - 在
tags
表中插入记录 - 使用对应的值设置外键
tag_id
和note_id
以及关系模型本身需要的属性(type: 0
)在关系表tagging
中插入记录
关系表本身需要的属性,通过传递一个额外的对象给设置方法来实现。
方法2
Sequelize
:
1 | var note = yield Note.create({'title': 'note'}); |
SQL
:
1 | INSERT INTO `notes` |
这种方法和上面的方法实际上是一样的。只是我们先手动create
了一个Tag
模型。
方法3
Sequelize
:
1 | var note = yield Note.create({'title': 'note'}); |
SQL
:
1 | INSERT INTO `notes` |
这种方法可以进行批量添加。当执行addTags
时,实际上就是设置好对应的外键及关系模型本身的属性,然后在关系表中批量的插入数据。
改
Sequelize
:
1 | // 先添加几个tag |
SQL
:
1 | /* 前面添加部分的sql,和上面一样*/ |
执行逻辑是,先将tag1
、tag2
在关系表中的关系删除,然后再将tag3
、tag4
对应的关系插入关系表。
删
Sequelize
:
1 | // 先添加几个tag |
SQL
:
1 | /* 删除一个 */ |
删除一个很简单,直接将关系表中的数据删除。
全部删除时,首先需要查出关系表中note_id
对应的所有数据,然后一次删掉。
查
情况1
查询note
所有满足条件的tag
。
Sequelize
:
1 | var tags = yield note.getTags({ |
SQL
:
1 | SELECT `tag`.`id`, `tag`.`name`, `tag`.`created_at`, `tag`.`updated_at`, |
可以看到这种查询,就是执行一个inner join
。
情况2
查询所有满足条件的tag
,同时获取每个tag
所在的note
。
Sequelize
:
1 | var tags = yield Tag.findAll({ |
SQL
:
1 | SELECT `tag`.`id`, `tag`.`name`, `tag`.`created_at`, `tag`.`updated_at`, |
这个查询就稍微有点复杂。首先是notes
和taggings
进行了一个inner join
,选出notes
;然后tags
和刚join
出的集合再做一次left join
,得到结果。
情况3
查询所有满足条件的note
,同时获取每个note
所有满足条件的tag
。
Sequelize
:
1 | var notes = yield Note.findAll({ |
SQL
:
1 | SELECT |
这个查询和上面的查询类似。首先是tags
和taggins
进行了一个inner join
,选出tags
;然后notes
和刚join
出的集合再做一次left join
,得到结果。
其他没有涉及东西
这篇文章已经够长了,但是其实我们还有很多没有涉及的东西,比如:聚合函数及查询(having
、group by
)、模型的验证(validate
)、定义钩子(hooks
)、索引等等。
这些主题下次再来写写。
本文采用 知识共享署名 3.0 中国大陆许可协议,可自由转载、引用,但需署名作者且注明文章出处 。