专栏首页全栈修仙之路Sequelize 系列教程之多对多模型关系

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

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

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

基本概念

Source & Target

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

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

belongsToMany

多对多关联用于将源与多个目标相连接。 此外,目标也可以连接到多个源。

Project.belongsToMany(User, { through: 'UserProject' });
User.belongsToMany(Project, { through: 'UserProject' });

这将创建一个名为 UserProject 的新模型,具有等效的外键 projectIduserId。 属性是否为 camelcase 取决于由表(在这种情况下为 UserProject )连接的两个模型。

  • User.belongsToMany(Project, {through: 'UserProject'}) —— 将添加方法 getUsers, setUsers, addUser, addUsersProject 上。
  • User.belongsToMany(Project, {through: 'UserProject'}) —— 将添加方法 getPorjects, setProjects, addProject, addProjectsUser 上。

有时,您可能需要在关联中使用它们时重命名模型。 让我们通过使用别名(as)选项将 users 定义为 workers 而 projects 定义为 tasks。 我们还将手动定义要使用的外键:

User.belongsToMany(Project, { as: 'Tasks', through: 'worker_tasks', foreignKey: 
  'userId' });

Project.belongsToMany(User, { as: 'Workers', through: 'worker_tasks', foreignKey:   
  'projectId' })

如果你想要连接表中的其他属性,则可以在定义关联之前为连接表定义一个模型,然后再说明它应该使用该模型进行连接,而不是创建一个新的关联:

const User = sequelize.define('user', {})
const Project = sequelize.define('project', {})
const UserProjects = sequelize.define('userProjects', {
    status: DataTypes.STRING
})
 
User.belongsToMany(Project, { through: UserProjects })
Project.belongsToMany(User, { through: UserProjects })

默认情况下,上面的代码会将 projectId 和 userId 添加到 UserProjects 表中, 删除任何先前定义的主键属性 - 表将由两个表的键的组合唯一标识,并且没有其他主键列。 若需要在 UserProjects 模型上添加一个主键,你可以手动添加它。

const UserProjects = sequelize.define('userProjects', {
  id: {
    type: Sequelize.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  status: DataTypes.STRING
})

使用多对多你可以基于 through 关系查询并选择特定属性,比如:

User.findAll({
  include: [{
    model: Project,
    through: {
      attributes: ['createdAt', 'startedAt', 'finishedAt'],
      where: {completed: true}
    }
  }]
});

多对多关系

模型定义

model/note.js

const Sequelize = require("sequelize");

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

    return Note;
};

model/tag.js

const Sequelize = require("sequelize");

module.exports = sequelize => {
    const Tag = sequelize.define("tag", {
        name: {
            type: Sequelize.CHAR(64),
            allowNull: false,
            unique: true
        }
    });

    return Tag;
};

model/tagging.js

const Sequelize = require("sequelize");

module.exports = sequelize => {
    const Tagging = sequelize.define("tag", {
        type: {
            type: Sequelize.INTEGER,
            allowNull: false
        }
    });

    return Tagging;
};

数据库连接及关系定义

db.js

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 Note = require('./model/note')(sequelize);
        const Tag = require('./model/tag')(sequelize);
        const Tagging = require('./model/tagging')(sequelize);
    
        // Note的实例拥有getTags、setTags、addTag、addTags、createTag、
        // removeTag、hasTag方法
        Note.belongsToMany(Tag, { through: Tagging });

        // Tag的实例拥有getNotes、setNotes、addNote、addNotes、createNote、
        // removeNote、hasNote方法
        Tag.belongsToMany(Note, { through: Tagging });

        sequelize.sync({
                force: true
            })
            .then(async () => {
                
            })
    })
    .catch(err => {
        console.error('Unable to connect to the database:', err);
    });

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

  1. 新建 notes 表
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, 
PRIMARY KEY (`id`)) ENGINE=InnoDB;
  1. 新建 tags 表
