前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Sequelize 系列教程之一对多模型关系

Sequelize 系列教程之一对多模型关系

作者头像
阿宝哥
发布2019-11-06 12:34:17
12.3K0
发布2019-11-06 12:34:17
举报
文章被收录于专栏:全栈修仙之路

Sequelize 是一个基于 Promise 的 Node.js ORM,目前支持 Postgres、MySQL、SQLite 和 Microsoft SQL Server。它具有强大的事务支持,关联关系、读取和复制等功能。在阅读本文前,如果你对 Sequelize 还不了解,建议先阅读 Sequelize 快速入门 这篇文章。

数据模型中的表关系一般有三种:一对一、一对多、多对多。Sequelize 为开发者提供了清晰易用的接口来定义关系、进行表之间的操作。本文我们将介绍在 Sequelize 中如何定义一对多的表关系。

基本概念

Source & Target

我们首先从一个基本概念开始,你将会在大多数关联中使用 sourcetarget 模型。 假设您正试图在两个模型之间添加关联。 这里我们在 UserProject 之间添加一个 hasOne 关联。

代码语言:javascript
复制
const User = sequelize.define('User', {
  name: Sequelize.STRING,
  email: Sequelize.STRING
});

const Project = sequelize.define('Project', {
  name: Sequelize.STRING
});

User.hasOne(Project);

User 模型(函数被调用的模型)是 sourceProject 模型(作为参数传递的模型)是 target

HasMany

一对多关联将一个来源与多个目标连接起来。 而多个目标接到同一个特定的源。

代码语言:javascript
复制
const User = sequelize.define('user', {/* ... */})
const Project = sequelize.define('project', {/* ... */})
 
// 首先我们来定义一个 hasMany 关联
Project.hasMany(User, {as: 'Workers'})

这会将 projectId 属性添加到 User。 根据当前的设置,表中的列将被称为 projectIdproject_id。 Project 的实例将获得访问器 getWorkerssetWorkers

有时你可能需要在不同的列上关联记录,这时候你可以使用 sourceKey 选项:

代码语言:javascript
复制
const City = sequelize.define('city', { countryCode: Sequelize.STRING });
const Country = sequelize.define('country', { isoCode: Sequelize.STRING });

// 在这里,我们可以根据国家代码连接国家和城市
Country.hasMany(City, {foreignKey: 'countryCode', sourceKey: 'isoCode'});
City.belongsTo(Country, {foreignKey: 'countryCode', targetKey: 'isoCode'});

一对多关系

模型定义

model/user.js

代码语言:javascript
复制
const Sequelize = require("sequelize");

module.exports = sequelize => {
  const User = sequelize.define("user", {
    empId: {
      type: Sequelize.STRING,
      allowNull: false,
      unique: true
    }
  });

  return User;
};

model/note.js

代码语言:javascript
复制
const Sequelize = require("sequelize");

module.exports = sequelize => {
    const Note = sequelize.define("note", {
        title: {
            type: Sequelize.CHAR(64),
            allowNull: false
        }
    });

    return Note;
};
数据库连接及关系定义

db.js

代码语言:javascript
复制
const Sequelize = require('sequelize');

const sequelize = new Sequelize(
    'exe', // 数据库名称
    'root', // 用户名
    '', // 密码
   {
    host: 'localhost',
    dialect: 'mysql',
    operatorsAliases: false,

    pool: {
        max: 5,
        min: 0,
        acquire: 30000,
        idle: 10000
    }
});

sequelize
    .authenticate()
    .then(async () => {
        console.log('Connection has been established successfully.');
        const User = require('./model/user')(sequelize);
        const Note = require('./model/note')(sequelize);
    
        // User的实例对象将拥有:getNotes、setNotes、addNote、createNote、
        // removeNote、hasNote方法
        User.hasMany(Note);

        // Note的实例对象将拥有getUser、setUser、createUser方法
        Note.belongsTo(User);

        sequelize.sync({
              // force: true
            })
            .then(async () => {
                console.log(`Database & tables created!`);
            })
    })
    .catch(err => {
        console.error('Unable to connect to the database:', err);
    });

