Sequelize 中的关联
Association in Sequelize
我正在尝试获取如下数据:
"data": {
"religions": {
"Major1Name": {
"AttendanceYear1Name": {
"student": [ {...}, {...}, {...} ]
},
"AttendanceYear2Name": {
"student": [ {...}, {...}, {...} ]
}
},
"Major2Name": {
"AttendanceYear1Name": {
"student": [ {...}, {...}, {...} ]
},
"AttendanceYear2Name": {
"student": [ {...}, {...}, {...} ]
}
}
}
}
我知道如何为例如建立基本级别的关联。学生和专业。但是以我的数据库知识,我不知道如何与 religions
和 majors
以及 Sequelize
关联。请帮忙。
我有以下 tables:
- 专业
- attendance_years
- 宗教
- 学生
- 注册人数
下面是我的模型。
专业
'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;
};
attendance_years
"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!
已更新
我在这个结构中有以下模型(将是数据库中的 tables)文件 src/database/models/
:
- 专业
- attendance_years
- 宗教
- 学生
- 注册人数
整个结构是:
database/migrations/....js
database/models/....js
database/seeders/....js
我在 models/
目录中有一个 index.js
文件,如下所示:
'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
。有了它,我可以通过 运行ning sequelize db:migrate
.
将我的模型变成 tables
我明确定义属性和 table 名称的原因是我希望它们遵循 MySQL 命名约定:例如 attendance_years
和 attendance_year_id
.但是当我 运行 调用数据库时,我在终端中看到很多命名别名: attendance_year_id as attendanceYearId 等。我认为这可能会影响查询性能,因此,我会考虑让 sequelize 完全管理命名约定。
您需要在 religions
文件中像这样在 Religion.hasMany(models.Student
关联旁边定义一个关联:
Religion.hasMany(models.Major, {
foreignKey: "religionId",
as: "major",
});
感谢您在 Twitter 上联系我。我真的很感激。话虽如此,让我们看看是否可以着手回答您的问题。在获得我希望提供的解决方案之前,请允许我先了解一下。
首先,根据我的个人经验提出一些建议。
- Sequelize 非常强大。在引擎盖下它为您解决了很多问题,这意味着您不必担心很多事情,例如 Primary key attributes,Foreign key column names,table names 等。在一些非常复杂的关联之外或在某些边缘情况下(例如,如果您正在使用遗留数据库),您实际上不必明确声明它们是什么,而是让 sequelize 做为你举重。我提到这一点是因为我注意到您尝试指定
primaryKey
属性并在模型定义的选项对象中包含 tableName
属性,这没问题,但确实没有必要,而且实际上可能会干扰续集引擎的查询,在这种情况下,您可能必须在任何地方定义这些属性,这简直是一团糟。 Sequelize 默认生成 primaryKey
属性和 tableName
s - 所以,如果可以的话,请尽可能减少不必要的定义 - 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
}
这既省去了必须唯一命名主键字段的麻烦,又避免了键可能具有相似值的情况。它还为您提供了一个额外的唯一属性,可在您的查询中使用,以确保您获得一条记录。
- 我还注意到您尝试在模型定义中的静态方法中定义模型关联。我不确定你为什么这样做,但我不认为这就是关联的定义方式,无论是根据我的经验还是官方续集文档(版本 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 会自动复数结果中的实体名称,具体取决于关系(即如果父实体有许多子实体),并且returns 作为数组的结果。例如如果 Religion.hasMany(Major)
,结果将 return religion.majors = [/*an array of majors*/]
.
- 从上面的示例中,您可以开始看到一些可以立即解决您的问题的更改。但在提出我的解决方案之前,我想提最后一件事。同样,与前一点不无关系,我注意到您尝试在某些属性上指定引用。你不需要这样做。那是一种 NoSQL 的事情。定义属性和它的类型就足够了,当建立关联时,sequelize 将找出外键。您还可以在关联中指定其他详细信息。例如;假设
Religion
和 Major
模型之间存在 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;
附带说明一下,您通常不必在关联中包含 onDelete
和 onUpdate
属性,因为默认值非常适合大多数用例。同样值得注意的是,您可以有多个关系,在这种情况下您可以使用别名。但这似乎与您的问题没有必要或相关(至少从一开始是这样),但仍然值得注意并且非常有用。
话虽这么说。现在回答您的问题:(可能的解决方案 1 - 简单方法)
您需要做的第一件事就是准确定义实体之间的关系结构。从 data
对象来看,在我看来它类似于
- 宗教到专业:1 到 N(一个宗教有很多专业)
- Major to AttendanceYear: 1 to 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*/}
]
好的。我不确定这是否是您想要的,但是从您给出的示例来看,这就是我正在使用的。对于代码,首先,一些 配置 将帮助您完成工作
- 将 sequelize 数据库连接实例保存在单独的文件中。我叫它
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)
会好得多,您还可以传入包含和其他选项等。
话虽这么说,但愿这能阐明 sequelize 的工作原理以及您如何解决该问题;但是,我觉得这不是您想要的,如果不是,那么这至少为我提出的第二个解决方案奠定了基础。
可能的解决方案 2:重组
在我看来,第二个解决方案是我认为更能解决问题的解决方案。从您的模型定义(并稍微考虑一下)看来它应该是 -
- 每个
Major
有很多Enrollment
,反之亦然,N - N(因为一个学生可能在同一个招生中有多个专业)
- 每个
AttendanceYear
有很多 Enrollment
,1 - N
- 每个
Religion
有很多Student
s, 1 - N,
- 每个
Student
可以有很多 Enrollment
(并且推而广之,Major
),1 - N
因此,第一步将是,恕我直言,弄清楚哪个是 'parent' 到哪个,以了解如何以及在何处进行正确的关联。但是,这将从根本上改变您的响应 json 的形成方式,因为某些实体之间没有直接关系(例如,Religion
与 Major
没有任何直接关系除了通过 Student
-> Enrollment
-> 然后 Major
)。所以响应会类似于 religions[i].students[i].enrollments[i].majors[i]。在这种情况下,直接按照 Religion
的顺序对 Major
进行排序将是您在 之后 获取所有宗教及其嵌套对象后要做的事情,并且将 Major
映射到 Student
并根据需要对它们进行排序。据我所知,没有一个查询(或嵌套查询的组合)可以在没有直接(甚至间接)外键的情况下在 SQL 数据库中为您执行此操作 - 剧透警报,这就是续集错误来自。
不过,还是有办法的
。请打鼓... "Through" 表格。即 intermediate/junction tables 充当模型之间的关系 tables。虽然通常用于 N - N 关系,但它们也可以用于这种情况,通过创建连接 table 来创建 none 以前可能存在的关联。然而,值得注意的是,使用 through/junction tables - See the docs on that here 会带来错综复杂的情况
总的来说,我认为最有效的数据库建模方法是这样的
Image Showing DB Design
那么,我们该怎么做呢?我们需要创建招生到专业、专业到出勤率、宗教到专业的“直通”模式。
*** 更新 ***
“通过”模型看起来像这样:
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});
AttendanceYearMajors
const { 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});
棘手的部分是您可能必须开始考虑何时以及如何在记录上建立这些关联。此外,这将 Major
和 Enrollments
模型之间的关系更改为多对多关系,但这没关系。
正如我之前所说,我们现在可以做的是弄清楚何时以及如何在 'through' 模型中创建记录以创建我们需要的关联。
实现 Religion
到 Major
关联的一种方法是,基本上使用您拥有的数据执行一系列步骤,即
const db = require('../path/to/db.js');
const Enrollment = require('../path/to/model/Enrollment');
const Major = require('../path/to/model/Major');
const Student = require('../path/to/model/Student');
const Religion = require('../path/to/model/Religion');
const EnrollmentMajors = require('../path/to/model/EnrollmentMajors');
const ReligionMajors = require('../path/to/model/ReligionMajors');
try{
const studentEnrollment = await Enrollment.create(
{
studentId: studentIdFromReq,
attendanceYearId: attendanceYearIdFromRequest,
}
);
if(studenEnrollment){
// associate the Enrollment with the Major if you have the Major id
const studentEnrolledMajor = await EnrollmentMajors.create(
{
enrollmentId: studentEnrollment.id,
majorId: majorId
}
)
// Then, get the students' Religion Id, and associate with Major
const studentWithReligion = await Student.findByPK(studentIdFromReq,
{include: [Religion]}
)
const religionMajorAssociation = await ReligionMajors.findOrCreate(
{
religionId: studentWithReligion.religion.id, // or student.religionId
majorId: majorId
}
)
/* we use findOrCreate to avoid making duplicate religion-major assocs */
if(religionMajorAssociation){
// The association was created successfully, so you can do whatever else
}
}
} catch(err){
console.log(err)
}
请注意,我将代码放在 try-catch 块中。这通常是一个很好的做法,所以你可以很容易地看到 sequelize 可能抛出的任何错误(如果有的话)...
我正在尝试获取如下数据:
"data": {
"religions": {
"Major1Name": {
"AttendanceYear1Name": {
"student": [ {...}, {...}, {...} ]
},
"AttendanceYear2Name": {
"student": [ {...}, {...}, {...} ]
}
},
"Major2Name": {
"AttendanceYear1Name": {
"student": [ {...}, {...}, {...} ]
},
"AttendanceYear2Name": {
"student": [ {...}, {...}, {...} ]
}
}
}
}
我知道如何为例如建立基本级别的关联。学生和专业。但是以我的数据库知识,我不知道如何与 religions
和 majors
以及 Sequelize
关联。请帮忙。
我有以下 tables:
- 专业
- attendance_years
- 宗教
- 学生
- 注册人数
下面是我的模型。
专业
'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;
};
attendance_years
"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!
已更新
我在这个结构中有以下模型(将是数据库中的 tables)文件 src/database/models/
:
- 专业
- attendance_years
- 宗教
- 学生
- 注册人数
整个结构是:
database/migrations/....js
database/models/....js
database/seeders/....js
我在 models/
目录中有一个 index.js
文件,如下所示:
'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
。有了它,我可以通过 运行ning sequelize db:migrate
.
我明确定义属性和 table 名称的原因是我希望它们遵循 MySQL 命名约定:例如 attendance_years
和 attendance_year_id
.但是当我 运行 调用数据库时,我在终端中看到很多命名别名: attendance_year_id as attendanceYearId 等。我认为这可能会影响查询性能,因此,我会考虑让 sequelize 完全管理命名约定。
您需要在 religions
文件中像这样在 Religion.hasMany(models.Student
关联旁边定义一个关联:
Religion.hasMany(models.Major, {
foreignKey: "religionId",
as: "major",
});
感谢您在 Twitter 上联系我。我真的很感激。话虽如此,让我们看看是否可以着手回答您的问题。在获得我希望提供的解决方案之前,请允许我先了解一下。
首先,根据我的个人经验提出一些建议。
- Sequelize 非常强大。在引擎盖下它为您解决了很多问题,这意味着您不必担心很多事情,例如 Primary key attributes,Foreign key column names,table names 等。在一些非常复杂的关联之外或在某些边缘情况下(例如,如果您正在使用遗留数据库),您实际上不必明确声明它们是什么,而是让 sequelize 做为你举重。我提到这一点是因为我注意到您尝试指定
primaryKey
属性并在模型定义的选项对象中包含tableName
属性,这没问题,但确实没有必要,而且实际上可能会干扰续集引擎的查询,在这种情况下,您可能必须在任何地方定义这些属性,这简直是一团糟。 Sequelize 默认生成primaryKey
属性和tableName
s - 所以,如果可以的话,请尽可能减少不必要的定义 - 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
}
这既省去了必须唯一命名主键字段的麻烦,又避免了键可能具有相似值的情况。它还为您提供了一个额外的唯一属性,可在您的查询中使用,以确保您获得一条记录。
- 我还注意到您尝试在模型定义中的静态方法中定义模型关联。我不确定你为什么这样做,但我不认为这就是关联的定义方式,无论是根据我的经验还是官方续集文档(版本 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 会自动复数结果中的实体名称,具体取决于关系(即如果父实体有许多子实体),并且returns 作为数组的结果。例如如果 Religion.hasMany(Major)
,结果将 return religion.majors = [/*an array of majors*/]
.
- 从上面的示例中,您可以开始看到一些可以立即解决您的问题的更改。但在提出我的解决方案之前,我想提最后一件事。同样,与前一点不无关系,我注意到您尝试在某些属性上指定引用。你不需要这样做。那是一种 NoSQL 的事情。定义属性和它的类型就足够了,当建立关联时,sequelize 将找出外键。您还可以在关联中指定其他详细信息。例如;假设
Religion
和Major
模型之间存在 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;
附带说明一下,您通常不必在关联中包含 onDelete
和 onUpdate
属性,因为默认值非常适合大多数用例。同样值得注意的是,您可以有多个关系,在这种情况下您可以使用别名。但这似乎与您的问题没有必要或相关(至少从一开始是这样),但仍然值得注意并且非常有用。
话虽这么说。现在回答您的问题:(可能的解决方案 1 - 简单方法)
您需要做的第一件事就是准确定义实体之间的关系结构。从 data
对象来看,在我看来它类似于
- 宗教到专业:1 到 N(一个宗教有很多专业)
- Major to AttendanceYear: 1 to 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*/}
]
好的。我不确定这是否是您想要的,但是从您给出的示例来看,这就是我正在使用的。对于代码,首先,一些 配置 将帮助您完成工作
- 将 sequelize 数据库连接实例保存在单独的文件中。我叫它
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)
会好得多,您还可以传入包含和其他选项等。
话虽这么说,但愿这能阐明 sequelize 的工作原理以及您如何解决该问题;但是,我觉得这不是您想要的,如果不是,那么这至少为我提出的第二个解决方案奠定了基础。
可能的解决方案 2:重组
在我看来,第二个解决方案是我认为更能解决问题的解决方案。从您的模型定义(并稍微考虑一下)看来它应该是 -- 每个
Major
有很多Enrollment
,反之亦然,N - N(因为一个学生可能在同一个招生中有多个专业) - 每个
AttendanceYear
有很多Enrollment
,1 - N - 每个
Religion
有很多Student
s, 1 - N, - 每个
Student
可以有很多Enrollment
(并且推而广之,Major
),1 - N 因此,第一步将是,恕我直言,弄清楚哪个是 'parent' 到哪个,以了解如何以及在何处进行正确的关联。但是,这将从根本上改变您的响应 json 的形成方式,因为某些实体之间没有直接关系(例如,Religion
与Major
没有任何直接关系除了通过Student
->Enrollment
-> 然后Major
)。所以响应会类似于 religions[i].students[i].enrollments[i].majors[i]。在这种情况下,直接按照Religion
的顺序对Major
进行排序将是您在 之后 获取所有宗教及其嵌套对象后要做的事情,并且将Major
映射到Student
并根据需要对它们进行排序。据我所知,没有一个查询(或嵌套查询的组合)可以在没有直接(甚至间接)外键的情况下在 SQL 数据库中为您执行此操作 - 剧透警报,这就是续集错误来自。
不过,还是有办法的
。请打鼓... "Through" 表格。即 intermediate/junction tables 充当模型之间的关系 tables。虽然通常用于 N - N 关系,但它们也可以用于这种情况,通过创建连接 table 来创建 none 以前可能存在的关联。然而,值得注意的是,使用 through/junction tables - See the docs on that here 会带来错综复杂的情况总的来说,我认为最有效的数据库建模方法是这样的
Image Showing DB Design
那么,我们该怎么做呢?我们需要创建招生到专业、专业到出勤率、宗教到专业的“直通”模式。 *** 更新 *** “通过”模型看起来像这样:
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});
AttendanceYearMajors
const { 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});
棘手的部分是您可能必须开始考虑何时以及如何在记录上建立这些关联。此外,这将 Major
和 Enrollments
模型之间的关系更改为多对多关系,但这没关系。
正如我之前所说,我们现在可以做的是弄清楚何时以及如何在 'through' 模型中创建记录以创建我们需要的关联。
实现 Religion
到 Major
关联的一种方法是,基本上使用您拥有的数据执行一系列步骤,即
const db = require('../path/to/db.js');
const Enrollment = require('../path/to/model/Enrollment');
const Major = require('../path/to/model/Major');
const Student = require('../path/to/model/Student');
const Religion = require('../path/to/model/Religion');
const EnrollmentMajors = require('../path/to/model/EnrollmentMajors');
const ReligionMajors = require('../path/to/model/ReligionMajors');
try{
const studentEnrollment = await Enrollment.create(
{
studentId: studentIdFromReq,
attendanceYearId: attendanceYearIdFromRequest,
}
);
if(studenEnrollment){
// associate the Enrollment with the Major if you have the Major id
const studentEnrolledMajor = await EnrollmentMajors.create(
{
enrollmentId: studentEnrollment.id,
majorId: majorId
}
)
// Then, get the students' Religion Id, and associate with Major
const studentWithReligion = await Student.findByPK(studentIdFromReq,
{include: [Religion]}
)
const religionMajorAssociation = await ReligionMajors.findOrCreate(
{
religionId: studentWithReligion.religion.id, // or student.religionId
majorId: majorId
}
)
/* we use findOrCreate to avoid making duplicate religion-major assocs */
if(religionMajorAssociation){
// The association was created successfully, so you can do whatever else
}
}
} catch(err){
console.log(err)
}
请注意,我将代码放在 try-catch 块中。这通常是一个很好的做法,所以你可以很容易地看到 sequelize 可能抛出的任何错误(如果有的话)...