CREATE TABLE IF NOT EXISTS `tags` (
  `id` INTEGER NOT NULL auto_increment , 
  `type` INTEGER NOT NULL, 
  `createdAt` DATETIME NOT NULL, 
  `updatedAt` DATETIME NOT NULL, 
PRIMARY KEY (`id`)) ENGINE=InnoDB;
  1. 新建 taggings 表
CREATE TABLE IF NOT EXISTS `taggings` (
  `type` INTEGER NOT NULL,
  `createdAt` DATETIME NOT NULL, 
  `updatedAt` DATETIME NOT NULL, 
  `noteId` INTEGER , 
  `tagId` INTEGER , 
PRIMARY KEY (`noteId`, `tagId`), 
FOREIGN KEY (`noteId`) REFERENCES `notes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (`tagId`) REFERENCES `tags` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB;

可以看到,多对多关系中我们单独生成了一张关系表,并设置了 2 个外键 tagIdnoteId 来和 tagsnotes 进行关联。

关系操作

  1. 新增

方式一

const note = await Note.create({ title: 'note' }); // (1)
await note.createTag({ name: 'tag' }, { through: { type: 0 }}); // (2)

步骤一:新增 note 记录,对应的 SQL 语句如下:

INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'note','2018-10-12 09:19:11','2018-10-12 09:19:11');

步骤二(1):新建 tag 记录,对应的 SQL 语句如下:

INSERT INTO `tags` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'tag','2018-10-12 09:19:11','2018-10-12 09:19:11');

步骤二(2):新建 tagging 记录,对应的 SQL 语句如下:

INSERT INTO `taggings` (`type`,`createdAt`,`updatedAt`,`noteId`,`tagId`) VALUES (0,'2018-10-12 09:19:11','2018-10-12 09:19:11',1,1);

关系表本身需要的属性,通过传递一个额外的对象给设置方法来实现。

方式二

const note = await Note.create({ title: 'note' });
const tag = await Tag.create({ name: 'tag' });
await note.addTag(tag, { through: { type: 0 } });

这种方法和上面的方法实际上是一样的。只是我们先手动 create 了一个 Tag 模型。

方式三

const note = await Note.create({ title: 'note' }); // (1)
const tag1 = await Tag.create({ name: 'tag1' }); // (2)
const tag2 = await Tag.create({ name: 'tag2' }); // (3)
await note.addTags([tag1, tag2], { through: { type: 2 }}); // (4)

步骤一:新增 note 记录,对应的 SQL 语句如下:

INSERT INTO `notes` (`id`,`title`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'note','2018-10-12 11:33:17','2018-10-12 11:33:17');

步骤二:新建第一条 tag 记录,对应的 SQL 语句如下:

INSERT INTO `tags` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'tag1','2018-10-12 11:33:17','2018-10-12 11:33:17');

步骤三:新建第二条 tag 记录,对应的 SQL 语句如下:

INSERT INTO `tags` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'tag2','2018-10-12 11:33:17','2018-10-12 11:33:17');

步骤四:新增两条 tagging 记录,对应的 SQL 语句如下:

INSERT INTO `taggings` (`type`,`createdAt`,`updatedAt`,`noteId`,`tagId`) VALUES (2,'2018-10-12 11:33:17','2018-10-12 11:33:17',1,1),(2,'2018-10-12 11:33:17','2018-10-12 11:33:17',1,2);
  1. 修改
const note = await Note.create({ title: 'note' });
const tag1 = await Tag.create({ name: 'tag1'});
const tag2 = await Tag.create({ name: 'tag2'});
await note.addTags([tag1, tag2], { through: { type: 2 }});

const tag3 = await Tag.create({ name: 'tag3'}); // (1)
const tag4 = await Tag.create({ name: 'tag4'}); // (2)
await note.setTags([tag3, tag4], { through: { type: 3 }}); // (3)

步骤一:新建第三条 tag 记录,对应的 SQL 语句如下:

INSERT INTO `tags` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'tag3','2018-10-12 11:43:16','2018-10-12 11:43:16');

步骤二:新建第四条 tag 记录,对应的 SQL 语句如下:

INSERT INTO `tags` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'tag4','2018-10-12 11:43:16','2018-10-12 11:43:16');

步骤三(1):删除当前 note 记录,与 tag1、tag2 之间的关联信息,对应的 SQL 语句如下:

DELETE FROM `taggings` WHERE `noteId` = 1 AND `tagId` IN (1, 2)

步骤三(2):设置当前 note 记录,与 tag3、tag4 之间的关联信息,对应的 SQL 语句如下:

INSERT INTO `taggings` (`type`,`createdAt`,`updatedAt`,`noteId`,`tagId`) VALUES (3,'2018-10-12 11:43:16','2018-10-12 11:43:16',1,3),(3,'2018-10-12 11:43:16','2018-10-12 11:43:16',1,4);
  1. 删除

删除单条记录

const note = await Note.create({ title: 'note' });
const tag1 = await Tag.create({ name: 'tag1' });
const tag2 = await Tag.create({ name: 'tag2' });
await note.addTags([tag1, tag2], { through: { type: 2 }});

await note.removeTag(tag1); // (1)

步骤一:删除 tag1 记录,对应的 SQL 语句如下:

DELETE FROM `taggings` WHERE `noteId` = 1 AND `tagId` IN (1)

删除单条记录很简单,直接将关系表 taggings 中的数据删除。

全部删除

const note = await Note.create({ title: 'note' });
const tag1 = await Tag.create({ name: 'tag1' });
const tag2 = await Tag.create({ name: 'tag2' });
await note.addTags([tag1, tag2], { through: { type: 2 }});

await note.setTags([]); // (1)

步骤一(1):查询关系表 taggings 中与当前 note 相关的记录,对应的 SQL 语句如下:

SELECT `type`, `createdAt`, `updatedAt`, `noteId`, `tagId` FROM `taggings` AS `tagging` WHERE `tagging`.`noteId` = 1;

步骤一(2):删除所有匹配的数据,对应的 SQL 语句如下:

DELETE FROM `taggings` WHERE `noteId` = 1 AND `tagId` IN (1, 2)
  1. 查询
  • 查询当前 note 中所有满足条件的 tag:
const Op = Sequelize.Op
const tags = await note.getTags({
  where: {
    name: {
      [Op.like]: 'tag%'
    }
  }
});

console.log(`Note has ${tags.length} tags`);

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

SELECT `tag`.`id`, `tag`.`name`, `tag`.`createdAt`, `tag`.`updatedAt`, `tagging`.`type` AS `tagging.type`, `tagging`.`createdAt` AS `tagging.createdAt`, `tagging`.`updatedAt` AS `tagging.updatedAt`, `tagging`.`noteId` AS `tagging.noteId`, `tagging`.`tagId` AS `tagging.tagId` FROM `tags` AS `tag` 
INNER JOIN `taggings` AS `tagging` 
ON `tag`.`id` = `tagging`.`tagId`
AND `tagging`.`noteId` = 1 WHERE (`tag`.`name` LIKE 'tag%');
  • 查询所有满足条件的 tag,同时获取每个 tag 所在的 note:
const tags = await Tag.findAll({
  include: {
    model: Note
  }
});

// tag的notes可以通过tag.notes访问,关系模型可以通过tag.notes[0].tagging访问
console.log(`Has found ${tags.length} tags`);

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

SELECT `tag`.`id`, `tag`.`name`, `tag`.`createdAt`, `tag`.`updatedAt`, `notes`.`id` AS `notes.id`, `notes`.`title` AS `notes.title`, `notes`.`createdAt` AS `notes.createdAt`, `notes`.`updatedAt` AS `notes.updatedAt`, `notes->tagging`.`type` AS `notes.tagging.type`, `notes->tagging`.`createdAt` AS `notes.tagging.createdAt`, `notes->tagging`.`updatedAt` AS `notes.tagging.updatedAt`, `notes->tagging`.`noteId` AS `notes.tagging.noteId`, `notes->tagging`.`tagId` AS `notes.tagging.tagId` FROM `tags` AS `tag` 
LEFT OUTER JOIN 
( `taggings` AS `notes->tagging` INNER JOIN `notes` AS `notes` 
   ON 
   `notes`.`id` = `notes->tagging`.`noteId`
) ON `tag`.`id` = `notes->tagging`.`tagId`;

首先是 notestaggings 进行了一个inner join,选出 notes ,然后 tags 和刚 join 出的集合再做一次 left join,得到结果。

  • 查询所有满足条件的 note,同时获取每个 note 相关联的 tag:
const notes = await Note.findAll({
  include: [
    {
       model: Tag // 支持tags设置查询条件
    }
  ]
});

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

SELECT `note`.`id`, `note`.`title`, `note`.`createdAt`, `note`.`updatedAt`, `tags`.`id` AS `tags.id`, `tags`.`name` AS `tags.name`, `tags`.`createdAt` AS `tags.createdAt`, `tags`.`updatedAt` AS `tags.updatedAt`, `tags->tagging`.`type` AS `tags.tagging.type`, `tags->tagging`.`createdAt` AS `tags.tagging.createdAt`, `tags->tagging`.`updatedAt` AS `tags.tagging.updatedAt`, `tags->tagging`.`noteId` AS `tags.tagging.noteId`, `tags->tagging`.`tagId` AS `tags.tagging.tagId` FROM `notes` AS `note` 
LEFT OUTER JOIN 
( `taggings` AS `tags->tagging` INNER JOIN `tags` AS `tags` 
   ON 
  `tags`.`id` = `tags->tagging`.`tagId`
) 
ON `note`.`id` = `tags->tagging`.`noteId`;

首先是 tagstaggins 进行了一个 inner join,选出 tags,然后 notes 和刚 join 出的集合再做一次 left join,得到结果。

参考资源

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Node.js 小打小闹之图片合成

    前阵子公司的产品经理找我谈个需求,希望能为每个用户生成专属的资讯分享图片及让开通专栏的用户能够生成专属的文章分享图片。这两天刚好有空,就抽空预研了 “生成专属的...

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

    Sequelize 是一个基于 Promise 的 Node.js ORM,目前支持 Postgres、MySQL、SQLite 和 Microsoft SQL...

    semlinker
  • Angular Title Service 详解

    Angular Title Service 用于获取和设置当前 HTML 文档的标题。Title Service 提供了以下方法:

    semlinker
  • 计算机网络基础知识

    计算机自诞生伊始,经历了一系列演变与发展。大型通用机计算机、超级计算机、小型机、个人电脑、工作站、便携式电以及现如今的智能手机终端都是这一过程的产物。它们性能逐...

    HACK学习
  • Tomcat Server处理一个http请求过程

    假设来自客户端的请求为:          http://localhost:8080/lizhx/lizhx_index.jsp 请求被发送到本机端口8080...

    YGingko
  • Apache虚拟主机-解惑篇

        有很多平时喜欢钻研的童鞋会发现,为什么有时候自己访问某XXse网站时,总是更新IP地址,内容却与以前一样。这个时候就要了解虚拟主机的概念了。了解这个概...

    用户1154259
  • 网络虚拟化是多云控制的关键

    现在很多IT企业在云计算时代逐渐失去对正在开发和部署在公有云中的应用程序的控制,形成这种现象的原因是每个公有云对网络来说都是一个孤岛。 ? 网络虚拟化(NV)o...

    SDNLAB
  • shell+curl监控网站页面(域名访问状态),并利用sedemail发送邮件

    应领导要求,对公司几个主要站点的域名访问情况进行监控。下面分享一个监控脚本,并利用sendemail进行邮件发送。 监控脚本如下: 下面是写了一个多线程的网站状...

    洗尽了浮华
  • 【Python3】04、内置数据结构

    1、把字符串形式的整数或浮点数转化为int或float, 不适用int和float函数

    用户2398817
  • 使用高级网络数据包代理(NPB)进行LTE GTP负载均衡

    随着微信、淘宝、微博、抖音等最终用户对高带宽的需求的增长,对复杂的网络管理和流量监控的需求也随之增加。 服务提供商希望通过主动识别网络中的问题,...

    用户6792078

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动