以上代码运行后,终端将会输出以下信息:

  1. 新建 users 表:
代码语言:javascript
复制
CREATE TABLE IF NOT EXISTS `users` (
  `id` INTEGER NOT NULL auto_increment , 
  `empId` VARCHAR(255) NOT NULL UNIQUE, 
  `createdAt` DATETIME NOT NULL, 
  `updatedAt` DATETIME NOT NULL, 
PRIMARY KEY (`id`)) ENGINE=InnoDB;
  1. 新建 notes 表:
代码语言:javascript
复制
CREATE TABLE IF NOT EXISTS `notes` (
  `id` INTEGER NOT NULL auto_increment , 
  `title` CHAR(64) NOT NULL, 
  `createdAt` DATETIME NOT NULL, 
  `updatedAt` DATETIME NOT NULL, 
  `userId` INTEGER, 
  PRIMARY KEY (`id`),
  FOREIGN KEY (`userId`) REFERENCES `users` (`id`) 
ON DELETE SET NULL ON UPDATE CASCADE) ENGINE=InnoDB;

通过观察上面的 notes 建表语句,我们发现 Sequelize 自动为 notes 表新增了 userId 字段,同时生成了相应的外键约束。

一般来说,外键约束可能会导致一些性能问题。所以,建表时我们一般会去掉约束,同时给外键加一个索引(加速查询),但之后的数据的一致性就需要应用层来保证了。

关系操作
  1. 新增

方式一

代码语言:javascript
复制
const user = await User.create({ empId: '1' }); // (1)
const note = await user.createNote({ title: 'learn sequelize' }); // (2)

步骤一:新建用户,对应的 SQL 语句如下:

代码语言:javascript
复制
INSERT INTO `users` (`id`,`empId`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'1','2018-10-10 07:42:26','2018-10-10 07:42:26');

步骤二:创建 Note,对应的 SQL 语句如下:

代码语言:javascript
复制
INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'learn sequelize','2018-10-10 07:42:26','2018-10-10 07:42:26',1);

可以看出,当调用 user.createNote 方法时,会使用新建用户的 userId 作为外键在 notes 表中插入一条新的数据。

方式二

代码语言:javascript
复制
const user = await User.create({ empId: '1' }); // (1)
const note = await Note.create({ title: 'learn sequelize' }); // (2)
await user.addNote(note); // (3)

步骤一:新建用户,对应的 SQL 语句如下:

代码语言:javascript
复制
INSERT INTO `users` (`id`,`empId`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'1','2018-10-10 07:53:26','2018-10-10 07:53:26');

步骤二:创建 Note,对应的 SQL 语句如下:

代码语言:javascript
复制
INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'learn sequelize','2018-10-10 07:53:26','2018-10-10 07:53:26');

以上 SQL 执行后,会插入一条 note 数据,但此时该条记录的外键 userId 为空。

步骤三:使用已创建用户的 id 值,设置步骤二 note 记录的外键 userId 的值,对应的 SQL 语句如下:

代码语言:javascript
复制
UPDATE `notes` SET `userId`=1,`updatedAt`='2018-10-10 07:53:26' WHERE `id` IN (1)
  1. 修改
代码语言:javascript
复制
const user = await User.create({ empId: '1' }); // (1)
const note1 = await user.createNote({ title: 'learn node.js' }); // (2)
const note2 = await user.createNote({ title: 'learn rx.js' }); // (3)

const note3 = await Note.create({ title: 'learn angular.js' }); // (4)
const note4 = await Note.create({ title: 'learn typescript.js' }); // (5)
await user.setNotes([note3, note4]); // (6)

步骤一:新建用户,对应的 SQL 语句如下:

代码语言:javascript
复制
INSERT INTO `users` (`id`,`empId`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'1','2018-10-10 08:09:13','2018-10-10 08:09:13');

步骤二与步骤三:创建 Note1 和 Note2,对应的 SQL 语句如下:

代码语言:javascript
复制
INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'learn node.js','2018-10-10 08:12:49','2018-10-10 08:12:49',1);

INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'learn rx.js','2018-10-10 08:12:49','2018-10-10 08:12:49',1);

步骤四与步骤五:创建 Note3 和 Note4,对应的 SQL 语句如下:

代码语言:javascript
复制
INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'learn angular.js','2018-10-10 08:12:49','2018-10-10 08:12:49');

INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'learn typescript.js','2018-10-10 08:12:49','2018-10-10 08:12:49');

步骤六:设置关联关系,执行流程及对应的 SQL 语句如下:

  • 查询 userId 为 1 的用户的所有 note 记录:
代码语言:javascript
复制
SELECT `id`, `title`, `createdAt`, `updatedAt`, `userId` FROM `notes` AS `note` WHERE `note`.`userId` = 1;
  • 将 note1、note2 记录的外键 userId 的值置为 NULL,切断之间的关系:
代码语言:javascript
复制
UPDATE `notes` SET `userId`=NULL,`updatedAt`='2018-10-10 08:12:49' WHERE `id` IN (1, 2)
  • 将 note3、note4 记录的外键 userId 的值置为当前用户的 id,完成关系的建立:
代码语言:javascript
复制
UPDATE `notes` SET `userId`=1,`updatedAt`='2018-10-10 08:12:49' WHERE `id` IN (3, 4)

因为我们需要根据传人 setNotes 的数组来计算出哪些 note 要切断关系、哪些要新增关系,所以就需要查出来进行一个计算集合的 “交集” 运算。

  1. 删除

方式一

代码语言:javascript
复制
const user = await User.create({ empId: '1' }); // (1)
const note1 = await user.createNote({ title: 'learn node.js' }); // (2)
const note2 = await user.createNote({ title: 'learn rx.js' }); // (3)
await user.setNotes([]); // (4)

步骤一至三的执行流程及对应 SQL 语句请参考修改环节,这里不再介绍。

步骤四:调用 user.setNotes([]) 方法,删除当前用户下的所有 note 记录,执行流程及对应的 SQL 语句如下:

  • 查询 userId 为 1 的用户的所有 note 记录:
代码语言:javascript
复制
SELECT `id`, `title`, `createdAt`, `updatedAt`, `userId` FROM `notes` AS `note` WHERE `note`.`userId` = 1;
  • userId 为 1 的用户的所有 note 记录的外键 userId 置为 NULL,切断关系:
代码语言:javascript
复制
UPDATE `notes` SET `userId`=NULL,`updatedAt`='2018-10-10 08:25:04' WHERE `id` IN (1, 2)

通过以上的 SQL 语句,我们知道调用 user.setNotes([]) 会删除当前用户下所关联的所有 note 记录,若需要删除指定 note 记录,则可以调用 user.removeNote 方法。

方式二

代码语言:javascript
复制
const user = await User.create({ empId: '1' }); // (1)
const note1 = await user.createNote({ title: 'learn node.js' }); // (2)
const note2 = await user.createNote({ title: 'learn rx.js' }); // (3)
user.removeNote(note2);

步骤一至三的执行流程及对应 SQL 语句请参考修改环节,这里不再介绍。

步骤四:调用 user.removeNote(note2) 方法,将删除当前用户下指定的 note2 记录,对应的 SQL 语句如下:

代码语言:javascript
复制
UPDATE `notes` SET `userId`=NULL,`updatedAt`='2018-10-10 08:38:40' WHERE `userId` = 1 AND `id` IN (2)
  1. 查询
  • 查询当前用户下所有满足条件的 note 数据:
代码语言:javascript
复制
const Op = Sequelize.Op
const user = await User.findById(1); // (1)
const notes = await user.getNotes({ // (2)
  where: {
    title: {
      [Op.like]: '%node%'
    }
  }
});

console.log(`User ${user.id}: has ${notes.length} notes`);

步骤一:查询 id 为 1 的用户,对应的 SQL 语句如下:

代码语言:javascript
复制
SELECT `id`, `empId`, `createdAt`, `updatedAt` FROM `users` AS `user` WHERE `user`.`id` = 1;

