Sequelize 中的关联

时间:2020-12-20 12:49:15

标签: javascript node.js sequelize.js

我正在尝试获取如下数据:

"data": {
    "religions": {
        "Major1Name": {
            "AttendanceYear1Name": {
                "student": [ {...}, {...}, {...} ]
            },
            "AttendanceYear2Name": {
                "student": [ {...}, {...}, {...} ]
            }
        },
        "Major2Name": {
            "AttendanceYear1Name": {
                "student": [ {...}, {...}, {...} ]
            },
            "AttendanceYear2Name": {
                "student": [ {...}, {...}, {...} ]
            }
        }
    }
}

我知道如何为例如建立基本级别的关联。学生和专业。但在我的数据库知识中,我不知道如何与 religionsmajors 以及 Sequelize 相关联。请帮忙。

我有以下表格:

  1. 专业
  2. 出勤_年
  3. 宗教
  4. 学生
  5. 注册

下面是我的模型。

专业

'use strict';

const { Model } = require('sequelize');

module.exports = (sequelize, DataTypes) => {
  class Major extends Model {
    static associate(models) {
      Major.hasMany(models.Enrollment, {
        foreignKey: 'majorId',
        as: 'major',
      });
    }
  }

  Major.init(
    {
      majorId: {
        allowNull: false,
        autoIncrement: true,
        field: 'major_id',
        primaryKey: true,
        type: DataTypes.INTEGER,
      },
    { ... }
    },
    {
      sequelize,
      modelName: 'Major',
      tableName: 'majors',
    }
  );

  return Major;
};

出勤_年

"use strict";

const { Model } = require("sequelize");

module.exports = (sequelize, DataTypes) => {
  class AttendanceYear extends Model {
    static associate(models) {
      AttendanceYear.hasMany(models.Enrollment, {
        as: "enrollments",
        foreignKey: "attendance_year_id",
      });
    }
  }

  AttendanceYear.init(
    {
      attendanceYearId: {
        allowNull: false,
        autoIncrement: true,
        field: "attendance_year_id",
        primaryKey: true,
        type: DataTypes.INTEGER,
      },
    { ... }
    },
    {
      sequelize,
      modelName: "AttendanceYear",
      tableName: "attendance_years",
    }
  );

  return AttendanceYear;
};

宗教

"use strict";

const { Model } = require("sequelize");

module.exports = (sequelize, DataTypes) => {
  class Religion extends Model {
    static associate(models) {
      Religion.hasMany(models.Student, {
        foreignKey: "religionId",
        as: "student",
      });
    }
  }

  Religion.init(
    {
      religionId: {
        allowNull: false,
        autoIncrement: true,
        field: "religion_id",
        primaryKey: true,
        type: DataTypes.INTEGER,
      },
      { ... }
    },
    {
      sequelize,
      modelName: "Religion",
      tableName: "religions",
    }
  );

  return Religion;
};

学生

'use strict';

const { Model } = require('sequelize');

module.exports = (sequelize, DataTypes) => {
  class Student extends Model {
    static associate(models) {

      Student.belongsTo(models.Religion, {
        foreignKey: 'religionId',
        as: 'religion',
        targetKey: 'religionId',
      });

      Student.belongsTo(models.Enrollment, {
        foreignKey: 'studentId',
        as: 'enrollment',
      });
    }
  }

  Student.init(
    {
      studentId: {
        allowNull: false,
        autoIncrement: true,
        field: 'student_id',
        primaryKey: true,
        type: DataTypes.INTEGER,
      },
      name: {
        allowNull: false,
        field: 'name_en',
        type: DataTypes.STRING(50),
      },
      religionId: {
        allowNull: false,
        field: 'religion_id',
        references: {
          model: 'religons',
          key: 'religion_id',
        },
        type: DataTypes.INTEGER,
      },
    },
    {
      sequelize,
      modelName: 'Student',
      tableName: 'students',
    }
  );
  return Student;
};

和注册

'use strict';

const { Model } = require('sequelize');

