使用 Sequelize 更简单地操作数据库 - Node

3,034 阅读8分钟

最近在做项目的时候需要用到 Sequelize,学习过程中踩了一些坑在这里记录一下。目的是帮助需要使用 Sequelize 的同学能够快速上手,文章包括 Sequelize 的使用流程,常见问题等。

Sequelize 官网

1. Sequelize 简单介绍

Sequelize 是一种 ORM 框架,ORM(Object Relational Mapping)即对象关系映射,它是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。从效果上说,它其实是创建了一个可在编程语言里使用的虚拟对象数据库。

ORM 技术是在对象和数据库之间提供了一座桥梁,我们在操作数据库的时候就不需要再去和复杂的 SQL 语句打交道,只需简单的操作实体对象的属性和方法,就可以达到操作数据库的效果

在不同的编程语言里有不同的 ORM 框架,在 Node 开发中,比较流行的主要有 Sequelize 和 Typeorm 两种,本文主要介绍 Sequelize。

2. 为什么要使用sequelize

关于为什么要使用 Sequelize 按照我现在的理解主要有下面两个方面的原因。

1)团队协作开发

很多同学在刚开始做个人项目的时候可能是在 Mysql 图形界面里创建好数据表,然后再开始实际开发,一个人在一台机器上,做全栈的开发,这个过程可能并不会出现什么问题。因为数据表结构以及整个项目代码都在一台电脑上,不管怎么修改,都是一套代码,一个数据库结构。但是在实际的团队开发的时候,如果成员 A 在电脑上修改了数据表结构,那么其它成员也必须手动修改这个数据表结构。如果使用 Sequelize,那么成员 A 在将代码提交之后,其它成员就可以利用提交的代码直接修改数据表结构。

当然,数据表结构可能并不会经常变化,而且利用代码操作数据表是有很大风险的,所以使用哪一种方式要经过慎重考虑,并且要有严格的规范才行。

2)复杂查询

相比于原生的 SQL 语句查询,使用 Sequelize 查询数据库非常简单和直观,这方面的说明可以参考 3.4.1 节。

3. 如何使用 Sequelize

3.1. 创建 Sequelize 实例

1)安装 Sequelize

npm install Sequelize

2)创建 Sequelize 实例

import sqlConfig from '../config/db'
import { Sequelize } from 'sequelize'

// 创建sequelize实例
const sequelize = new Sequelize(sqlConfig.database, sqlConfig.user, sqlConfig.password, {
    host: sqlConfig.host,
    dialect: 'mysql'
})

export default sequelize

3.2. 建立模型

在 Sequelize 中建立的模型就相当于数据库中建立的表。

这里新建了一个 Actor 模型,它就相当于数据库中的 Actor 表(当然数据库中对应表的名字可能是 Actors,这是你自己在利用数据模型生成数据库表的时候在 Sequelize 中设置的)。

import { DataTypes } from 'sequelize'
import { sequelize } from '../../db/seq'

const Actor = sequelize.define('Actor', { name: DataTypes.STRING });

export default Actor

3.3. 定义关联关系

Sequelize 提供了 四种 关联类型,并将它们组合起来以创建关联:

  • hasOne 关联类型
  • belongsTo 关联类型
  • hasMany 关联类型
  • belongsToMany 关联类型

如果我们不设置外键,那么在我们定义好关联关系后 Sequelize 会自动帮我们设置外键,当然我们也可以自己设置外键。

一对一关系

例如,班级和班长之间的关系就是一对一的关系。

Class.hasOne(Monitor)
Monitor.belongsTo(Class)
// 手动设置外键
Class.hasOne(Monitor, {
  foreignKey: 'classId'
});
Monitor.belongsTo(Class);

一对多关系

例如,一个团队有很多成员,成员和团队之间就是一对多关系。

Team.hasMany(Member)
Member.belongsTo(Team)
// 手动设置外键
Team.hasOne(Member, {
  foreignKey: 'teamId'
});
Member.belongsTo(Team);

多对多关系

例如,一部电影有很多演员,一个演员有很多电影作品,它们之间就是多对多的关系。对于这种多对多的关联关系,需要一个联结表来表示它们之间的关系。

Actor.belongsToMany(Movie, { through: ActorMovies })
Movie.belongsToMany(Actor, { through: ActorMovies })

3.4. 操作数据库

使用 Sequelize 后不需要写 sql 语句去操作数据库了,当然它也提供了 query 方法可以将 SQL 语句传递进去进行查询。

但是我们一般会利用第二步建立的模型,然后调用 Sequelize 提供的方法操作数据库。Sequelize 提供的方法有非常多,这里只介绍一些对数据库基本的增删改查操作,其它复杂的操作可以参考 Sequelize 官网

3.4.1 查询

简单的 select 查询
// 1. 简单select查询
const result = await Actors.findAll();
// 等价于
select * from Actors;
// 2. 查询特定属性
const result = await Actors.findAll({
  attributes: ['name']
})
// 等价于
select name from Actors;
// 3. 应用where子句
Movies.findAll({
  where: {
    name: 'Tom'
  }
})
// 等价于
select * from Movies where name = 'TOM';
复杂的 select 查询

对于一些关系稍微复杂的表,利用原生的 sql 语句写起来就没有那么直观了,但是使用 Sequelize 可以很简单地将需要的内容查询出来。

