Getters, Setters & Virtuals - 获取器, 设置器 & 虚拟字段

Sequelize 允许你为模型的属性定义自定义获取器和设置器.

Sequelize 还允许你指定所谓的 虚拟属性,它们是 Sequelize 模型上的属性,这些属性在基础 SQL 表中实际上并不存在,而是由 Sequelize 自动填充. 它们对于创建自定义属性非常有用, 这也可以简化您的代码.

获取器

获取器是为模型定义中的一列定义的 get() 函数:

  1. const User = sequelize.define('user', {
  2. // 假设我们想要以大写形式查看每个用户名,
  3. // 即使它们在数据库本身中不一定是大写的
  4. username: {
  5. type: DataTypes.STRING,
  6. get() {
  7. const rawValue = this.getDataValue('username');
  8. return rawValue ? rawValue.toUpperCase() : null;
  9. }
  10. }
  11. });

就像标准 JavaScript 获取器一样,在读取字段值时会自动调用此获取器:

  1. const user = User.build({ username: 'SuperUser123' });
  2. console.log(user.username); // 'SUPERUSER123'
  3. console.log(user.getDataValue('username')); // 'SuperUser123'

注意,尽管上面记录为 SUPERUSER123,但是真正存储在数据库中的值仍然是 SuperUser123. 我们使用了 this.getDataValue('username') 来获得该值,并将其转换为大写.

如果我们尝试在获取器中使用 this.username,我们将陷入无限循环! 这就是为什么 Sequelize 提供 getDataValue 方法的原因.

设置器

设置器是为模型定义中的一列定义的 set() 函数. 它接收要设置的值:

  1. const User = sequelize.define('user', {
  2. username: DataTypes.STRING,
  3. password: {
  4. type: DataTypes.STRING,
  5. set(value) {
  6. // 在数据库中以明文形式存储密码是很糟糕的.
  7. // 使用适当的哈希函数来加密哈希值更好.
  8. this.setDataValue('password', hash(value));
  9. }
  10. }
  11. });
  1. const user = User.build({ username: 'someone', password: 'NotSo§tr0ngP4$SW0RD!' });
  2. console.log(user.password); // '7cfc84b8ea898bb72462e78b4643cfccd77e9f05678ec2ce78754147ba947acc'
  3. console.log(user.getDataValue('password')); // '7cfc84b8ea898bb72462e78b4643cfccd77e9f05678ec2ce78754147ba947acc'

Sequelize 在将数据发送到数据库之前自动调用了设置器. 数据库得到的唯一数据是已经散列过的值.

如果我们想将模型实例中的另一个字段包含在计算中,那也是可以的,而且非常容易!

  1. const User = sequelize.define('user', {
  2. username: DataTypes.STRING,
  3. password: {
  4. type: DataTypes.STRING,
  5. set(value) {
  6. // 在数据库中以明文形式存储密码是很糟糕的.
  7. // 使用适当的哈希函数来加密哈希值更好.
  8. // 使用用户名作为盐更好.
  9. this.setDataValue('password', hash(this.username + value));
  10. }
  11. }
  12. });

注意: 上面涉及密码处理的示例尽管比单纯以明文形式存储密码要好得多,但远非完美的安全性. 正确处理密码很困难,这里的所有内容只是为了举例说明 Sequelize 功能. 我们建议让网络安全专家阅读 OWASP 文档或者访问 InfoSec StackExchange.

组合获取器和设置器

获取器和设置器都可以在同一字段中定义.

举个例子,假设我们正在建一个 Post 模型,其 content 是无限长度的文本. 假设要提高内存使用率,我们要存储内容的压缩版本.

