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

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

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

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

BelongsTo

BelongsTo 关联是在 source model 上存在一对一关系的外键的关联。

一个简单的例子是 Player 通过 player 的外键作为 Team 的一部分。

代码语言:javascript
复制
const Player = this.sequelize.define('player', {/* attributes */});
const Team  = this.sequelize.define('team', {/* attributes */});

Player.belongsTo(Team); // 将向 Player 添加一个 teamId 属性以保存 Team 的主键值

默认情况下,将从目标模型名称和目标主键名称生成 belongsTo 关系的外键。默认的样式是 camelCase,但是如果源模型配置为 underscored: true ,那么将使用字段 snake_case 创建 foreignKey。比如:

代码语言:javascript
复制
const User = this.sequelize.define('user', {/* attributes */}, {underscored: true})
const Company  = this.sequelize.define('company', {
  uuid: {
    type: Sequelize.UUID,
    primaryKey: true
  }
});

// 将用字段 company_uuid 添加 companyUuid 到 user
User.belongsTo(Company);

此外,默认外键可以用 foreignKey 选项覆盖。 当设置外键选项时,Sequelize 将使用设置的参数值:

代码语言:javascript
复制
const User = this.sequelize.define('user', {/* attributes */})
const Company  = this.sequelize.define('company', {/* attributes */});

User.belongsTo(Company, { foreignKey: 'fk_company' }); // 将 fk_company 添加到 User
HasOne

HasOne 关联是在 target model 上存在一对一关系的外键的关联。

代码语言:javascript
复制
const User = sequelize.define('user', {/* ... */})
const Project = sequelize.define('project', {/* ... */})
 
// 单向关联
Project.hasOne(User)

以上示例中,hasOne 将向 User 模型添加一个 projectId 属性。此外,Project.prototype 将根据传递给定义的第一个参数获取 getUser 和 setUser 的方法。 如果启用了 underscore 样式,则添加的属性将是 project_id 而不是 projectId。外键将放在 users 表上。

你也可以定义外键,比如如果你已经有一个现有的数据库并且想要处理它:

代码语言:javascript
复制
Project.hasOne(User, { foreignKey: 'initiator_id' })
HasOne vs BelongsTo

在 Sequelize 1:1 关系中可以使用 HasOne 和 BelongsTo 进行设置,它们适用于不同的场景。

我们先来定义以下两个模型:

代码语言:javascript
复制
const Player = this.sequelize.define('player', {/* attributes */})
const Team  = this.sequelize.define('team', {/* attributes */});

当我们连接 Sequelize 中的两个模型时,我们可以将它们称为一对 sourcetarget 模型。

Player 作为 sourceTeam 作为 target

代码语言:javascript
复制
Player.belongsTo(Team);
//或
Player.hasOne(Team);

Team 作为 sourcePlayer 作为 target

代码语言:javascript
复制
Team.belongsTo(Player);
//Or
Team.hasOne(Player);

HasOne 和 BelongsTo 将关联键插入到不同的模型中。 HasOne 在 target 模型中插入关联键,而 BelongsTo 将关联键插入到 source 模型中。

一对一关系

模型定义

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/account.js

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

module.exports = sequelize => {
    const Account = sequelize.define("account", {
        email: {
            type: Sequelize.CHAR(20),
            allowNull: false
        }
    });

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

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 Account = require('./model/account')(sequelize);

        sequelize.sync({
             // force: true 
            })
            .then(() => {
                console.log(`Database & tables created!`)
            	// User的实例对象将拥有getAccount、setAccount、createAccount方法
        		User.hasOne(Account); // 在target模型中插入关联键
        		// Account的实例对象将拥有getUser、setUser、createUser方法
        		Account.belongsTo(User); // 将关联键插入到source模型中
            })
    })
    .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. 新建 accounts 表
