这是我参与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": "男"