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

栏目: 数据库 · 发布时间: 6年前

内容简介:数据模型中的表关系一般有三种:一对一、一对多、多对多。我们首先从一个基本概念开始,你将会在大多数关联中使用

Sequelize 是一个基于 Promise 的 Node.js ORM,目前支持 Postgres、 MySQLSQLite 和 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

HasMany

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

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

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

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

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

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

const Sequelize = require("sequelize");

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

    return Note;
};

数据库连接及关系定义

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 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 表:
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 表:
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. 新增

方式一

const user = await User.create({ empId: '1' }); // (1)
const note = await user.createNote({ title: 'learn sequelize' }); // (2)

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

INSERT INTO `users` (`id`,`empId`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'1','2018-10-10 07:42:26','2018-10-10 07:42:26');

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

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 表中插入一条新的数据。

方式二

const user = await User.create({ empId: '1' }); // (1)
const note = await Note.create({ title: 'learn sequelize' }); // (2)
await user.addNote(note); // (3)

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

INSERT INTO `users` (`id`,`empId`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'1','2018-10-10 07:53:26','2018-10-10 07:53:26');

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

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 语句如下:

UPDATE `notes` SET `userId`=1,`updatedAt`='2018-10-10 07:53:26' WHERE `id` IN (1)
  1. 修改
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 语句如下:

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 语句如下:

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 语句如下:

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 记录:
SELECT `id`, `title`, `createdAt`, `updatedAt`, `userId` FROM `notes` AS `note` WHERE `note`.`userId` = 1;
  • 将 note1、note2 记录的外键 userId 的值置为 NULL,切断之间的关系:
UPDATE `notes` SET `userId`=NULL,`updatedAt`='2018-10-10 08:12:49' WHERE `id` IN (1, 2)
  • 将 note3、note4 记录的外键 userId 的值置为当前用户的 id,完成关系的建立:
UPDATE `notes` SET `userId`=1,`updatedAt`='2018-10-10 08:12:49' WHERE `id` IN (3, 4)

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

  1. 删除

方式一

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 记录:
SELECT `id`, `title`, `createdAt`, `updatedAt`, `userId` FROM `notes` AS `note` WHERE `note`.`userId` = 1;
  • userId 为 1 的用户的所有 note 记录的外键 userId 置为 NULL,切断关系:
UPDATE `notes` SET `userId`=NULL,`updatedAt`='2018-10-10 08:25:04' WHERE `id` IN (1, 2)

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

方式二

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 语句如下:

UPDATE `notes` SET `userId`=NULL,`updatedAt`='2018-10-10 08:38:40' WHERE `userId` = 1 AND `id` IN (2)
  1. 查询
  • 查询当前用户下所有满足条件的 note 数据:
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 语句如下:

SELECT `id`, `empId`, `createdAt`, `updatedAt` FROM `users` AS `user` WHERE `user`.`id` = 1;

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

SELECT `id`, `title`, `createdAt`, `updatedAt`, `userId` FROM `notes` AS `note` WHERE (`note`.`userId` = 1 AND `note`.`title` LIKE '%node%');
  • 查询所有满足条件的 note,同时获取 note 所属的 user:
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 语句如下:

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:
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 语句如下:

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 传递的是需获取的相关模型,默认是获取全部,我们也可以根据实际需求再对这个模型进行一层过滤。比如:

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

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

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Writing Windows VxDs and Device Drivers, Second Edition

Writing Windows VxDs and Device Drivers, Second Edition

Karen Hazzah / CMP / 1996-01-12 / USD 54.95

Software developer and author Karen Hazzah expands her original treatise on device drivers in the second edition of "Writing Windows VxDs and Device Drivers." The book and companion disk include the a......一起来看看 《Writing Windows VxDs and Device Drivers, Second Edition》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具