Relação Muitos-para-Muitos (Many-to-Many) com Sequelize no Node.js
Uma relação Muitos-para-Muitos implica que uma entidade de um tipo pode ter conexões simultâneas com várias entidades de outro tipo, e vice-versa. Por exemplo, um estudante pode estar matriculado em vários cursos universitários, assim como um curso universitário pode ser frequentado por vários estudantes. Nesse caso, temos uma relação de muitos para muitos.
Fisicamente, no nível do banco de dados, é comum criar uma tabela intermediária para estabelecer essa conexão entre as duas tabelas principais. No Sequelize, para criar uma relação muitos-para-muitos entre duas entidades, é necessário definir um modelo intermediário. Vamos considerar o exemplo com cursos e estudantes:
const Sequelize = require("sequelize");
// definindo o objeto Sequelize
const sequelize = new Sequelize({
dialect: "sqlite",
storage: "programicio.db",
define: {
timestamps: false
}
});
const Student = sequelize.define("student", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
name: {
type: Sequelize.STRING,
allowNull: false
}
});
const Course = sequelize.define("course", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
name: {
type: Sequelize.STRING,
allowNull: false
}
});
// entidade intermediária que conecta curso e estudante
const Enrolment = sequelize.define("enrolment", {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
grade: { // nota do estudante no curso
type: Sequelize.INTEGER,
allowNull: false
}
});
Student.belongsToMany(Course, {through: Enrolment});
Course.belongsToMany(Student, {through: Enrolment});
sequelize.sync({force:true}).then(() => {
console.log("Tabelas foram criadas");
}).catch(err => console.log(err));
Aqui, o modelo Enrolment
grade
Para criar a relação muitos-para-muitos, utiliza-se o método belongsToMany()
through
Como resultado, ao executar este código, três tabelas serão criadas no banco de dados SQLite por meio dos seguintes comandos SQL:
CREATE TABLE IF NOT EXISTS `students` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(255) NOT NULL);
CREATE TABLE IF NOT EXISTS `courses` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(255) NOT NULL);
CREATE TABLE IF NOT EXISTS `enrolments` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT, `grade` INTEGER NOT NULL,
`studentId` INTEGER REFERENCES `students` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
`courseId` INTEGER REFERENCES `courses` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE (`studentId`, `courseId`)
);
Adicionando Dados Relacionados
Ao estabelecer uma relação muitos-para-muitos, os modelos podem utilizar o método addNomeDoModelo()
student.addCourse()
course.addStudent()
Course.create({ name: "JavaScript"});
Course.create({ name: "TypeScript"});
Course.create({ name: "Node.js"});
Student.create({ name: "Tom"});
Student.create({ name: "Bob"});
Student.create({ name: "Alice"});
Agora, vamos adicionar o curso de JavaScript ao estudante chamado Tom:
// obtendo o estudante chamado Tom
Student.findOne({where: {name: "Tom"}})
.then(student => {
if (!student) return;
// adicionando o curso de JavaScript ao Tom
Course.findOne({where: {name: "JavaScript"}})
.then(course => {
if (!course) return;
student.addCourse(course, {through:{grade:1}});
});
});
O primeiro parâmetro em student.addCourse()
grade
enrolments
INSERT INTO `enrolments` (`id`,`grade`,`studentId`,`courseId`) VALUES (NULL,1,1,3);
Obtendo Dados Relacionados
Para obter dados relacionados, cada um dos modelos envolvidos na relação pode usar o método getNomeDoModelo()
Student.findOne({where: {name: "Tom"}})
.then(student=>{
if (!student) return;
student.getCourses().then(courses => {
for (course of courses) {
console.log(course.name);
}
});
});
Na realidade, neste caso, não obtemos apenas os cursos da tabela courses
enrolments
SELECT `course`.`id`, `course`.`name`, `enrolment`.`id` AS `enrolment.id`, `enrolment`.`grade` AS `enrolment.grade`, `enrolment`.`studentId` AS `enrolment.studentId`, `enrolment`.`courseId` AS `enrolment.courseId` FROM `courses` AS `course` INNER JOIN `enrolments` AS `enrolment` ON `course`.`id` = `enrolment`.`courseId` AND `enrolment`.`studentId` = 1;
Ou seja, podemos obter o nome e o id do curso, bem como o id e a nota (grade
Enrolment
Removendo Dados Relacionados
Para remover dados relacionados, é necessário obter o objeto da tabela intermediária e excluí-lo. Por exemplo, vamos remover o curso de JavaScript do estudante chamado Tom:
Student.findOne({where: {name: "Tom"}})
.then(student => {
if (!student) return;
student.getCourses().then(courses => {
for (course of courses) {
if (course.name === "JavaScript") course.enrolment.destroy();
}
});
});