Node.js Sequelize 模型(表)之间的关联及关系模型的操作

 2016年07月07日    4495     声明


Sequelize模型之间存在关联关系,这些关系代表了数据库中对应表之间的主/外键关系。基于模型关系可以实现关联表之间的连接查询、更新、删除等操作。本文将通过一个示例,介绍模型的定义,创建模型关联关系,模型与关联关系同步数据库,及关系模型的增、删、改、查操作。

  1. 模型(表)之间的关联关系
  2. 关系模型(表)的操作(CRUD)

1. 模型(表)之间的关联关系

1.1 模型关系概述

数据库中的表之间存在一定的关联关系,表之间的关系基于主/外键进行关联、创建约束等。关系表中的数据分为1对1(1:1)、1对多(1:M)、多对多(N:M)三种关联关系。

Sequelize中建立关联关系,通过调用模型(源模型)的belongsTohasOnehasManybelongsToMany方法,再将要建立关系的模型(目标模型)做为参数传入即可。这些方法会按以下规则创建关联关系:

  • hasOne - 与目标模型建立1:1关联关系,关联关系(外键)存在于目标模型中。详见:Model.hasOne()
  • belongsTo - 与目标模型建立1:1关联关系,关联关系(外键)存在于源模型中。详见:Model.belongsTo()
  • hasMany - 与目标模型建立1:N关联关系,关联关系(外键)存在于目标模型中。详见:Model.hasMany()
  • belongsToMany - 与目标模型建立N:M关联关系,会通过sourceIdtargetId创建交叉表。详见:Model.belongsToMany()


1.2 定义关系模型

为了能够清楚说明模型关系的定义及关系模型的使用,我们定义如下4个模型对象:

  • 用户(User)-与其它模型存在1:11:NN:M
  • 用户登录信息(UserCheckin)-与User存在1:1关系
  • 用户地址(UserAddress)-与User存在N:1关系
  • 角色(Role)-与User存在N:M关系

这几个模型的E-R结构如下:

Node.js Sequelize 模型(表)之间的关联及关系模型的操作

定义User模型如下:

module.exports = function (sequelize, DataTypes) {
  return sequelize.define('User', {
    id:{type:DataTypes.BIGINT(11), autoIncrement:true, primaryKey : true, unique : true},
    username: { type: DataTypes.STRING,  allowNull: false, comment:'用户名' },
    password: { type: DataTypes.STRING, allowNull: false, comment:'用户密码' },
    active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, comment:'是否正常状态' }
  },
  {
    timestamps: true,
    underscored: true,
    paranoid: true,
    freezeTableName: true,
    tableName: 'user',
    charset: 'utf8',
    collate: 'utf8_general_ci'
});
}

在这个模型中,配置模型时,我们使用了以下配置:

  • timestamps: true-时间戳,启用该配置后会自动添加createdAtupdatedAt两个字段,分别表示创建和更新时间
  • underscored: true-使用下划线,自动添加的字段会在数据段中使用“蛇型命名”规则,如:createdAt在数据库中的字段名会是created_at
  • paranoid: true-虚拟删除。启用该配置后,数据不会真实删除,而是添加一个deletedAt属性

更多关于模型配置,请参考:配置模型

定义UserCheckin模型如下:

module.exports = function (sequelize, DataTypes) {
  return sequelize.define('UserCheckin', {
    id: { type: DataTypes.BIGINT(11), autoIncrement: true, primaryKey: true, unique: true },
    userId: { 
      type: DataTypes.BIGINT(11), 
      field: 'user_id',
      unique: true, 
      references: {
        model: 'User',
        key: 'id'
      },
      comment:'用户Id' },
    loginIp: { type: DataTypes.STRING, field: 'login_ip', allowNull: false, defaultValue: '' , validate: {isIP: true}, comment:'登录IP'}
  },
  {
    underscored: true,
    timestamps: true,
    tableName: 'userCheckin',
    comment: '用户登录信息',
    charset: 'utf8',
    collate: 'utf8_general_ci',
    indexes: [{
      name: 'userCheckin_userId',
      method: 'BTREE',
     fields: ['user_id']
    }]
  });
}

在定义这个模型时,我们通过references特性将userId定义为外键,并通过field特性将其在数据库中的字段名指定为user_id

定义UserAddress模型如下:

module.exports = function (sequelize, DataTypes) {
  return sequelize.define('UserAddress', {
    id: { type: DataTypes.BIGINT(11), autoIncrement: true, primaryKey: true, unique: true, comment:'主键' },
    userId: {type: DataTypes.BIGINT(11), field: 'user_id', allowNull: false, comment:'用户Id' },
    consignee : { type: DataTypes.STRING, field: 'consignee', allowNull: false, comment:'收货人' },
    address: { type: DataTypes.STRING(1024), field: 'address', allowNull: false, comment:'详细地址' },
    zipCode: { type: DataTypes.STRING(16), field: 'zip_code', allowNull: true, comment:'邮编' },
    tel: { type: DataTypes.STRING(32), field: 'tel', allowNull: false, comment:'电话' },
  },
  {
    underscore: true,
    timestamps: false,
    freezeTableName: true,
    tableName: 'userAddress',
    comment: '用户地址表',
    charset: 'utf8',
    collate: 'utf8_general_ci',
    indexes: [{
      name: 'userAddress_userId',
      method: 'BTREE',
      fields: ['user_id']
    }]
  });
}

User模型与UserAddress存在1:N的关联关系,但在这样我们并没有用references特性显式的指定外键。这是因为,Sequlieze不仅可以在模型定义时指定外键,还可以在建立模型关系时指定,甚至主外键关系并不需要显示的存在,只要在建立模型关系时指定关联键即可。

定义Role模型如下:

module.exports = function (sequelize, DataTypes) {
  return sequelize.define('Role', {
    id: { type: DataTypes.BIGINT(11), autoIncrement: true, primaryKey: true, unique: true, comment:'角色Id' },
    roleName: { type: DataTypes.STRING, field: 'role_name', comment:'角色名' }
  },
  {
    underscored: true,
    timestamps: false,
    freezeTableName: true,
    tableName: 'role',
    charset: 'utf8',
    collate: 'utf8_general_ci'
  });
}

Role模型与User存在N:M的关系,这样就需要两者通过一个关系表(关系模型)进行关联。但并不需要手工建立这个关系表,指定关联关系后Sequelize会自动创建关系表。

注意:在上面定义模型时,我们使用了comment属性添加字段描述。经测试及查看Sequlize源码,这一特性并不会向数据中添加相关描述信息,但仍然建议添加这一属性以增强代码的可读性。


更多关于模型定义的介绍,请参考:模型定义


1.3 模型关联与数据库同步

定义好模型后,就可以建立模型关联关系,并将模型及关系同步到数据库中。

模型导入

在上面定义模型时,我们每个模型定义为了单独的文件,这样就需要通过sequlize.import()方法导入模型:

var sequelize=require('./_db').sequelize();
var User = sequelize.import('./user.js');
var UserCheckin = sequelize.import('./userCheckin.js');
var UserAddress = sequelize.import('./userAddress.js');
var Role = sequelize.import('./role.js');

关系建立

导入后,建立模型关系:

// 建立模型之间的关系
User.hasOne(UserCheckin);
UserCheckin.belongsTo(User);
User.hasMany(UserAddress, {foreignKey:'user_id', targetKey:'id', as:'Address'});
User.belongsToMany(Role, {through: 'userRoles', as:'UserRoles'});
Role.belongsToMany(User, {through: 'userRoles', as:'UserRoles'});

在定义UserAddress模型时,我们没有定义关联模型,所以需要在hasMany()方法中通过foreignKeytargetKey来指定关联关系(主外键关系),指定后该关系同样会被同步到数据库中。除指定关联关系外,我们还指定了as选项,该选项表示“别名”,目标模型会混入到源模型后会使用该名称。

通过belongsToMany()方法建立RoleUser之间的关系时,设置了through选项,该选项表示“关系”(可以是一个模型或字符串,使用字符串时表示在数据库中表名)。

同步数据库

建立关联关系后,调用sequelize.sync()方法即可以将模型及关联关系同步到数据库中。

在本例中,相关操作定义在了index.js文件中,运行项后sync()方法会被调用,模型及关联关系会自动同步到数据库中。

同步结果

接下来,我们看一下同步结果。

运行项目后,数据库中会创建以下表:

mysql> show tables;
+---------------------+
| Tables_in_modeltest |
+---------------------+
| role                |
| user                |
| userAddress         |
| userCheckin         |
| userRoles           |
+---------------------+
5 rows in set (0.04 sec)

模型已经被同步到了数据库中。各表结构如下:

User模型所对应的user表:

mysql> desc user;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(11)   | NO   | PRI | NULL    | auto_increment |
| username   | varchar(255) | NO   |     | NULL    |                |
| password   | varchar(255) | NO   |     | NULL    |                |
| active     | tinyint(1)   | NO   |     | 1       |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
| deleted_at | datetime     | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
7 rows in set (0.03 sec)

除模型中定义的字段外,Sequlize还自动添加了created_at/updated_at/deleted_at三个字段,这与我们前面的模型配置有关。

UserAddress模型所对应的userAddress表:

mysql> desc userAddress;
+-----------+---------------+------+-----+---------+----------------+
| Field     | Type          | Null | Key | Default | Extra          |
+-----------+---------------+------+-----+---------+----------------+
| id        | bigint(11)    | NO   | PRI | NULL    | auto_increment |
| user_id   | bigint(11)    | YES  | MUL | NULL    |                |
| consignee | varchar(255)  | NO   |     | NULL    |                |
| address   | varchar(1024) | NO   |     | NULL    |                |
| zip_code  | varchar(16)   | YES  |     | NULL    |                |
| tel       | varchar(32)   | NO   |     | NULL    |                |
+-----------+---------------+------+-----+---------+----------------+
6 rows in set (0.01 sec)

由上可见,在建立模型时指定的外键约束,也被添加到了user_id字段中。

UserCheckin模型所对应的userCheckin表:

mysql> desc userCheckin;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(11)   | NO   | PRI | NULL    | auto_increment |
| user_id    | bigint(11)   | YES  | UNI | NULL    |                |
| login_ip   | varchar(255) | NO   |     |         |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
5 rows in set (0.01 sec)

Role模型所对应的role表:

mysql> desc role;
+-----------+--------------+------+-----+---------+----------------+
| Field     | Type         | Null | Key | Default | Extra          |
+-----------+--------------+------+-----+---------+----------------+
| id        | bigint(11)   | NO   | PRI | NULL    | auto_increment |
| role_name | varchar(255) | YES  |     | NULL    |                |
+-----------+--------------+------+-----+---------+----------------+
2 rows in set (0.01 sec)

定义Role模型时,设置了timestamps: false,所以并没有生成created_at/updated_at两个字段。

除前面定义4个模型所对应的表外,Sequelize还自动创建了一个关系表userRoles,该表使用UserRole两个表的外键做为联合主键。其结构如下:

mysql> desc userRoles;
+------------+------------+------+-----+---------+-------+
| Field      | Type       | Null | Key | Default | Extra |
+------------+------------+------+-----+---------+-------+
| created_at | datetime   | NO   |     | NULL    |       |
| updated_at | datetime   | NO   |     | NULL    |       |
| role_id    | bigint(11) | NO   | PRI | 0       |       |
| user_id    | bigint(11) | NO   | PRI | 0       |       |
+------------+------------+------+-----+---------+-------+
4 rows in set (0.01 sec)


2. 关系模型(表)的操作(CRUD)

为了方便操作,本示例以一个Web应用的方式提供,每个操作都做为一个单独的路由,实现详细请查看routes/index.js文件。运行项目后,在浏览器输入相应路径即可查看效果。

2.1 插入数据

单独插入数据

UserRole添加数据:

Promise.all([
  User.create({username:'itbilu', password:'itbilu.com'}),
  Role.create({roleName:'管理员'})
  ]).then(function(results){
  res.set('Content-Type', 'text/html; charset=utf-8')
  res.end('创建成功:'+JSON.stringify({user:results[0].dataValues, role:results[1].dataValues}));
}).catch(next);

运行项目,并访问以下路径可查看执行效果:

http://localhost:3000/

控制台打印,执行的SQL类似如下:

Executing (default): INSERT INTO `user` (`id`,`username`,`password`,`active`,`created_at`,`updated_at`) VALUES (DEFAULT,'itbilu','itbilu.com',true,'2016-07-07 10:00:11','2016-07-07 10:00:11');
Executing (default): INSERT INTO `role` (`id`,`role_name`) VALUES (DEFAULT,'管理员');

关联模型插入数据

被关联的“目标模型”可以调用其自身的create()等方法插入数据,可以通过“源模型”的模型实例设置器方法插入数据。

注意:定义模型的关联关系后,对于1:11:N关系模型,目标模型会做为源模型的一个实例属性提供,同时会相应的设置器方法源模型实例中;而对于N:M关系模型,源模型及目标模型会做为彼些的实例属性提供,并为双方相应的设置器方法

如:通过User实例,UserCheckin中插入数据:

User.create({username:'itbilu', password:'itbilu.com'}).then(function(user){

  var userCheckin = UserCheckin.build({loginIp:'127.0.0.1'});
  user.setUserCheckin(userCheckin);

  res.set('Content-Type', 'text/html; charset=utf-8');
  res.end('UserCheckin 插入数据成功');
}).catch(next);

访问URI:

http://localhost:3000/create/checkin

在上面setUserCheckin()操作中,会执行类似以下SQL语句:

INSERT INTO `userCheckin` (`id`,`login_ip`,`created_at`,`updated_at`,`user_id`) VALUES (DEFAULT,'127.0.0.1','2016-07-07 11:06:23','2016-07-07 11:06:23',26);

