Node系列 · ORM:访问器和虚拟字段
Sequelize 模型字段除了最基础的"映射数据库列",还能做访问器(getter / setter)和虚拟字段(不存数据库)。前者做数据转换(脱敏、加密、JSON 解析),后者做"计算字段"(如 fullName = firstName + lastName)。
一、Getter:读取时转换
Getter 在字段被读取时自动调用——可以做脱敏、格式化、序列化。
1.1 基础用法
javascript
const { DataTypes } = require('sequelize');
const User = sequelize.define('User', {
name: {
type: DataTypes.STRING,
get() {
// this 指向原始字段值
const raw = this.getDataValue('name');
return raw ? raw.toUpperCase() : raw;
},
},
});
const user = await User.create({ name: 'alice' });
console.log(user.name); // 'ALICE'(自动大写)
const raw = user.getDataValue('name');
console.log(raw); // 'alice'(原始值)1.2 实际场景:敏感字段脱敏
javascript
const User = sequelize.define('User', {
email: {
type: DataTypes.STRING,
get() {
const raw = this.getDataValue('email');
if (!raw) return raw;
const [name, domain] = raw.split('@');
const masked = name[0] + '***' + (name.length > 1 ? name[name.length - 1] : '');
return `${masked}@${domain}`;
},
},
});
const user = await User.create({ email: 'alice@example.com' });
console.log(user.email); // 'a***e@example.com'1.3 实际场景:JSON 字段自动解析
javascript
const User = sequelize.define('User', {
preferences: {
type: DataTypes.JSON,
get() {
const raw = this.getDataValue('preferences');
return raw ? JSON.parse(raw) : {};
},
set(value) {
this.setDataValue('preferences', JSON.stringify(value));
},
},
});
const user = await User.create({
preferences: { theme: 'dark', lang: 'zh-CN' },
});
console.log(user.preferences); // { theme: 'dark', lang: 'zh-CN' }(已是对象)二、Setter:写入时处理
Setter 在字段被赋值时自动调用——可以做加密、规范化、清洗。
2.1 基础用法
javascript
const User = sequelize.define('User', {
email: {
type: DataTypes.STRING,
set(value) {
// 自动小写 + trim
this.setDataValue('email', value.toLowerCase().trim());
},
},
});2.2 实际场景:密码自动哈希
javascript
const bcrypt = require('bcrypt');
const User = sequelize.define('User', {
password: {
type: DataTypes.STRING,
set(value) {
if (!value) {
throw new Error('密码不能为空');
}
const hash = bcrypt.hashSync(value, 10);
this.setDataValue('password', hash);
},
},
});
const user = await User.create({ password: 'plain-password' });
console.log(user.password); // '$2b$10$...'(自动 bcrypt 哈希)2.3 ⚠️ Setter 与 hooks 的关系
Setter 在赋值时触发,在 beforeCreate hook 之前:
javascript
// 时序:
// 1. user.password = 'plain' → setter 触发 → 哈希
// 2. user.save() → beforeCreate hook
// 3. INSERT SQL所以在 hook 里拿到的 user.password 已经是哈希后的值——可以直接入库。
三、虚拟字段(VIRTUAL)
虚拟字段不存数据库——它是从其他字段计算出来的"派生字段"。
javascript
const User = sequelize.define('User', {
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
fullName: {
type: DataTypes.VIRTUAL,
get() {
return `${this.firstName} ${this.lastName}`;
},
},
});
const user = await User.create({ firstName: '张', lastName: '三' });
console.log(user.fullName); // '张 三'3.1 虚拟字段不存数据库
sql
-- 自动生成的表结构不会有 fullName 列
CREATE TABLE `users` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`firstName` VARCHAR(255),
`lastName` VARCHAR(255),
`createdAt` DATETIME,
`updatedAt` DATETIME
);但虚拟字段会被 JSON 序列化包含(user.toJSON() 会带 fullName),API 输出时自动包含。
3.2 虚拟字段做关联聚合
javascript
const User = sequelize.define('User', { name: DataTypes.STRING });
User.hasMany(Post, { foreignKey: 'userId' });
Post.belongsTo(User, { foreignKey: 'userId' });
User.prototype.postCount = async function () {
return await Post.count({ where: { userId: this.id } });
};但这种"每次访问都查库"会引发 N+1。更好的做法是用 findAll 时 attributes.include + 聚合:
javascript
// ✅ 一次性查询,不触发 N+1
const users = await User.findAll({
attributes: {
include: [[Sequelize.fn('COUNT', Sequelize.col('posts.id')), 'postCount']],
},
include: [{ model: Post, attributes: [] }],
group: ['User.id'],
});
users.forEach((u) => console.log(u.name, u.getDataValue('postCount')));四、JSON 字段:实际项目最常用组合
DataTypes.JSON + 自定义 getter/setter 是实际项目里最常见的模式:
javascript
const Article = sequelize.define('Article', {
title: DataTypes.STRING,
// 标签数组
tags: {
type: DataTypes.JSON,
get() {
const raw = this.getDataValue('tags');
return Array.isArray(raw) ? raw : [];
},
set(value) {
// 写入时确保是数组
if (!Array.isArray(value)) {
throw new Error('tags 必须是数组');
}
this.setDataValue('tags', value);
},
},
// 配置对象
config: {
type: DataTypes.JSON,
get() {
const raw = this.getDataValue('config');
return raw || {};
},
},
});
const article = await Article.create({
title: 'Hello',
tags: ['node', 'orm'],
config: { allowComment: true },
});
console.log(article.tags); // ['node', 'orm']
console.log(article.config); // { allowComment: true }五、虚拟字段 vs 真实字段:何时用
| 场景 | 推荐 |
|---|---|
| 派生字段(fullName、postCount) | 虚拟字段 |
| 频繁查询的字段(status_label) | 真实字段(性能更好) |
| 敏感数据脱敏 | getter(按需展示) |
| 输入清洗(trim、小写) | setter |
| 自动加密(密码哈希) | setter |
| 自动 JSON 解析 | getter + setter 组合 |
WARNING
虚拟字段不能用于 WHERE / ORDER BY——因为它不存数据库。如果一定要按派生字段排序,要么:
- 把字段真实化(加
allowNull: false+ 迁移) - 用 SQL 函数排序:
order: [[Sequelize.literal('firstName || lastName'), 'ASC']]
六、与 JSON 序列化的关系
javascript
const user = await User.create({ firstName: '张', lastName: '三' });
// 直接读:虚拟字段生效
console.log(user.fullName); // '张 三'
// toJSON:虚拟字段会包含
console.log(user.toJSON());
// { id: 1, firstName: '张', lastName: '三', fullName: '张 三', createdAt: ... }
// toJSON 也触发 getter
console.log(user.toJSON().email); // 'a***e@example.com'七、最佳实践
| 场景 | 推荐 |
|---|---|
| 字段脱敏 | 用 getter;保留原始值用 getDataValue |
| 输入清洗 | 用 setter(小写、trim、去除危险字符) |
| 密码哈希 | setter(hashSync)或 beforeCreate hook |
| JSON 字段 | getter + setter 组合,自动 parse / stringify |
| 派生字段 | 虚拟字段;但不用于 WHERE |
| 频繁查询的派生 | 真实化字段 + 维护逻辑 |
| 关联计数 | attributes.include + fn('COUNT'),不要用 prototype 方法 |
八、小结
- Getter 在读取时转换:脱敏、格式化、自动 JSON 解析
- Setter 在写入时处理:清洗、加密、规范化
- 虚拟字段(
DataTypes.VIRTUAL):不存数据库,按需从其他字段计算 - 虚拟字段会包含在
toJSON()里,但不能用于 WHERE / ORDER BY - 关联计数用
attributes.include + fn('COUNT'),避免 N+1 - getter 内部用
getDataValue取原始值;setter 内部用setDataValue存值 - 密码哈希用 setter 或
beforeCreatehook——两者都在 INSERT 前生效
