sequelize belongsToMany vs hasMany
22 Nov 2018 | sequelize associationssequelize belongsTo vs hasOne 포스트에서 1대1 관계에 대해서 알아보았다. 이번에는 hasMany와 belongsToMany가 어떤 차이점이 있고 언제 쓰이는지에 대해서 알아보았다. 물론 이것 역시 공식문서를 번역한 것이나 다름없다.
One-To-Many associations (hasMany)
hasMany의 경우 하나의 source 모델을 여러개의 Target 모델과 연결시킨다. 그리고 Target의 경우 단 하나의 soucce모델을 참조 할 수 있게 된다. 예를 들자면 여러개의 City(도시)를 가지는 Country(국가)를 들 수 있다. 각 모델을 생성하기 위해 간단히 아래와 같이 define했다고 가정하자.
const City = sequelize.define('city', { countryCode: Sequelize.STRING });
const Country = sequelize.define('country', { isoCode: Sequelize.STRING });
Country는 여러개의 City를 가지므로 Country hasMany City는 자연스럽다. 반대로 City는 단 하나의 Country만을 가지는 것이 자연스러운 그런 관계이다. 따라서 관계를 아래와 같이 설정할 수 있다.
Country.hasMany(City, {as : "Cities"});
위와 같이 설정하면 Country는 두가지 메서드를 가지게 된다. getCities와 setCities이다. Country가 여러 cities들을 가져오는 것은 자연스러운 일이다. 한편 반대로 City는 한개의 Country를 가지므로, City모델에는 Country를 참조하는 countryId 또는 country_id라는 attribute가 생긴다.
꼭 country_id를 가지고 관계를 설정하지 않아도 된다. default는 id지만 sourceKey 옵션을 통해 isoCode를 가지고 관계를 설정하게 할 수도 있다.
// Here we can connect countries and cities base on country code
Country.hasMany(City, {foreignKey: 'countryCode', sourceKey: 'isoCode'});
City.belongsTo(Country, {foreignKey: 'countryCode', targetKey: 'isoCode'});
Belongs-To-Many associations (belongsToMany)
n:m 관계 설정시 사용한다.
한 User가 여러개의 Project에 참여할 수 있고, 또한 하나의 Project에 여러명의 User들이 참여하는 상황이라면 n:m 이라고 할 수 있다. 그러나 이런 관계는 데이터베이스에서 직접 구현 할 수 없다. 대신, 이러한 관계를 두 개의 일대다 관계로 분리해야한다. 즉, 또 다른 하나의 table, 즉 연결 테이블이라 불리는 또 다른 모델이 필요하다(예를 들어 UserProject 테이블). 코드를 보자면 아래와 같다.
Project.belongsToMany(User, {through: 'UserProject'});
User.belongsToMany(Project, {through: 'UserProject'});
이렇게 관계를 설정해 준다면, UserProject라는 테이블이 하나 자동으로 생성되고, 이 테이블(모델)에는 projectId와 userId가 존재하게 된다. 즉, 관계를 중계해주는 하나의 테이블(모델)이 생성되는 것이다. 이때 테이블의 이름을 정하는 through 속성은 필수적이다.
이렇게 설정할 경우 좋은 장점은, UserProject를 생성했지만 Project와 User테이블 각각에 메서드가 추가된다는 점이다. 예를 들어 Project모델에 getUsers, setUsers, addUser, addUsers 등의 메서드가 추가되고, User모델에 getProjects, setProjects, addProject, addProjects 등의 메서드가 추가된다.
또한 as옵션, foreignKey옵션을 사용할 수 있다.
User.belongsToMany(Project, { as: 'Tasks', through: 'worker_tasks', foreignKey: 'userId' })
Project.belongsToMany(User, { as: 'Workers', through: 'worker_tasks', foreignKey: 'projectId' })
위의 예시처럼, User를 Workers라는 별칭으로 지정할 수 있고, worker_tasks라는 join table에 userId라는 이름으로 attributes를 직접 지정할 수 있다. 이렇게 하면 Project 모델에서 getWorkers()나 addWorkers()처럼 사용할 수 있다. 마찬가지로 Project를 Tasks라는 이름으로 사용할 수 있게 된다.
그러나 이렇게만 한다면 worker_tasks 테이블, UserProject 테이블에는 userId와 projectId라는 속성밖에 없다. 그래서 UserProject 테이블에 추가적인 attributes를 추가할 수 있는 방법이 있다.
const User = sequelize.define('user', {})
const Project = sequelize.define('project', {})
const UserProjects = sequelize.define('userProjects', {
status: DataTypes.STRING
})
User.belongsToMany(Project, { through: UserProjects })
Project.belongsToMany(User, { through: UserProjects })
User에 새로운 Project를 추가하고 싶은 경우, options.through를 setter에 추가한다.
user.addProject(project, { through: { status: 'started' }})
위와 같은 n:m관계에서 Query문을 날릴 땐, through와 구체적인 attributes를 명시해줘야 한다.
User.findAll({
include: [{
model: Project,
through: {
attributes: ['createdAt', 'startedAt', 'finishedAt'],
where: {completed: true}
}
}]
});