代码语言:javascript
复制
CREATE TABLE IF NOT EXISTS `accounts` (
  `id` INTEGER NOT NULL auto_increment , 
  `email` CHAR(20) 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;

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

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

关系操作
  1. 新增
代码语言:javascript
复制
const user = await User.create({ empId: '1' }); // (1)
const account = await user.createAccount({ email: 'semlinker@gmail.com' }); // (2)
console.log(account.get({ plain: true }));

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

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

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

代码语言:javascript
复制
INSERT INTO `accounts` (`id`,`email`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'semlinker@gmail.com','2018-10-09 04:18:23','2018-10-09 04:18:23',1);

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

  1. 修改
代码语言:javascript
复制
const user = await User.findById(1); // (1)
const newAccount = await Account.create({ email: 'sequelize@gmail.com' }); // (2)
user.setAccount(newAccount); // (3)
console.log(newAccount.get({ plain: true }));

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

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

步骤二:创建新账号,对应的 SQL 语句如下:

代码语言:javascript
复制
INSERT INTO `accounts` (`id`,`email`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'sequelize@gmail.com','2018-10-09 05:46:11','2018-10-09 05:46:11');

该 SQL 语句会插入一条新的 account 记录,此时 userId 的值为空,还未关联 user。

步骤三:关联新的账号,对应的 SQL 语句如下:

代码语言:javascript
复制
UPDATE `accounts` SET `userId`=NULL,`updatedAt`='2018-10-09 05:46:11' WHERE `id` = 1

UPDATE `accounts` SET `userId`=1,`updatedAt`='2018-10-09 05:46:11' WHERE `id` = 2

以上 SQL 语句,首先会找出当前 user 所关联的 account 并将其 userId 设置为 NULL (为了保证一对一关系)。

然后设置新的 account 的外键 userId 为当前 user 的 id,从而建立关系。

  1. 删除
代码语言:javascript
复制
const user = await User.findById(1); // (1)
user.setAccount(null); // (2)

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

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

步骤二:查找 userId 为 1 的账号,对应的 SQL 语句如下:

代码语言:javascript
复制
SELECT `id`, `email`, `createdAt`, `updatedAt`, `userId` FROM `accounts` AS `account` WHERE `account`.`userId` = 1 LIMIT 1;

步骤三:当 userId 的账号存在时,才会执行该步骤,即更新相应的 account 记录,对应的 SQL 语句如下:

代码语言:javascript
复制
UPDATE `accounts` SET `userId`=NULL,`updatedAt`='2018-10-09 06:19:30' WHERE `id` = 2

通过观察以上的 SQL 语句,我们发现执行删除操作时,并不会真正的删除物理记录,只是执行对应的软删除操作。即通过将外键 userId 设置为 NULL,完成表关系的切除。

  1. 查询
代码语言:javascript
复制
const user = await User.findById(1); // (1)
user.getAccount(); // (2)

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

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

步骤二:获取 id 为 1 的用户相关联的账号,对应的 SQL 语句如下:

代码语言:javascript
复制
SELECT `id`, `email`, `createdAt`, `updatedAt`, `userId` FROM `accounts` AS `account` WHERE `account`.`userId` = 1 LIMIT 1;

以上的 SQL 语句就是根据外键 userId 来获取相关联的 account。

eager loading

对于开发者来说,我们更习惯通过 . 操作来快速访问对象的属性,比如 user.account。前面我们就已经提到过 Sequelize 功能很强大,它当然也支持这种操作。但需要借助 Sequelizeeager loading(急加载,和懒加载相反)特性来实现。eager loading 的含义是说,取一个模型的时候,同时也自动获取相关的模型数据。

代码语言:javascript
复制
const user = await User.findById(1, {
  include: [Account]
});
console.log(user.get({ plain: true }));

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

代码语言:javascript
复制
SELECT `user`.`id`, `user`.`empId`, `user`.`createdAt`, `user`.`updatedAt`, `account`.`id` AS `account.id`, `account`.`email` AS `account.email`, `account`.`createdAt` AS `account.createdAt`, `account`.`updatedAt` AS `account.updatedAt`, `account`.`userId` AS `account.userId` FROM `users` AS `user` LEFT OUTER JOIN `accounts` AS `account` ON `user`.`id` = `account`.`userId` WHERE `user`.`id` = 1;

即通过左外连接在获取 id 为 1 的用户时,同时获取其关联的账号。此外,命令行还会输出相应的 user 对象:

代码语言:javascript
复制
{ 
  id: 1,
  empId: '1',
  createdAt: 2018-10-09T04:18:23.000Z,
  updatedAt: 2018-10-09T04:18:23.000Z,
  account:
   { id: 3,
     email: 'sequelize@gmail.com',
     createdAt: 2018-10-09T06:49:44.000Z,
     updatedAt: 2018-10-09T06:49:44.000Z,
     userId: 1 
   } 
}
相关说明
  1. 要避免重复调用 user.createAccount 方法,这样会在数据库里面生成多条 userId 一样的记录,并不是真正的一对一关系。
代码语言:javascript
复制
const user = await User.findById(1); // (1)
const account1 = await user.createAccount({ email: 'test1@gmail.com' }); // (2)
const account2 = await user.createAccount({ email: 'test2@gmail.com' }); // (3)

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

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

步骤二:为 id 为 1 的用户,创建新的账号,对应的 SQL 语句如下:

代码语言:javascript
复制
INSERT INTO `accounts` (`id`,`email`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'test1@gmail.com','2018-10-09 07:05:57','2018-10-09 07:05:57',1);

步骤三:为 id 为 1 的用户,创建新的账号,对应的 SQL 语句如下:

代码语言:javascript
复制
INSERT INTO `accounts` (`id`,`email`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,'test2@gmail.com','2018-10-09 07:05:57','2018-10-09 07:05:57',1);

上面的 SQL 成功执行之后 accounts 表将会生成两条新记录,具体如下:

id

email

createdAt

updatedAt

userId

4

test1@gmail.com

2018-10-09 07:05:57

2018-10-09 07:05:57

1

5

test2@gmail.com

2018-10-09 07:05:57

2018-10-09 07:05:57

1

可以看到上面并不是我们想要的结果,在应用层要保证数据一致性,我们就需要遵循良好的编码约定。新增用户账号时使用 user.createAccount 方法,更新用户账号时就使用 user.setAccount 方法。

当然也可以为 account 表的 userId 字段,增加一个 UNIQUE 唯一约束,在数据库层面保证一致性,这时就需要做好 try/catch,发生插入异常的时候能够知道是因为插入了为同一用户创建了多个账号。具体的实现方式如下:

model/account.js

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

module.exports = sequelize => {
    const Account = sequelize.define("account", {
        email: {
            type: Sequelize.CHAR(20),
            allowNull: false
        },
        userId: {
            type: Sequelize.INTEGER,
            unique: true
        },
    });

    return Account;
};

定义一对一的表关系:

代码语言:javascript
复制
// User的实例对象将拥有getAccount、setAccount、createAccount方法
User.hasOne(Account, {
  foreignKey: 'userId'
});
  1. 上面的示例,我们都是通过 user 对象来操作 account。实际上也可以通过 account 来操作 user,这是因为我们定义了 Account.belongsTo(User)。在 Sequelize 里面定义关系时,关系的调用方会获得相关联的方法,一般为了两边都能操作,会同时定义双向关系(这里双向关系指的是模型层面,并不会在数据库表中出现两个表都加上外键的情况)。

参考资源

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

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

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

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

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