注意:在这种情况下,现代数据库应会自动进行一些压缩. 这只是为了举例.

  1. const { gzipSync, gunzipSync } = require('zlib');
  2. const Post = sequelize.define('post', {
  3. content: {
  4. type: DataTypes.TEXT,
  5. get() {
  6. const storedValue = this.getDataValue('content');
  7. const gzippedBuffer = Buffer.from(storedValue, 'base64');
  8. const unzippedBuffer = gunzipSync(gzippedBuffer);
  9. return unzippedBuffer.toString();
  10. },
  11. set(value) {
  12. const gzippedBuffer = gzipSync(value);
  13. this.setDataValue('content', gzippedBuffer.toString('base64'));
  14. }
  15. }
  16. });

通过上述设置,每当我们尝试与 Post 模型的 content 字段进行交互时,Sequelize 都会自动处理自定义的获取器和设置器. 例如:

  1. const post = await Post.create({ content: 'Hello everyone!' });
  2. console.log(post.content); // 'Hello everyone!'
  3. // 一切都在幕后进行,所以我们甚至都可以忘记内容实际上是
  4. // 作为 gzip 压缩的 base64 字符串存储的!
  5. // 但是,如果我们真的很好奇,我们可以获取 'raw' 数据...
  6. console.log(post.getDataValue('content'));
  7. // Output: 'H4sIAAAAAAAACvNIzcnJV0gtSy2qzM9LVQQAUuk9jQ8AAAA='

虚拟字段

虚拟字段是 Sequelize 在后台填充的字段,但实际上它们不存在于数据库中.

例如,假设我们有一个 User 的 firstNamelastName 属性.\

同样,这仅是为了示例.

如果有一种简单的方法能直接获取 全名 那会非常好! 我们可以将 getters 的概念与 Sequelize 针对这种情况提供的特殊数据类型结合使用:DataTypes.VIRTUAL:

  1. const { DataTypes } = require("sequelize");
  2. const User = sequelize.define('user', {
  3. firstName: DataTypes.TEXT,
  4. lastName: DataTypes.TEXT,
  5. fullName: {
  6. type: DataTypes.VIRTUAL,
  7. get() {
  8. return `${this.firstName} ${this.lastName}`;
  9. },
  10. set(value) {
  11. throw new Error('不要尝试设置 `fullName` 的值!');
  12. }
  13. }
  14. });

VIRTUAL 字段不会导致数据表也存在此列. 换句话说,上面的模型虽然没有 fullName 列. 但是它似乎存在着!

  1. const user = await User.create({ firstName: 'John', lastName: 'Doe' });
  2. console.log(user.fullName); // 'John Doe'

在 Sequelize v7 中已弃用:getterMethodssetterMethods

Sequelize 在模型定义中还提供了 getterMethodssetterMethods 参数,以指定看起来像但与虚拟属性不完全相同的事物. 不鼓励使用此方法,并且将来可能会不建议使用(建议直接使用虚拟属性).

示例:

  1. const { Sequelize, DataTypes } = require('sequelize');
  2. const sequelize = new Sequelize('sqlite::memory:');
  3. const User = sequelize.define('user', {
  4. firstName: DataTypes.STRING,
  5. lastName: DataTypes.STRING
  6. }, {
  7. getterMethods: {
  8. fullName() {
  9. return this.firstName + ' ' + this.lastName;
  10. }
  11. },
  12. setterMethods: {
  13. fullName(value) {
  14. // 注意:这仅用于演示.
  15. // 查阅: https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/
  16. const names = value.split(' ');
  17. const firstName = names[0];
  18. const lastName = names.slice(1).join(' ');
  19. this.setDataValue('firstName', firstName);
  20. this.setDataValue('lastName', lastName);
  21. }
  22. }
  23. });
  24. (async () => {
  25. await sequelize.sync();
  26. let user = await User.create({ firstName: 'John', lastName: 'Doe' });
  27. console.log(user.fullName); // 'John Doe'
  28. user.fullName = 'Someone Else';
  29. await user.save();
  30. user = await User.findOne();
  31. console.log(user.firstName); // 'Someone'
  32. console.log(user.lastName); // 'Else'
  33. })();