NodeJS: Sequelize中表示继承关系

68 阅读5分钟

关于Sequelize的碎碎念(可省略)

我今天也是第一次接触到这个Sequelize,给我打开了一个新世界的大门。在学校做课程练手项目的时候都是直接写SQL语句操作数据库的,今天才觉得我当时像一个原始人一样(捂脸)。

今天看着这个Sequelize,给我的感觉是:很像python在操作MongoDB。你看,获取数据的时候不是直接用SELECT语句,而是用findall这个api,而且配置条件的时候也跟MongoDB差不多:.findall({ where: { ... } })

总之,有一种亲切感在里面哈哈哈哈哈。

然后我练手的数据库是当时学MySQL的数据库,其中有两个库(Employee,User)是继承关系。我就在想怎么在Sequelize里面表达继承关系。经过一系列搜索求证,我将它总结在下面:

Sequelize中的继承关系

(PS: 省流:文章最后面有总结)

Sequelize提供了一些表示关系的API

  • hasOne:一对一
  • hasMany:一对多
  • belongsTo:属于
  • belognsToMany:属于多个

很明显,我们这里会使用到 belongsTo 来表达 Employee ISA User。但是明显归明显,有一些细节还是要注意的。

测试数据

首先,来介绍一下我用的数据吧 EmployeeUser

User(uid, name, age, street, city, state, zipcode)
Employee(uid, hireDate, hourlyRate)

User

const { DataTypes, Model } = require("sequelize");
const { sequelize } = require("./db");

class User extends Model {}

User.init(
  {
    uid: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true },
    name: { type: DataTypes.STRING, allowNull: true },
    age: { type: DataTypes.INTEGER, allowNull: true },
    street: { type: DataTypes.STRING, allowNull: true },
    city: { type: DataTypes.STRING, allowNull: true },
    state: { type: DataTypes.STRING, allowNull: true },
    zipcode: { type: DataTypes.INTEGER, allowNull: true },
  },
  {
    sequelize,
    modelName: "User",
    tableName: "User", // 为了不破坏之前的数据库
    timestamps: false, // 为了不破坏之前的数据库
  }
);

// 没写sync是为了不破坏之前的数据库

module.exports = User;

Employee

const { DataTypes, Model } = require("sequelize");
const { sequelize } = require("./db");
const User = require("./user");

class Employee extends Model {
  // (optional) 定义这个,可以在获取 name 的时候,直接 employee.name,而不需要 employee.User.name
  get name() {
    return this.dataValues.name;
  }
}

Employee.init(
  {
    uid: {
      type: DataTypes.INTEGER,
      allowNull: false,
      primaryKey: true,
      references: { model: User, key: "uid" }, // 声明这玩意是个 foreignKey
    },
    hireDate: {
      type: DataTypes.DATE,
    },
    hourlyRate: {
      type: DataTypes.FLOAT,
    },
  },
  {
    sequelize,
    modelName: "Employee",
    tableName: "Employee", // 为了不破坏之前的数据库
    timestamps: false, // 为了不破坏之前的数据库
  }
);

// { foreignKey: "uid" } 可以自定义连接两个表的 foreignKey, 这里设定为 uid
Employee.belongsTo(User, { foreignKey: "uid" });

// 没写sync是为了不破坏之前的数据库

module.exports = Employee;

细节1:子表Model记得要填上belongsTo,可以配置连接两个表的 foreignKey

  • 子表.belongsTo(父表, { foreignKey: "自定义的外键" });
  • 像我这里我就这样子了:Employee.belongsTo(User, { foreignKey: "uid" });,表示 Employee 继承自 User,用 uid 相关联。
  • 默认会以表名+Id来作为 foreignKey 来连接两个表。如果我在Employee里面的35行没有加 { foreignKey: "uid" } 的话,就会用 EmployeeId 来关联。

细节2:通过 子表 查询记录在 父表 的数据,要配置 include

  • include 是一个数组,里面放着要 JOIN 的表
  • 然后在获取 父表 的数据的时候,记得要在中间多一个 父表 才行:子表结果.父表.父表元素
(async () => {
  const e = await Employee.findOne({
    include: [User],
  });
  console.log(e.User.name);
})();

细节3:(optional) 其实也有办法省略中间的那个 父表

  • 原理:观察子表结果我们发现取元素要么从 dataValues 中取本表的数据,要么从 父表 中取父表的数据。于是就衍生出来两种方法:1. 通过 dataValues 获取,2. 直接通过父表获取
Employee {
  dataValues: {
    uid: 75,
    hireDate: '2019-11-16',
    hourlyRate: '15.75',
    User: ...
  },
  User: User {
    dataValues: {
      uid: 75,
      name: 'Raina Simonutti',
      age: 79,
      street: '9163 Badeau Circle',
      city: 'Honolulu',
      state: 'Hawaii',
      zipcode: 96845
    },
    ...
  }
  ...
}
  1. 方法一:通过 dataValues 获取

    • 在定义子表Model的时候可以给它多加一个 getter 方法:get 父表属性() { return this.dataValues.父表属性 }
    class Employee extends Model {
      // (optional) 定义这个,可以在获取 name 的时候,直接 employee.name,而不需要 employee.User.name
      get name() {
        return this.dataValues.name;
      }
    }
    
    • 然后在查询的时候加一个 attributes 字样,并把 'User.name' 重命名为 'name',于是在查询属性的时候就可以直接 子表结果.父表元素
    (async () => {
      const e = await Employee.findOne({
        include: [User],
        attributes: {
          include: [
            "uid",
            "hireDate",
            "hourlyRate",
            [Sequelize.col("User.name"), "name"],
          ],
        },
      });
      console.log(e.name);
    })();
    

    还有一个方法甚至可以省略 attributes 重命名的操作

  2. 方法二:直接通过父表获取(可以省略 attributes 重命名的操作)

    • 在定义子表Model的时候可以给它多加一个 getter 方法:get 父表属性() { return this.父表.父表属性 }
    class Employee extends Model {
      // (optional) 定义这个,可以在获取 name 的时候,直接 employee.name,而不需要 employee.User.name
      get name() {
        return this.User.name;
      }
    }
    
    • 然后在查询的时候可以直接 子表结果.父表元素
    (async () => {
      const e = await Employee.findOne({
        include: [User],
      });
      console.log(e.name);
    })();
    
  • 个人感觉挺麻烦的,不如直接在中间加 父表 呢,又方便,又可以避免重复,还能清楚的知道数值的来源

细节4:(optional) 用 references 可以规定哪个元素是 foreignKey

还有一个小细节,就是在 子表.init() 的时候,用 references 可以规定哪个元素是 foreignKey,虽然只要有 .belongsTo 就可以正常运作了,但是我总觉得加上这个可读性更强

Employee.init(
  {
    uid: {
      ...,
      references: { model: User, key: "uid" }, // 声明这玩意是个 foreignKey
    },
    ...
  },
  {...}
);

总结

直接在子表Model里面添加 子表.belongsTo(父表, { foreignKey: "自定义的外键" }); 就完事儿了。然后查询的时候记得在 findBalabala 里面加一个配置 includes: [父表1, 父表2, ...],还有获取数据的时候不要省略中间的 父表 就可以了。

PS:第一次接触 Sequelize,如果有什么讲的不对的地方,请千万不吝赐教。直接在评论区指明或者私信我都可以,千万不要有所顾忌,大佬们的每一次指点都是我宝贵的进步资源。