举一个不算复杂的多对多数据表的查询吧,有三个数据表分别是演员表(Actors),电影表(Movies)和联结表(ActorMovies),现在需要查询演员 Tom 主演的电影有哪些?

Actors表结构

在这里插入图片描述

Movies表结构

在这里插入图片描述

ActorMovies表结构

在这里插入图片描述

通过上面的表结构我们可以看到,Tom 主演的电影有 movie1 和 movie2 两部电影,通过 SQL 语句和 Sequelize 都可以查到正确的结果,但是它们的写法有很大的区别。

SQL 查询

select `name` from Movies where id in (select ActorMovies.MovieId from ActorMovies where ActorMovies.ActorId in (select Actors.id from Actors where name = 'Tom'));

Sequelize 查询:

在 Sequelize 中对于复杂关系表的查询提供了预先加载和延迟加载两种方法。

这里采用预习加载的方法,所谓预先加载是指从一开始就通过较大的查询一次获取所有内容的技术,有点抽象,结合例子来说明。

下面的 Sequelize 代码中我们查询了 Actors 表中姓名为 Tom 的信息,注意下面有一个 include ,并且里面包含了一个 Movies 模型。这就表示应用了预先加载,在执行下面的代码后,姓名为 Tom 的演员主演的电影的信息也会被查询出来。

import Movies from './movies'
const result = Actors.findAll({
  where: {
    name: 'Tom'
  },
  include: {
    model: Movies
  }
})

查询结果(Movies 数组中包含Tom主演的电影的信息):

在这里插入图片描述

当然你也可以通过定义一些筛选条件(where等),只获取你想获取的信息。

通过上面的查询可以看到使用 Sequelize 时更直观和简单一些,对于比较复杂的查询更是如此。

3.4.2 删除

在 Sequelize 中可以通过 destroy 删除数据。

await Actors.destroy({
  where: {
    name: 'Tom'
  }
});
// 等价于
delete from Actors where name = 'Tom';

3.4.3 更新

在 Sequelize 中可以通过 update 更新数据。

await Actors.update({ name: "Mike" }, {
  where: {
    name: 'Tom'
  }
});
// 等价于
update Actors set name = 'Mike' where name = 'Tom';

3.4.4 新增

单个创建

在演员表里新增一个姓名为 John 的演员。

await Actors.create({
  name: 'John'
})

批量创建

向演员表里一次性新增多个演员。

await Actors.bulkCreate([
  {name: 'Amy'},
  {name: 'cole'}
])

4. 常见问题及解决方法

4.1 在定义表的关系时,如果添加了 as,那么在 include 中不要忘记也应该添加 as,否则不会正常工作。

在定义表关系时:

User.belongsToMany(Env, {  
  through: {    
    model: SpaceGroupEnvUser,    
    unique: false,  
  },  
  foreignKey: 'f_user_id',  
  as: 'env'
});

使用预先加载时:

const result = await User.findAll({    
  where: {      
    rtx: 'orange'    
  },    
  include: {      
    model: Env,      
    as: 'env'    
  }  
})

由于在定义表关系时使用了 as,所以在使用预先加载时也需要加上 as,否则预先加载就不会生效。

as: 定义别名,简单来说就是给模型取一个别名,一个模型可以有很多别名,就像一个人有很多外号一样。这在某些情况下是很有用处的,举一个官网给出的示例吧。

我们有MailPerson 模型,则可能需要将它们关联两次,以表示邮件的 senderreceiver. 在这种情况下,我们必须为每个关联使用别名,因为否则,诸如 mail.getPerson() 之类的调用将是模棱两可的. 使用 senderreceiver 别名,我们将有两种可用的可用方法:mail.getSender()mail.getReceiver()

4.2 throw new Error(`${this.name}.belongsToMany called with something that's not a subclass of Sequelize.Model

这个问题很常见,例如下面这种写法就会出现这种问题。

actor.ts

import Movies from './movie'
import ActorMovies from './actormovies'

const Actors = sequelize.define({
  ...
})
Actors.belongsToMany(Movies, { through: ActorMovies })

export default Actors

movie.ts

import Actors from './actor'
import ActorMovies from './actormovies'

const Movies = sequelize.define({
  ...
})
Movies.belongsToMany(Actors, { through: ActorMovies })

export default Movies

上面这种写法就会报上面的错误,原因在于例如在 actor.ts 中引入 Movies 模型,此时在 belongstomany 中使用 Movies,实际上 Movies 还并没有被初始化,会输出 undefined,所以会报 not a subclass of Sequelize.Model 的错误。

这部分的具体原因还不是特别清楚,在查看了 Sequelize 的 define 和 belongsToMany 等 API 的源码后发现有可能是异步的原因。在 stackoverflow 上查阅和提问也没有得到很好的解答,所以现在也不是特别确定,如果哪位同学知道这样写为什么会出错的具体原因,欢迎留下你的答案哦。

解决方法:我们可以建立一个专门存放各种模型的 model 文件夹,在模型文件中就不要定义模型之间的关系了,新建一个 index.ts 文件专门处理模型之间的关系。这样可以在 index.ts 文件中统一导入模型,然后在定义好模型之间的关系后再将这些模型导出供外部使用。

如果你使用是 egg-sequelize 框架,那么可以直接在模型文件中通过 associate 方法建立模型之间的关系,而且不会抛出上面的错误。