자, 이제 드디어 테이블 간의 관계를 설정할 것이다. 지금 존재하는 테이블들을 간단히 살펴보자.
테이블 간의 관계는 크게 1:1, 1:M(1대다), M:N(다대다) 이렇게 3가지로 나누어볼 수 있다. 지금 이 테이블들 사이에는 이 관계들이 모두 존재한다.
(1) 일단 학생 한 명 당 하나의 장학금 통장만을 가질 것이므로 Students - ScholarshipAccounts 간의 관계는 1:1
(2) 수업 하나는 교수님 한 명이 담당해서 가르치지만, 교수님 한 명은 여러 개의 수업을 가르칠 수 있기 때문에 Professors - Courses 간의 관계는 1:M
(3) 수업 하나는 여러 명의 학생들이 듣고 한 명의 학생은 여러 수업을 들을 수 있기 때문에 Courses - Students 간의 관계는 M:N
이라고 할 수 있다. 이 각각의 관계를 코드로 어떻게 설정할 수 있는지, 코드로 설정하면 실제 데이터베이스에 무엇이 반영되는지 살펴보겠다.
일단 app.js 파일을 이렇게 수정하자.
// app.js
const db = require('./models/index.js');
const sequelize = db.sequelize;
(async () => {
await sequelize.sync({ force: true });
})();
그리고 Student.js 파일에서 associate 함수 안을 이렇게 채우자.
// Student.js
'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Student extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
Student.hasOne(models.ScholarshipAccount); //
}
}
Student.init(
{
registrationNum: DataTypes.STRING,
name: DataTypes.STRING,
age: DataTypes.INTEGER,
},
{
sequelize,
modelName: 'Student',
}
);
return Student;
};
이 부분이 아주 중요하다. 그리고 ScholarshipAccount.js 파일은 이렇게 수정해주어야 한다.
// ScholarshipAccount.js
'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class ScholarshipAccount extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
ScholarshipAccount.belongsTo(models.Student); //
}
}
ScholarshipAccount.init(
{
accountNum: DataTypes.STRING,
validDate: DataTypes.DATE,
balance: DataTypes.INTEGER,
},
{
sequelize,
modelName: 'ScholarshipAccount',
}
);
return ScholarshipAccount;
};
지금 두 파일에서 바뀐 부분은
// Student.js
~~
Student.hasOne(models.ScholarshipAccount);
~~
// Scholarship.js
~~
ScholarshipAccount.belongsTo(models.Student);
~~
이 부분이다. hasOne 메소드와 belongsTo 메소드를 볼 수 있는데 1:1 관계는 바로 이런 식으로 설정한다. 이때 foreign key를 갖고 있는 테이블에서 belongsTo를 호출해줘야 한다. 이 associate 메소드 내부의 코드들은 models 디렉토리의 index.js 파일에 있는 코드 중
~~
Object.keys(db).forEach((modelName) => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
~~
이 부분이 실행될 때 실행된다. 자, 전체 코드를 실행해보자.
node app.js
를 실행하면
ScholarshipAccounts 테이블에 자동으로 StudentId가 생긴 것을 확인할 수 있다. 정말로 foreign key로 설정된 것이 맞는지 확인해보면
이렇게 foreign key로 잘 설정되어 있는 것을 알 수 있다.
이번엔 Professors 테이블과 Courses 테이블에 1:M 관계를 설정해보자. 방금 전과 똑같이 각 파일의 associate 함수 부분을 바꾸면 된다. 이렇게 코드를 바꾸자.
// professor.js
~~
Professor.hasMany(models.Course);
~~
// course.js
~~
Course.belongsTo(models.Professor);
~~
그리고 코드를 실행하면
node app.js
이렇게 ProfessorId라고 하는 foreign key가 자동으로 생성된 것을 알 수 있다.
마지막으로 Students 테이블과 Courses 테이블에 M:N 관계를 설정해보겠다. 주의할 점은 앞서 살펴본 1:1, 1:M 관계와 달리 M:N 관계는 단지 두 테이블 중 한 쪽 테이블에 foreign key를 두는 방법으로 관계를 설정할 수 없다. 대신 둘의 관계를 담고 있는 새로운 별도 테이블(보통 junction table이라고 한다)을 만들고 junction table의 두 컬럼이 각각 Students 테이블을 참조하는 foreign key, Courses 테이블을 참조하는 foreign key를 갖는다. 한번 해보겠다.
// Student.js
Student.hasOne(models.ScholarshipAccount);
Student.belongsToMany(models.Course, { through: 'Grade' });
// Course.js
Course.belongsTo(models.Professor);
Course.belongsToMany(models.Student, { through: 'Grade' });
코드를 실행하면
이렇게 Grade라는 테이블이 자동으로 생기고, CourseId, StudentId라고 하는 foreign key가 자동으로 생성되어 있는 것을 알 수 있다. (createdAt, updatedAt 컬럼은 이전에 배운 것처럼 테이블이 생성될 때 timestamps 옵션의 기본값이 true이기 때문에 생긴 것 뿐이다)
그런데 M:N 관계를 설정하는 것은 어떤 의미있는 정보를 추가로 함께 저장하고 싶기 때문이다. 우리는 각 학생이 각 수업에 대해 맞은 점수를 저장해보자. 이렇게 자동으로 생기게 하지 말고, 이미 존재하는 테이블을 junction table로 활용하는 것도 가능하다. 일단 Grade라는 모델을 직접 만들어보자. 일단 다음 명령을 실행하면
npx sequelize model:generate --name Grade --attributes score:integer
// Grade.js
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Grade extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
};
Grade.init({
score: DataTypes.INTEGER
}, {
sequelize,
modelName: 'Grade',
});
return Grade;
};
Grade 모델이 잘 생성됩니다. 그리고 이제 각 코드에서 문자열 'Grade' 대신 실제로 이미 존재하는 모델을 나타내기 위해 model.Grade를 넣어주겠습니다.
// Student.js
Student.hasOne(models.ScholarshipAccount);
Student.belongsToMany(models.Course, { through: models.Grade });
// Course.js
Course.belongsTo(models.Professor);
Course.belongsToMany(models.Student, { through: models.Grade });
실행하면(혹시 문제가 생기면 Workbench에서 직접 테이블들을 모두 삭제하고 실행하세요)
score 컬럼이 추가된 Grades 테이블을 볼 수 있습니다.
자, 이때까지 1:1, 1:M, M:N 관계에 대해서 배워보았는데요. 각 경우에 어떤 메소드 쌍(pair)을 사용해야 하는지 잘 이해해둡시다.
참고로 관계를 설정할 때 ON DELETE, ON UPDATE 옵션을 설정하는 것도 가능한데요. 예를 들어,
Student.hasOne(models.ScholarshipAccount, {
onDelete: 'RESTRICT',
onUpdate: 'RESTRICT'
});
ScholarshipAccount.belongsTo(Student);
이런 식으로 설정할 수 있습니다. 줄 수 있는 옵션에는 RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL이 있으니 각자가 원하는 것을 선택하면 됩니다. 주지 않으면 각 메소드의 기본 옵션들이 적용됩니다.