步骤二:根据查询条件,获取 id 为 1 的用户下的所有满足条件的 note 记录,对应的 SQL 语句如下:

代码语言:javascript
复制
SELECT `id`, `title`, `createdAt`, `updatedAt`, `userId` FROM `notes` AS `note` WHERE (`note`.`userId` = 1 AND `note`.`title` LIKE '%node%');
  • 查询所有满足条件的 note,同时获取 note 所属的 user:
代码语言:javascript
复制
const Op = Sequelize.Op
const notes = await Note.findAll({
  include: [User],
  where: {
    title: {
      [Op.like]: '%node%'
    }
 }
});

// 当前note属于哪个user可以通过note.user访问
console.log(`Has found ${notes.length} notes`);

以上操作对应的 SQL 语句如下:

代码语言:javascript
复制
SELECT `note`.`id`, `note`.`title`, `note`.`createdAt`, `note`.`updatedAt`, `note`.`userId`, `user`.`id` AS `user.id`, `user`.`empId` AS `user.empId`, `user`.`createdAt` AS `user.createdAt`, `user`.`updatedAt` AS `user.updatedAt` FROM 
`notes` AS `note` 
LEFT OUTER JOIN `users` AS `user` 
ON `note`.`userId` = `user`.`id` WHERE `note`.`title` LIKE '%node1%';
  • 查询所有满足条件的 user,同时获取该 user 所有满足条件的 note:
代码语言:javascript
复制
const Op = Sequelize.Op
const users = await User.findAll({
  include: [Note],
  where: {
    createdAt: {
      [Op.lt]: new Date()
    }
 }
});

// user的notes可以通过user.notes访问
console.log(`Has found ${users.length} users`);

以上操作对应的 SQL 语句如下:

代码语言:javascript
复制
SELECT `user`.`id`, `user`.`empId`, `user`.`createdAt`, `user`.`updatedAt`, `notes`.`id` AS `notes.id`, `notes`.`title` AS `notes.title`, `notes`.`createdAt` AS `notes.createdAt`, `notes`.`updatedAt` AS `notes.updatedAt`, `notes`.`userId` AS `notes.userId` FROM 
`users` AS `user` 
LEFT OUTER JOIN `notes` AS `notes` 
ON `user`.`id` = `notes`.`userId` WHERE `user`.`createdAt` < '2018-10-10 09:21:15';

这里需要注意的是,eager loadinginclude 传递的是需获取的相关模型,默认是获取全部,我们也可以根据实际需求再对这个模型进行一层过滤。比如:

代码语言:javascript
复制
const Op = Sequelize.Op
const users = await User.findAll({
  include: [{
    model: Note,
    where: {
      title: {
        [Op.like]: '%node%'
    }
   }
 }],
 where: {
   createdAt: {
     [Op.lt]: new Date()
   }
 }
});

以上操作对应的 SQL 语句如下:

代码语言:javascript
复制
SELECT `user`.`id`, `user`.`empId`, `user`.`createdAt`, `user`.`updatedAt`, `notes`.`id` AS `notes.id`, `notes`.`title` AS `notes.title`, `notes`.`createdAt` AS `notes.createdAt`, `notes`.`updatedAt` AS `notes.updatedAt`, `notes`.`userId` AS `notes.userId` FROM 
`users` AS `user` 
INNER JOIN `notes` AS `notes` 
ON `user`.`id` = `notes`.`userId` AND `notes`.`title` LIKE '%node%' WHERE `user`.`createdAt` < '2018-10-10 09:42:26';

当我们对 include 的模型加了 where 过滤条件时,会使用 inner join 来进行查询,这样保证只有那些拥有标题含有 node 关键词 note 的用户才会返回。关于各种 join 的区别,可以参考:a-visual-explanation-of-sql-joins

参考资源

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018/10/15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基本概念
    • Source & Target
      • HasMany
      • 一对多关系
        • 模型定义
          • 数据库连接及关系定义
            • 关系操作
            • 参考资源
            相关产品与服务
            云数据库 SQL Server
            腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档