Reputation: 476
I'm trying to get data like below:
"data": {
"religions": {
"Major1Name": {
"AttendanceYear1Name": {
"student": [ {...}, {...}, {...} ]
},
"AttendanceYear2Name": {
"student": [ {...}, {...}, {...} ]
}
},
"Major2Name": {
"AttendanceYear1Name": {
"student": [ {...}, {...}, {...} ]
},
"AttendanceYear2Name": {
"student": [ {...}, {...}, {...} ]
}
}
}
}
I know how to set up a basic level of associations for eg. student and major. But in my database knowledge, I have no idea how to associate with religions
and majors
, and also in Sequelize
. Please help.
I have the followings tables:
Below is my models.
majors
'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;
};
religions
"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;
};
students
'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;
};
and enrollments
'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;
};
What I've done and doesn't work
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,
},
},
],
},
],
},
],
},
],
});
Error
SequelizeEagerLoadingError: Major is not associated to Religion!
Updated
I have the followings models (which will be the tables in the database) files in this structure src/database/models/
:
The whold structure is:
database/migrations/....js
database/models/....js
database/seeders/....js
I have an index.js
file inside that models/
directory and it is like below:
'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;
With that implementation, I don't need to import the required models inside the model files and also route handlers and just need the following line.
const models = require('../database/models');
/** I can easily get model instance by calling models.Student to get Student model. **/
And the reason that I don't use the sync
approach is that I'm afraid to accidentally lose my data in the production if I update the models or add a new one. Therefore, I used sequelize-cli
. With it, I can turn my models into tables by running sequelize db:migrate
.
The reason that I have explicitly defined the attribute and table name is that I want them to follow the MySQL naming conventions: attendance_years
and attendance_year_id
for instance. But when I run the calls to the database, I see lots of naming aliases in the terminal: attendance_year_id as attendanceYearId etc. I think this might impact the querying performance and so, I will consider letting sequelize completely manage the naming conventions.
Upvotes: 1
Views: 2946
Reputation: 58
Thanks for reaching out to me on Twitter. I really appreciate it. That being said, let's see if we can go about answering your question. Please allow me to geek out a bit before getting to the solution I wish to provide.
primaryKey
attributes and included the tableName
attribute in the options object of your model definitions, which is okay, but really unnecessary, and could in fact actually interfere with the sequelize engines' queries in which case you may have to define these attributes everywhere and that's just a mess. Sequelize generates primaryKey
attributes and tableName
s by default - so, if you can, please minimize unnecessary definitions as much as possible - See why from the docs on table name inference here. If you do feel the need to have your own custom key for models, consider using a UUID attribute, like so.// 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
}
That saves you both the trouble of having to uniquely name primaryKey fields, as well as preventing situations where keys may have similar values. It also gives you an extra unique attribute to use in your queries to ensure you get a single record.
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;
Also worth keeping in mind is that, when you associate model entities, sequelize will automatically pluralize the name of the entity in the results, depending on the relationship (i.e. if the parent entity hasMany of the child entity), and returns the results as an array. e.g. if Religion.hasMany(Major)
, the result will return religion.majors = [/*an array of majors*/]
.
Religion
and Major
models -in Major.js
model file you can specify the religion FK like this
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;
Then in 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;
As a side note, you very often don't have to include the onDelete
and onUpdate
attributes in the association, as the defaults are well suited for most use cases. Also worth noting, you can have multiple relationships, in which case you can use aliases. But that doesn't seem to be necessary or relevant to your question (at least from the onset), but still worth noting and very useful.
The very first thing you need to do is define exactly what the structure of the relationships between the Entities will be like. From the data
object, it appears to me to be something like
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*/}
]
Okay. I'm not sure if this is what you're going for, but going off of the example you gave, that's what I'm working with. For the code, first, some configurations that will help you down the line
db.js
const { Sequelize } = require('sequelize');
module.exports = new Sequelize('dbName', 'dbUsername', 'dbPassword', {
host: 'localhost',
dialect: 'mysql', // or whatever dialect you're using
});
I'm putting this here now, just so it's clear what I'm referring to when using the db
variable elsewhere. Then we create the models
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;
And with that, all your entities are setup to get the data in the that shape you requested. e.g. (using in a function)
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]}]}]
})
Worth noting, if you're going to search for something using it's primary key, then the .findByPK(idValueOrVariable)
is much better for that, and you can also pass in includes and other options etc.
That being said, Hopefully this sheds some light on how sequelize works and how you can approach the problem; However, I get the feeling that this isn't what you're going for, and if not, then this at least lays the ground work for the 2nd solution I'm proposing.
Major
has many Enrollment
s and vice versa, N - N (because a student may have multiple majors in the same enrollment)AttendanceYear
has many Enrollment
s, 1 - NReligion
has many Student
s, 1 - N,Student
can have many Enrollment
s (and by extension, Major
s), 1 - N
Thus first step would then be, imho, figuring out which is 'parent' to which, to know how and where to make the right associations. However, this will fundamentally change the way your reponse json will be shaped, seeing as there's no direct relationships between some entities (for instance, Religion
is not directly related to Major
in any way except through Student
-> Enrollment
-> then Major
). So the response would be something like religions[i].students[i].enrollments[i].majors[i]. And in that case, to directly sort Major
s in order of Religion
s would be something you would do after getting all the religions and their nested objects, and mapping Major
s by Student
s and sorting them however you want. As far as I know, there's no single query (or combination of nested queries) that can do this for you in an SQL db without a direct (or even an indirect) Foreign Key - Spoiler alert, this is where the sequelize error is coming from.Overall, I think the most effective way to model the database would be something like this
So, how would we do this? We need to create "through" models for enrollment to major, major to attendance, and religion to major. *** Update *** The "through" models will looks something like this:
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});
The tricky part with this is that you may have to start thinking about when and how you want to make these associations on a record. Also, this changes the relationship between the Major
and Enrollments
models to a many to many relationship, but that's okay.
What we can now do, like I said before, is figure out when and how to create records in the 'through' models to create the associations we need.
One way to do the Religion
to Major
association would be to, basically perform a series of steps with the data you have i.e.
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)
}
Notice I put the code in a try-catch block. This is good practice generally, so you can easily see whatever errors sequelize might throw (if any)...
Upvotes: 2
Reputation: 22758
You need to define an association in religions
file like this next to the Religion.hasMany(models.Student
association:
Religion.hasMany(models.Major, {
foreignKey: "religionId",
as: "major",
});
Upvotes: 2