module.exports = (sequelize, DataTypes) => {
  class Enrollment extends Model {
    static associate(models) {

      Enrollment.belongsTo(models.Major, {
        foreignKey: 'majorId',
        as: 'major',
      });

      Enrollment.belongsTo(models.Student, {
        foreignKey: 'studentId',
        as: 'student',
      });

      Enrollment.belongsTo(models.AttendanceYear, {
        foreignKey: 'attendanceYearId',
        as: 'attendanceYear',
      });
    }
  }

  Enrollment.init(
    {
      enrollmentId: {
        allowNull: false,
        autoIncrement: true,
        field: 'enrollment_id',
        primaryKey: true,
        type: DataTypes.INTEGER,
      },
      majorId: {
        allowNull: false,
        field: 'major_id',
        onDelete: 'NO ACTION',
        onUpdate: 'CASCADE',
        references: {
          model: 'majors',
          key: 'major_id',
        },
        type: DataTypes.INTEGER,
      },
      studentId: {
        allowNull: false,
        field: 'student_id',
        onDelete: 'CASCADE',
        onUpdate: 'CASCADE',
        references: {
          model: 'students',
          key: 'student_id',
        },
        type: DataTypes.INTEGER,
      },
      attendanceYearId: {
        allowNull: false,
        field: 'attendance_year_id',
        onDelete: 'NO ACTION',
        onUpdate: 'CASCADE',
        references: {
          model: 'attendance_years',
          key: 'attendance_year_id',
        },
        type: DataTypes.INTEGER,
      },
    },
    {
      sequelize,
      modelName: 'Enrollment',
      tableName: 'enrollments',
    }
  );

  return Enrollment;
};

我做过和没做过的事情

const religions = await models.Religion.findAll({
    where: { religionId: req.params.religionId },
    include: [
      {
        model: models.Major,
        as: 'major',
        include: [
          {
            model: models.AttendanceYear,
            as: 'attendanceYear',
            include: [
              {
                model: models.Student,
                as: 'student',
                attributes: ['studentId', 'nameMm', 'nameEn', 'nrc'],
                include: [
                  {
                    model: models.Parent,
                    as: 'parent',
                    attributes: ['fatherNameMm', 'fatherNameEn', 'fatherNrc'],
                  },
                  {
                    model: models.Enrollment,
                    as: 'enrollment',
                    attributes: ['rollNo'],
                    where: {
                      academicYearId: req.params.academicYearId,
                    },
                  },
                ],
              },
            ],
          },
        ],
      },
    ],
  });

错误

SequelizeEagerLoadingError: Major is not associated to Religion!

更新

我在这个结构 src/database/models/ 中有以下模型(将是数据库中的表)文件:

  1. 专业
  2. 出勤_年
  3. 宗教
  4. 学生
  5. 注册

整个结构是:

database/migrations/....js
database/models/....js
database/seeders/....js

我在 index.js 目录中有一个 models/ 文件,如下所示:

'use strict';

const config = require('../../config/config');
const fs = require('fs');
const path = require('path');
const { Sequelize, DataTypes } = require('sequelize');
const basename = path.basename(__filename);
const db = {};

const logger = require('../../startup/logger')();

const ENV = config[process.env.NODE_ENV];

let sequelize;
sequelize = new Sequelize(ENV.database, ENV.username, ENV.password, {
  dialect: 'mysql',
  host: ENV.host,
  define: {
    charset: 'utf8',
    collate: 'utf8_general_ci',
    timestamps: false, // omit createdAt and updatedAt
  },
});

sequelize
  .authenticate()
  .then(() => {
    // logger.info('Connected to the database.');
    console.log('Connected to the database.');
  })
  .catch((error) => {
    logger.error('Unable to connect to the database.', error);
    console.log(`Unable to connect to the database.`, error);
    process.exit(1);
  });

fs.readdirSync(__dirname)
  .filter((file) => {
    return (
      file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'
    );
  })
  .forEach((file) => {
    const model = require(path.join(__dirname, file))(sequelize, DataTypes);
    db[model.name] = model;
  });

db.sequelize = sequelize;
db.Sequelize = Sequelize;

