Отношение многие-ко-многим предполагает, что сущность одного типа одновременно может иметь связи с множеством сущностей другого типа и наоборот. Например, один студент может посещать несколько университетских курсов. Соответственно один университетский курс может посещаться множеством студентов. То есть есть в данном сслучае имеем связь многие ко многим.
Физически на уровне базы данных обычно для создания подобной связи создается промежуточная таблица, через которую связываются две основные таблицы. В Sequelize поэтому для связи двух сущностей отношением многие-ко-многим нам надо задать промежуточную модель. Так, возьмем пример с курсами и студентами:
const Sequelize = require("sequelize"); // определяем объект Sequelize const sequelize = new Sequelize({ dialect: "sqlite", storage: "metanit.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 } }); // промежуточная сущность, которая связывает курс и студента const Enrolment = sequelize.define("enrolment", { id: { type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true, allowNull: false }, grade: { // оценка студента по данному курсу type: Sequelize.INTEGER, allowNull: false } }); Student.belongsToMany(Course, {through: Enrolment}); Course.belongsToMany(Student, {through: Enrolment}); sequelize.sync({force:true}).then(()=>{ console.log("Tables have been created"); }).catch(err=>console.log(err));
Здесь в качестве промежуточной сущности выступает модель Enrolment - по сути данные успеваемости определенного студента по определенному курсу. В этой модели можно определить различные свойства. Так, в данному случае определено свойство "grade", которое призвано хранить оценку студена по данному курсу. Аналогично в этой модели мы могли бы определить какие-нибудь атрибуты, которые бы связывали студента с курсом, например, дату поступления на данный курс, дату окончания и т.д.
Непосредственно для создания связи многие-ко-многим применяется метод belongsToMany(). Первый параметр метода - сущность, с которой надо установить связь. Второй параметр - объект конфигурации связи, который с помощью параметра through обязательно должен задавать промежуточную сущность, через которую будут связаны обе основные сущности.
В итоге при выполнении данного кода в базе данных SQLite будут созданы три таблицы с помощью следующих 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`) );
При установке связи многие ко многим модели могут использовать метод addИМЯ_МОДЕЛИ()
для добавления объектов (например,
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"});
Добавим студенту с именем Tom курс по JavaScript:
// получаем пользователя с именем Tom Student.findOne({where: {name: "Tom"}}) .then(student=>{ if(!student) return; // добавим Тому курс по JavaScript Course.findOne({where: {name: "JavaScript"}}) .then(course=>{ if(!course) return; student.addCourse(course, {through:{grade:1}}); }); });
Первым параметром в student.addCourse()
передается добавляемый курс. Вторым параметром устанавливается значение для столбца grade в таблице enrolments.
В итоге данный метод будет выполнять sql-команду:
INSERT INTO `enrolments` (`id`,`grade`,`studentId`,`courseId`) VALUES (NULL,1,1,3);
Для получения связанных данных у каждой из моделей, участвующих в связи, мы можем использовать метод getИМЯ_МОДЕЛИs()
. Например,
получим все курсы студента по имени Tom:
Student.findOne({where: {name: "Tom"}}) .then(student=>{ if(!student) return; student.getCourses().then(courses=>{ for(course of courses){ console.log(course.name); } }); });
Однако в реальности в данном случае мы получаем не просто курс из таблицы courses, а сводные данные на основании таблицы enrolments - выполняемая sql-команда в данном случае будет выглядеть следующим образом:
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;
То есть в данном случае мы сможем получить название и id курса, а также id и значение grade объекта Enrolment:
Student.findOne({where: {name: "Tom"}}) .then(student=>{ if(!student) return; student.getCourses().then(courses=>{ for(course of courses){ console.log("course:", course.name, "grade:", course.enrolment.grade); } }); });
Для удаления связанных данных необходимо получить объект из промежуточной таблицы и удалить его. Например, удалим у студента по имени Tom курс JavaScript:
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(); } }); });