这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战
一、模型关联
Sequelize 提供了 四种 关联类型, 分别为:
HasOne关联类型BelongsTo关联类型HasMany关联类型BelongsToMany关联类型
1. 一对一关联(HasOne、BelongsTo)
一对一是比较简单的关联关系,一般用于某张数据表的扩展表与主表之间的关联关系。例如系统中有用户表 user, 有用户的基本信息,在后面产品迭代中增加了微信登录功能,通常会增加一张微信用户扩展表 wechat_user ,来存储用户的微信信息。当我们需要微信信息的时候才去查询,这样的场景就可以对两个模型创建一对一关联。
-
表结构及模型创建
user模型
// app/model/user.js module.exports = app => { const { STRING, INTEGER, DATE } = app.Sequelize; const User = app.model.define('user', { id: { type: INTEGER, primaryKey: true, autoIncrement: true, }, nickname: STRING(20), password: STRING(32), create_time: DATE, update_time: DATE, delete_time: DATE, }, { freezeTableName: false, tableName: 'user', underscored: false, paranoid: true, timestamps: true, createdAt: 'create_time', updatedAt: false, deletedAt: 'delete_time', }); return User; };-
wechat_user模型
// app/model/wechat_user.js module.exports = app => { const { STRING, INTEGER, DATE } = app.Sequelize; const WechatUser = app.model.define('wechatUser', { id: { type: INTEGER, primaryKey: true, autoIncrement: true, }, user_id: INTEGER, nickname: STRING, avatar_url: STRING, open_id: STRING, gender: INTEGER, create_time: DATE, }, { freezeTableName: false, tableName: 'wechat_user', underscored: false, timestamps: true, createdAt: 'create_time', updatedAt: false, deletedAt: false, }); return WechatUser; };-
模型参数说明:
freezeTableName : 强制表名称等于模型名称,示例中设为false,配合
tableName直接告诉 Sequelize 表名称。underscored : 列字段名称是否使用驼峰转下划线。
timestamps : 是否自动写入时间,配合
createdAt,updatedAt使用,创建或更新内容时,这些字段都会被自动设置。
-
创建关联关系(HasOne)
修改
User模型,在模型中增加如下内容,指定关联关系// app/model/user.js module.exports = app => { ... const User = ... ... User.associate = () => { app.model.User.hasOne(app.model.WechatUser, { as: 'wechat_user', foreignKey: 'user_id', sourceKey: 'id', }); }; return User; }as : 别名,相当于给这个关联关系起了个名称,在查询结果中关联结果集字段名也就是这个。
foreignKey : 自定义外键,当前关联关系相当于
user为主表,wechat_user为关联表,foreignKey就是wechat_user表中关联user的字段(user_id)。sourceKey : 主键,主表
user的主键。 -
创建反向一对一关联关系(BelongsTo)
修改
WechatUser模型,指定反向关联关系// app/model/wechat_user.js module.exports = app => { ... WechatUser.associate = () => { app.model.WechatUser.belongsTo(app.model.User, { as: 'user', foreignKey: 'user_id', targetKey: 'id', }); }; return WechatUser; }反向一对一相当于还是以
user为主表,只不过是通过wechat_user反向查询。targetKey : 目标键,反向关联的主表的主键。
-
关联关系使用示例
-
一对一关联
class UserService extends Service { async user (id) { const { User, WechatUser } = this.app.model; return await User.findOne({ where: { id }, attributes: { exclude: [ 'password', 'delete_time' ], }, include: [ { as: 'wechat_user', model: WechatUser, } ] }); } }attributes : 查询字段信息,可以通过
exclude来指定不查询的字段,或者可以直接传入数组查询数组中的字段(attributes: ['id', 'nickname']) 。include : 指定查询的关联,
as必须和定义关联关系的as相同,model指定关联的模型。打印
user结果如下: -
反向一对一关联
class UserService extends Service { async wechatUser (id) { const { WechatUser, User } = this.app.model; return await WechatUser.findOne({ where: { id }, include: [ { as: 'user', model: User, attributes: { exclude: [ 'password', 'delete_time' ], }, }, ], }); } }同样也可以在
include传入各种查询参数,查询字段attributes、 查询条件where等。打印查询结果如下:
-
2. 一对多关联(HasMany)
一对多也是常用的关联关系,用于定义单个模型拥有多个其它模型的关联关系。例如上面的 user 表,每个用户都可以发布多篇文章,但是篇文章只能属于一个用户,用户和文章就属于一对多关系。
-
新增文章表及模型
// app/service/article.js const Article = app.model.define('article', { id: { type: INTEGER, primaryKey: true, autoIncrement: true, }, title: STRING(255), content: STRING, user_id: INTEGER, create_time: DATE, update_time: DATE, delete_time: DATE, }, { freezeTableName: false, tableName: 'article', underscored: false, paranoid: true, timestamps: true, createdAt: 'create_time', updatedAt: false, deletedAt: 'delete_time', }); -
创建一对多关联关系
修改
User模型:// app/model/user.js module.exports = app => { ... const User = ... ... User.associate = () => { ... app.model.User.hasMany(app.model.Article, { as: 'articles', foreignKey: 'user_id', sourceKey: 'id', }); }; return User; } -
使用关联查询
const { User, Article } = this.app.model; const user = await User.findOne({ where: { id: 1 }, attributes: { exclude: [ 'password', 'delete_time' ], }, include: [ { as: 'articles', model: Article, attributes: { exclude: [ 'delete_time' ], } } ] });结果打印:
3. 多对多关联(BelongsToMany)
多对多关联将一个源与多个目标相连,而所有这些目标又可以与第一个目标之外的其他源相连。
不能像其他关系那样通过向其中一个表添加一个外键来表示这一点. 取而代之的是使用联结模型的概念. 这将是一个额外的模型(以及数据库中的额外表),它将具有两个外键列并跟踪关联。
多对多关联关系比于一对一、一对多复杂一些,以简单的权限管理举例,一个用户可以扮演多个角色,反过来一个角色可以属于多个用户。要借助中间表建立关联关系。现有模型user ,创建新模型 role 。关联表role_user 其中包含外键 user_id 和 role_id 。
-
模型创建
-
role模型module.exports = app => { const { STRING, INTEGER, DATE } = app.Sequelize; return app.model.define('role', { id: { type: INTEGER, primaryKey: true, autoIncrement: true, }, name: STRING, create_time: DATE, }, { freezeTableName: false, tableName: 'role', timestamps: false, }); }; -
role_user模型module.exports = app => { const { INTEGER } = app.Sequelize; return app.model.define('role_user', { user_id: { type: INTEGER, references: { model: app.model.User, key: 'id', }, }, role_id: { type: INTEGER, references: { model: app.model.Role, key: 'id', }, }, }, { freezeTableName: false, tableName: 'role_user', underscored: true, timestamps: false, }); };references : 给定的模型将用作联结模型,指定字段的约束。
-
-
创建关联关系
在
User模型中增加如下代码:// app/model/user.js module.exports = app => { ... const User = ... ... User.associate = () => { ... app.model.User.belongsToMany(app.model.Role, { as: 'user_roles', through: app.model.RoleUser, foreignKey: 'user_id', otherKey: 'role_id', }); }; return User; }如果不指定
foreignKey和otherKey, Sequelize 默认使用的外键是userId和roleId, 要更改这些名称,Sequelize 分别接受参数foreignKey和otherKey(即,foreignKey定义联结关系中源模型的 key,而otherKey定义目标模型中的 key)。可以使用同样的方法在
Role模型中创建关联关系,可以查询角色绑定的用户列表,foreignKey和otherKey与此处相反。 -
使用关联查询
在service中增加方法,查询用户列表,列表中每个用户包含用户的所有角色:
class UserService extends Service { async users () { const { User, Role } = this.app.model; return await User.findAll({ attributes: { exclude: [ 'password', 'delete_time' ], }, include: [ { as: 'user_roles', model: Role, }, ], }); } }结果打印:
二、逻辑删除
在项目中,对数据频繁的删除操作会产生性能问题。逻辑删除作用就是在数据中增加一列字段,来标记数据是否删除,不是真正的删除,也方便必要时候的数据恢复。
在 Sequelize 中这个功能叫 Paranoid (偏执) ,这么个奇怪的名字翻了好几遍文档才找到这个功能。
以上面User 模型为例。要实现逻辑删,除必须将 paranoid: true 参数传递给模型定义。需要开启自动写入时间才能起作用(即,如果你传递 timestamps: false 了,逻辑删除将不起作用)。
可以将逻辑删除默认的字段名(默认是 deletedAt)更改为其他名称。如果不需要自动写入创建时间和更新时间要把 createdAt 、updatedAt 设为false。
- 模型定义逻辑删除
// app/model/user.js
module.exports = app => {
const { STRING, INTEGER, DATE } = app.Sequelize;
const User = app.model.define('user', {
...// 此处省略n行字段定义
}, {
...
paranoid: true,
timestamps: true,
createdAt: 'create_time',
updatedAt: false,
deletedAt: 'delete_time',
});
return User;
};
-
逻辑删除的使用
上面
User模型设置了逻辑删除,当调用模型的destroy()方法就会执行UPDATE `user` SET `delete_time`=? WHERE `id` = ?语句,将delete_time设置为当前时间。查询的时候也会自动添加条件 ````delete_time` IS NULL``` 。class UserService extends Service { async deleteUser (id) { const { User } = this.app.model; const user = await User.findOne({ where: { id } }); await user.destroy(); } }
三、获取器、虚拟字段
1. 获取器
获取器的作用是对模型中的字段原始值做处理,输出处理后的数据。
以上面文中WechatUser 模型为例,查询之后输出时间格式为2021-08-04T05:18:32.000Z,不符合要求,希望时间输出格式为 YYYY-MM-DD HH:mm:ss ,将模型做出如下修改:
定义一个转换时间的方法,使用的 dayjs,在字段中增加 get 方法,返回格式化之后的数据。
// app/model/wechat_user.js
// 定义一个转换时间的方法
const dayjs = require('dayjs');
const formatDate = date => (date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : date);
module.exports = app => {
const { STRING, INTEGER, DATE } = app.Sequelize;
const WechatUser = app.model.define('wechatUser', {
... // 防止干扰省略其他字段
create_time: {
type: DATE,
get () {
// 返回修改后的时间
return formatDate(this.getDataValue('create_time'));
},
},
}, {
... //省略配置
});
return WechatUser;
};
再次重新查询,发现结果变成了 "create_time": "2021-08-04 13:18:32" 。
2. 虚拟字段
虚拟字段是 Sequelize 在后台填充的字段,但实际上它们不存在于数据库中。
还是以WechatUser 模型为例,我们需要展示用户的性别 gender (男、女、保密) 但是还需要保留数据中的原始字段 gender ,我们可以使用Sequelize 的 DataTypes.VIRTUAL 增加一个数据表中不存在的字段 gender_text 。继续修改模型:
// app/model/wechat_user.js
// 定义性别枚举
const genderMap = { 0: '保密', 1: '男', 2: '女', };
module.exports = app => {
const { STRING, INTEGER, DATE, VIRTUAL } = app.Sequelize;
const WechatUser = app.model.define('wechatUser', {
... // 防止干扰省略其他字段
gender: INTEGER,
gender_text: {
type: VIRTUAL,
get () {
const gender = this.getDataValue('gender');
return genderMap[gender] ? genderMap[gender] : '';
},
}
}, {
... //省略配置
});
return WechatUser;
};
这时查询,结果中就多了字段 "gender_text": "男"