Object.keys(db).forEach((modelName) => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

module.exports = db;

通过该实现,我不需要在模型文件中导入所需的模型以及路由处理程序,只需要以下行。

const models = require('../database/models');
/** I can easily get model instance by calling models.Student to get Student model. **/

我不使用 sync 方法的原因是,如果我更新模型或添加新模型,我害怕在生产中意外丢失我的数据。因此,我使用了 sequelize-cli。有了它,我可以通过运行 sequelize db:migrate 将我的模型变成表格。

我明确定义属性和表名的原因是我希望它们遵循 MySQL 命名约定:例如 attendance_yearsattendance_year_id。但是当我运行对数据库的调用时,我在终端中看到了很多命名别名:attachment_year_id 为attachmentYearId 等。我认为这可能会影响查询性能,因此,我会考虑让 sequelize 完全管理命名约定。

2 个答案:

答案 0 :(得分:2)

您需要在 religions 关联旁边的 Religion.hasMany(models.Student 文件中定义一个关联:

 Religion.hasMany(models.Major, {
        foreignKey: "religionId",
        as: "major",
      });

答案 1 :(得分:2)

感谢您在 Twitter 上与我联系。对此,我真的非常感激。话虽如此,让我们看看我们是否可以回答您的问题。在找到我希望提供的解决方案之前,请允许我稍微了解一下。

首先,我个人经验的一些建议。

  1. Sequelize 非常强大。在底层,它为你解决了很多问题,这意味着你不必担心很多事情,比如主键属性、外键列名、表名等。除了一些非常复杂的关联或在某些边缘情况下(例如,如果您正在使用遗留数据库),您实际上不必明确声明它们是什么,而是让 sequelize 为您完成繁重的工作。我提到这一点是因为我注意到您尝试指定 primaryKey 属性并在模型定义的选项对象中包含 tableName 属性,这是可以的,但确实没有必要,实际上可能会干扰续集引擎的查询,在这种情况下,您可能必须在任何地方定义这些属性,这简直是一团糟。 Sequelize 默认生成 primaryKey 属性和 tableName - 因此,如果可以,请尽可能减少不必要的定义 - See why from the docs on table name inference here。如果您确实觉得需要为模型使用自己的自定义键,请考虑使用 UUID 属性,就像这样。
// Custom UUID attribute seperate from the model id but also generated by sequelize.
studentUUID: {
  type: DataTypes.UUID,
  defaultValue: Sequelize.UUIDV4 // will generate a UUID for every created instance
}

这既省去了必须唯一命名主键字段的麻烦,也可以避免键可能具有相似值的情况。它还为您提供了一个额外的独特属性,可在您的查询中使用以确保您获得单个记录。

  1. 我还注意到您尝试在模型定义中的静态方法中定义模型关联。我不确定你为什么这样做,但我不认为这就是关联的定义方式,无论是根据我的经验还是官方的续集文档(第 6 版 - as seen here)。相反,它的完成方式是在模型文件定义关联模型已经初始化之后,在导出之前 - 例如
const { Model, Sequelize, Datatypes } = require('sequelize');
const db = require('../path/to/sequelizeConnectionInstance');

// Let's say we want to associate Religion model to Major model in a 1 - N relationship;
// To do that, we import the Major model 
const Major = require('./Major'); 

class Religion extends Model { /* Yes, it's an empty object and that's okay */ }

Religion.init({
  // Model attributes are defined here
  name: {
    type: DataTypes.STRING,
    allowNull: false
  },
  founder: {
    type: DataTypes.STRING
    // allowNull defaults to true
  }, 
  {...}
}, {
  // Other model options go here, but you rarely need more than the following 2
  sequelize: db, // We need to pass the connection instance
  modelName: 'religion' // I use small letters to avoid ambiguity. you'll see why in a bit
  // No table name attribute is required. the table "religions" is automatically created
});

// The relationship(s) is/are then defined below
Religion.hasMany(Major);
Major.belongsTo(Religion); // The Major model must have a religionId attribute
/* 
*  Sequelize will automagically associate Major to Religion even without the FK being   
*  explicitly described in the association. What sequelize does is find the `modelName+id` 
*  attribute in the associated model. i.e. if `Foo.hasMany(Bar)` and `Bar.belongsTo(Foo)`, *  sequelize will look for the `FooId` property in the Bar model, unless you specifiy 
*  otherwise. Also, the convention I use (and what I've found to work) is to import 
*  'child' models to 'parent' model definitions to make 
*  associations.
*/
// THEN we export the model 
modules.export = Religion;

另外值得记住的是,当你关联模型实体时,sequelize 会自动将结果中实体的名称复数,这取决于关系(即父实体是否有许多子实体),并返回结果作为数组。例如如果 Religion.hasMany(Major),结果将返回 religion.majors = [/*an array of majors*/]

  1. 从上面的示例中,您可以开始看到一些可以立即解决您的问题的更改。但在提出我的解决方案之前,我觉得最后一件事要提。同样,与上一点无关,我注意到您尝试在某些属性上指定引用。你不需要这样做。那是一种 NoSQL 的东西。定义属性和类型就足够了,当建立关联时,sequelize 会找出外键。您还可以在关联中指定其他详细信息。例如;假设 ReligionMajor 模型之间存在 1 - N 关系 -

Major.js 模型文件中,您可以像这样指定宗教 FK

class Major extends Model {}
Major.init(
  {
     religionId: {
        type: Datatypes.INTEGER, 
        allowNull: true, // set to false if the value is compulsory
        // that's all folks. no other details required.
     }, 
     {/* ...otherAttributes */}
  }, 
  {/* ...options, etc */}
)

module.exports = Major;

然后在Religion.js

const Major = require('./Major');
Religion.init(
  {
// just declare religions OWN attributes as usual  
     {/* ...religionObjectAttributes */}
  }, 
  {/* ...options, etc */}
)
Religion.hasMany(Major, {
  foreignKey: 'religionId',
  onDelete: 'NO ACTION', // what happens when you delete Major ? 
  onUpdate: 'CASCADE',
})

Major.belongsTo(Religion, {
  foreignKey: 'religionId',
})

module.exports = Religion;

附带说明,您通常不必在关联中包含 onDeleteonUpdate 属性,因为默认值非常适合大多数用例。同样值得注意的是,您可以有多个关系,在这种情况下您可以使用别名。但这似乎与您的问题无关(至少从一开始),但仍然值得注意且非常有用。

话虽如此。现在回答您的问题:(可能的解决方案 1 - 简单方法)

您需要做的第一件事就是准确定义实体之间的关系结构。从 data 对象来看,在我看来就像

  • 宗教到专业:1 到 N(一个宗教有多个专业)
  • 专业到出勤年数:1 到 N(一个专业有多个出勤年数)
  • 出勤年到学生:1到N(一个出勤年有很多学生) 因此,我想你想要的续集回应是这样的:
religions: [ // array of religions since your'e fetching multiple
  {
     id: 1, // the religion Id
     name: string, // name of religion or whatever
     /*... any other religion attributes */
     majors: [ // array since each religion has multiple majors
        {
           id: 1, // the major Id 
           name: string, // the name of the major or whatever
           /*... any other major attributes */
           attendanceYears: [ // array since each major has multipl
              {
                  id: 1, // the first attendanceYear id
                  name: string, // name of first attendanceYear
                   /*... any other attendanceYear attributes */
                  students: [ // array since ...
                      {
                         id: 1, // student id
                         name: string, // student name
                         /*... any other student attributes */
                      },
                      {
                         id: 2, // id of second student
                         name: string, // 2nd student name
                          /*... any other student attributes */
                      }, 
                      {
                         id: 3, // id of 3rd student
                         name: string, // 3rd student name
                          /*... any other student attributes */
                      }, 
                  ]
              }, 
              {
                  id: 2, // the second attendanceYear id
                  name: string, // name of second attendanceYear
                   /*... other attributes of 2nd attendance year */
                  students: [
                      {
                         id: 4, // student id
                         name: string, // student name
                         /*... any other student attributes */
                      },
                      {
                         id: 5, // id of second student
                         name: string, // 2nd student name
                          /*... any other student attributes */
                      }, 
                      {
                         id: 6, // id of 3rd student
                         name: string, // 3rd student name
                          /*... any other student attributes */
                      }, 
                  ]
              }
           ]
        }, 
        {/*... this is another major instance in majors array */}
     ]
  }, 
  {/*... this is another religion instance in religions array*/}
]

好的。我不确定这是否是你想要的,但除了你给出的例子,这就是我正在使用的。对于代码,首先,一些配置将帮助您完成

  1. 将 sequelize db 连接实例保存在单独的文件中。我称之为db.js
const { Sequelize } = require('sequelize');

module.exports = new Sequelize('dbName', 'dbUsername', 'dbPassword', {
  host: 'localhost',
  dialect: 'mysql', // or whatever dialect you're using
});

我现在把它放在这里,只是为了清楚我在其他地方使用 db 变量时所指的是什么。然后我们创建模型 Religion.js

const { Model, Sequelize, DataTypes } = require('sequelize');
const db = require('../path/to/db.js');
// import any models to associate
const Student = require('./Student'); 

class Religion extends Model {}
Religion.init(
   {
     /* religion only attrs, let sequelize generate id*/
   }, 
   {
      sequelize: db, 
      modelName: 'religion'
   }
)
// make association
Religion.hasMany(Student);
Student.belongsTo(Religion);
module.exports = Religion;

Major.js

const { Model, Sequelize, DataTypes } = require('sequelize');
const db = require('../path/to/db.js');
// import any models to associate
const Enrollment = require('./Enrollment');
class Major extends Model {}

Major.init(
   {
      /* major only attrs, let sequelize generate id*/
   }, 
   {
      sequelize: db, 
      modelName: 'major'
   }
)
Major.hasMany(Enrollment)
Enrollment.belongsTo(Major);
module.exports = Major;

Student.js

const { Model, Sequelize, DataTypes } = require('sequelize');
const db = require('../path/to/db.js');
const Enrollment = require('./Enrollment');
class Student extends Model {}

Student.init(
   {
      religionId: {
          type: DataTypes.INTEGER,
      },
      /* other student attrs, let sequelize generate id attr */
   }, 
   {
      sequelize: db, 
      modelName: 'student'
   }
)
Student.hasMany(Enrollment);
Enrollment.belongsTo(Student);
module.exports = Student;

Enrollment.js

const { Model, Sequelize, DataTypes } = require('sequelize');
const db = require('../path/to/db.js');
class Enrollment extends Model {}

Enrollment.init(
   {
      attendanceYearId: {
          type: DataTypes.INTEGER,  // FK for attendanceYear
      }, 
      studentId: {
          type: DataTypes.INTEGER, // FK for student
      }, 
      majorId: {
          type: DataTypes.INTEGER, // FK for major
      },
      /* other 'Major' attrs, let sequelize generate id attr */
   }, 
   {
      sequelize: db, 
      modelName: 'enrollment'
   }
)
module.exports = Enrollment;

AttendanceYear.js

const { Model, Sequelize, DataTypes } = require('sequelize');
const db = require('../path/to/db.js');
const Enrollment = require('./Enrollment');
class AttendanceYear extends Model {}

AttendanceYear.init(
   {
      /* attendanceYear attrs, let sequelize generate id attr */
   }, 
   {
      sequelize: db, 
      modelName: 'attendanceYear'
   }
)

AttendanceYear.hasMany(Enrollment);
Enrollment.belongsTo(AttendanceYear);
module.exports = AttendanceYear;

这样,您的所有实体都已设置为以您请求的形状获取数据。例如(在函数中使用)

someOtherFile.js

// First import all the models you may wish to use i.e. 
const db = require('../path/to/db.js');
const Religion = require('../path/to/models/Religion');
const Major = require('../path/to/models/Major');
const AttendanceYear = require('../path/to/models/AttendanceYear');
const Student = require('../path/to/models/Student');
const Enrollment = require('../path/to/models/Enrollment');

// Uncomment the line below to update db structure if model changes are made
// db.sync({alter: true}) 

/* query function starts here */
const religions = await Religion.findAndCountAll({
   // If you want all religions then you don't need a where object
   // Also "where: {id: someValue}" should get only 1 result
   include: [{model: Major, include: [{ model: Enrollment, include:[AttendanceYear, Student]}]}]
})

值得注意的是,如果您要使用它的主键搜索某些内容,那么 .findByPK(idValueOrVariable) 对此要好得多,您还可以传入包含和其他选项等。 话虽如此,但愿这能说明续集的工作原理以及如何解决问题;但是,我觉得这不是您想要的,如果不是,那么这至少为我提出的第二个解决方案奠定了基础。

可能的解决方案2:重组

在我看来,第二个解决方案是我认为更贴切地解决问题的解决方案。从您的模型定义(并稍微考虑一下)看来,它应该是-
  • 每个Major都有许多Enrollment,反之亦然,N - N(因为一个学生可能在同一个注册中有多个专业)
  • 每个 AttendanceYear 有许多 Enrollment,1 - N
  • 每个 Religion 有许多 Student,1 - N,
  • 每个 Student 可以有多个 Enrollment(扩展为 Major),1 - N 因此,第一步就是,恕我直言,弄清楚哪个是“父母”,以了解如何以及在何处进行正确的关联。但是,这将从根本上改变您的响应 json 的形成方式,因为某些实体之间没有直接关系(例如,ReligionMajor 没有任何直接关系,除了通过 {{ 1}} -> Student -> 然后是 Enrollment)。因此,响应将类似于宗教[i].students[i].enrollments[i].majors[i]。在这种情况下,直接按 Major 的顺序对 Major 进行排序将是您 获取所有宗教及其嵌套对象并映射 {{ 1}} 和 Religion 并根据需要对它们进行排序。据我所知,没有单个查询(或嵌套查询的组合)可以在没有直接(甚至间接)外键的 SQL 数据库中为您执行此操作 - 剧透警报,这就是续集错误即将到来的地方来自。

不过,有办法

。请鼓点...“通过”表。即充当模型之间关系表的中间/连接表。虽然通常用于 N - N 关系,但它们也可以在这样的情况下使用,通过创建连接表来创建以前可能不存在的关联。然而值得注意的是,使用直通表/连接表带来的复杂性 - See the docs on that here

总的来说,我认为最有效的数据库建模方法是这样的

Image Showing DB Design

那么,我们将如何做到这一点?我们需要创建专业招生、专业到出勤、宗教到专业的“直通”模式。 *** 更新 *** “通过”模型看起来像这样:

Major

Student

ReligionMajors

const { Model, Sequelize, DataTypes } = require('sequelize');
const db = require('../path/to/db.js');
// import any models to associate
const Religion = require('./Religion');
const Major = require('./Major');

class ReligionMajors extends Model {}
ReligionMajors.init({
  religionId: {
    type: DataTypes.INTEGER,
    references: {  // You don't need to include this, just showing for reference 
      model: Religion,
      key: 'id'
    }
  },
  majorId: {
    type: DataTypes.INTEGER,
    references: {  // again you don't need this, just showing for reference
      model: Major,
      key: 'id'
    }
  }
});
Religion.belongsToMany(Major, { through: ReligionMajors });
Major.belongsToMany(Religion, { through: ReligionMajors});

EnrollmentMajors

const { Model, Sequelize, DataTypes } = require('sequelize');
const db = require('../path/to/db.js');
// import any models to associate
const Enrollment = require('./Enrollment');
const Major = require('./Major');

class EnrollmentMajors extends Model {}
EnrollmentMajors.init({
  enrolmentId: {
    type: DataTypes.INTEGER,
  },
  majorId: {
    type: DataTypes.INTEGER,
  }
});
Enrollment.belongsToMany(Major, { through: EnrollmentMajors });
Major.belongsToMany(Enrollment, { through: EnrollmentMajors});

这个问题的棘手之处在于,您可能必须开始考虑何时以及如何将这些关联记录在案。此外,这会将 AttendanceYearMajorsconst { Model, Sequelize, DataTypes } = require('sequelize'); const db = require('../path/to/db.js'); // import any models to associate const AttendanceYear = require('./AttendanceYear'); const Major = require('./Major'); class AttendanceYearMajors extends Model {} AttendanceYearMajors.init({ attendanceYearId: { type: DataTypes.INTEGER, }, majorId: { type: DataTypes.INTEGER, } }); AttendanceYear.belongsToMany(Major, { through: AttendanceYearMajors }); Major.belongsToMany(AttendanceYear, { through: AttendanceYearMajors}); 模型之间的关系更改为多对多关系,但这没关系。

正如我之前所说,我们现在可以做的是弄清楚何时以及如何在“通过”模型中创建记录以创建我们需要的关联。

进行 MajorEnrollments 关联的一种方法是,基本上使用您拥有的数据执行一系列步骤,即

Religion

请注意,我将代码放在 try-catch 块中。这通常是一个很好的做法,因此您可以轻松查看 sequelize 可能抛出的任何错误(如果有)...