对于N:M关系的两个模型,如果未显式定义关系模型(关系表),就只能通过源模型实例或目标模型实例向数据库中的关系表插入数据。通过UserRole实例,向关系表插入数据:

Promise.all([
  User.create({username:'itbilu', password:'itbilu.com'}),
  Role.create({roleName:'管理员'})
]).then(function(results){
  var user = results[0];
  var role = results[1];
  user.setUserRoles(role);
  // 或
  // role.setUserRoles(user);
  res.set('Content-Type', 'text/html; charset=utf-8');
  res.end('userRoles 插入数据成功');
}).catch(next);

访问URI:

http://localhost:3000/create/userRoles

会执行类似如下SQL语句:

INSERT INTO `userRoles` (`user_id`,`role_id`,`created_at`,`updated_at`) VALUES (41,24,'2016-07-07 11:29:11','2016-07-07 11:29:11');


2.2 数据查询

对于1:1关联关系的模型,可以在查询时通过include指定要连接查询的模型。指定后Sequelize会自动生成连接查询语句:

User.findOne({include:[UserCheckin]}).then(function(user){
  console.log(user);
  res.set('Content-Type', 'text/html; charset=utf-8');
  res.end(JSON.stringify(user));
}).catch(next);

访问URI:

http://localhost:3000/select/user

生成的查询语句类型如下:

SELECT `User`.`id`, `User`.`username`, `User`.`password`, `User`.`active`, `User`.`created_at`, `User`.`updated_at`, `User`.`deleted_at`, `UserCheckin`.`id` AS `UserCheckin.id`, `UserCheckin`.`user_id` AS `UserCheckin.userId`, `UserCheckin`.`login_ip` AS `UserCheckin.loginIp`, `UserCheckin`.`created_at` AS `UserCheckin.created_at`, `UserCheckin`.`updated_at` AS `UserCheckin.updated_at`, `UserCheckin`.`user_id` AS `UserCheckin.user_id` FROM `user` AS `User` LEFT OUTER JOIN `userCheckin` AS `UserCheckin` ON `User`.`id` = `UserCheckin`.`user_id` WHERE `User`.`deleted_at` IS NULL LIMIT 1;

1:NN:M关系的模型,可以通过调用源模型实例的访问器方法查询目标模型。如,查询UserAddress

User.findOne().then(function(user){
	user.getAddress();
	res.set('Content-Type', 'text/html; charset=utf-8');
	res.end(JSON.stringify(user));
}).catch(next);

访问URI:

http://localhost:3000/select/userAddress

调用user.getAddress()时,会执行类似如下语句:

SELECT `id`, `user_id` AS `userId`, `consignee`, `address`, `zip_code` AS `zipCode`, `tel`, `user_id` FROM `userAddress` AS `UserAddress` WHERE `UserAddress`.`user_id` = 1;


2.3 数据更新

访问器方法同样可以用于关系模型的更新,使用设置器设置属性时,设置器方法首先会通过isNewRecord特性判断是否是新记录,从而进行插入数据或更新数据。

如,通过User实例更新UserCheckin

User.findOne({include:[UserCheckin]}).then(function(user){
  var userCheckin = UserCheckin.build({userId:user.id, loginIp:'192.168.0.1'});
  user.setUserCheckin(userCheckin);
  res.set('Content-Type', 'text/html; charset=utf-8');
  res.end(JSON.stringify(user));
}).catch(next);

访问URI:

http://localhost:3000/delete/user

会生成类似如下两条SQL语句:

UPDATE `userCheckin` SET `user_id`=NULL,`updated_at`='2016-07-07 14:12:07' WHERE `id` = 17;
INSERT INTO `userCheckin` (`id`,`user_id`,`login_ip`,`created_at`,`updated_at`) VALUES (DEFAULT,1,'192.168.0.1','2016-07-07 14:12:07','2016-07-07 14:12:07');


2.4 数据删除

对于逻辑删除的模(paranoid: true),删除时会向表中更新一个deleted_at时间戳。

删除User

User.destroy({where:{id:2}}).then(function(result){
  res.set('Content-Type', 'text/html; charset=utf-8');
  res.end('删除完成');	
}).catch(next);
// 使用模型实例删除
// User.findOne().then(function(user){
// 	user.destroy();
// 	res.set('Content-Type', 'text/html; charset=utf-8');
// 	res.end('删除完成');	
// }).catch(next);

访问URI:

http://localhost:3000/delete/user

逻辑删除相当于一个更新操作。生成的SQL语句类型如下:

UPDATE `user` SET `deleted_at`='2016-07-07 14:46:01' WHERE `deleted_at` IS NULL AND `id` = 2;