관계

  • 이 글은 7. Associations를 번역한 글입니다.
  • 아직 입문자이다보니 오역이 있을 수 있습니다. 양해 부탁드립니다.
  • 매끄러운 번역을 위하여 의역을 한 경우가 있습니다. 원문의 뜻을 최대한 해치지 않도록 노력했으니 안심하셔도 됩니다.
  • 영어 단어가 자연스러운 경우 원문 그대로의 영단어를 적었습니다.
  • 저의 보충 설명은 인용문에 적었습니다.

시작하기 전에

  • 이 글은 공식 도큐먼트의 7번째 챕터입니다. 너무 길어서 상/하로 나눴습니다.
  • 원문에서는 예시 코드를 제공하지만, 코드만 제공할 뿐 CRUD를 위한 데이터셋 세팅, 실습 환경에 대한 정보가 없고, 실행 결과를 확인할 수 있는 방법도 알려주지 않습니다. 그래서 간단하게나마 실습해볼 수 있도록 세팅하는 방법을 이전 글에서 소개하였으니 필요에 따라 활용해주시면 감사드리겠습니다.

이번 장에서는 Sequelize에서 제공하는 다양한 연결 관계에 대하여 알아봅니다. 예를 들어 User.hasOne(Project)와 같은 메서드를 호출하면, 여기서 User 모델(함수를 호출한 모델)이 source 이고 Project 모델(함수에 인자로 들어가는 모델)이 target 입니다.

연결 관계를 정의하는 메서드 기준으로 왼쪽, 즉 함수를 호출하는 모델 객체가 source, 메서드 안에 인자로 전달되는 모델 객체가 target 입니다. 어떤 메서드를 사용하더라도 기본적으로 적용됩니다.

1:1 관계

일대일 관계는 하나의 외래 키로 연결된 두 모델 간의 연결 관계입니다.

BelongsTo

BelongsTo 관계는 일대일 관계의 외래키가 source 모델 에 존재하는 연결 관계입니다.
이 관계를 보여주는 간단한 예시로는, Team 의 일부분으로 Player 가 존재하면서 외래 키를 가지는 경우가 있습니다.

const Player = this.sequelize.define('player', {/* attributes */});
const Team  = this.sequelize.define('team', {/* attributes */});

Player.belongsTo(Team); // 이제 Player 모델에는 teamId 특성이 추가되며, 여기에는 Team의 주요 키가 들어있습니다.

Foreign Keys

belongsTo 관계를 위한 외래 키는 [target 모델의 이름 + target 모델의 주요 키 이름] 로 자동 생성되는 것이 기본값입니다.
기본 명명 규칙은 camelCase이지만, source 모델이 underscored: true로 설정되어있다면, 외래 키는 snake_case로 명명됩니다.

const User = this.sequelize.define('user', {/* attributes */})
const Company  = this.sequelize.define('company', {/* attributes */});

User.belongsTo(Company); // User 모델에 companyId 를 추가

const User = this.sequelize.define('user', {/* attributes */}, { underscored: true })
const Company  = this.sequelize.define('company', {
  uuid: {
    type: Sequelize.UUID,
    primaryKey: true
  }
});

User.belongsTo(Company); // User 모델에 company_uuid 를 추가

Sequelize에서 외래 키는 모델 정의할 때에 만들어지는 것이 아니라, 두 모델의 연결 관계를 정의 할 때에, 연결 관계에 따라 Sequelize가 적절한 모델에 컬럼을 자동으로 만들어줍니다. 이 작업과 관련된 설정을 Sequelize에게 맡길 수도 있고, 아래에 후술하겠지만 몇몇 옵션을 주어서 사용자가 제어할 수도 있습니다.

외래 키 자동 생성의 대원칙은 [target 모델의 이름 + target 모델의 주요 키 이름]입니다. 물론, 이제 나올 as 때문에 조금 헷갈립니다.

as가 사용되면 여기서 정의된 이름이 target 모델의 이름으로 사용됩니다.

const User = this.sequelize.define('user', {/* attributes */})
const UserRole  = this.sequelize.define('userRole', {/* attributes */});

User.belongsTo(UserRole, { as: 'role' }); // userRoleId 가 아니라 roleId 가 추가된다

외래 키 설정의 관한 부분은 foreignKey 옵션으로 항상 대체할 수 있습니다. foreignKey 옵션이 사용되면 Sequelize는 무조건 이를 기반으로 외래 키를 설정합니다.

foreignKey 옵션을 주면, as 옵션을 모두 무시하고 foreignKey 옵션의 값에 따라 외래 키 컬럼을 이름짓습니다.

const User = this.sequelize.define('user', {/* attributes */})
const Company  = this.sequelize.define('company', {/* attributes */});

User.belongsTo(Company, {foreignKey: 'fk_company'}); // User 모델에 fk_company 특성을 추가한다

Target keys

Target key는 target 모델에 있는 컬럼으로, source 모델에 있는 외래 키 컬럼은 targetKey 컬럼을 가리키게 됩니다. belongsTo 관계에서 targetKey는 기본값으로 target 모델의 주요 키를 가리킵니다. 이에 대하여 별도의 컬럼을 지정하려면 targetKey 옵션을 사용합니다.

const User = sequelize.define('user', {/* attributes */})
const Company  = sequelize.define('company', {
  name: Sequelize.STRING
});

User.belongsTo(Company, {foreignKey: 'fk_companyname', targetKey: 'name'});
// User 모델에 fk_companyname을 추가합니다.
// 여기서 fk_companyname이 가리키는 target 모델의 컬럼은 name 컬럼입니다.

MySQL를 사용하여 위 예제를 실행할 경우 오류가 발생합니다. 외래 키가 참조하는 다른 테이블의 컬럼은 반드시 주요 키 이어야 하기 때문입니다. 다른 DB는 제가 확인을 해보지 않아서 잘 모르겠습니다만, MySQL을 사용하시는 분들은 당황하지 마시기 바랍니다. 앞으로도 이런 경우가 종종 발생합니다.
참고: MySQL Foreign key 사용 시 주의 사항

Unhandled rejection SequelizeDatabaseError: Failed to add the foreign key constraint. Missing index for constraint 'users_ibfk_1' in the referenced table 'companies'

HasOne

HasOne 관계는 일대일 관계를 위한 외래 키가 target 모델 에 존재하는 관계입니다.

const User = sequelize.define('user', {/* ... */})
const Project = sequelize.define('project', {/* ... */})

// 일방향적 관계
Project.hasOne(User)

/*
  이 예제에서 hasOne()은 User 모델에 projectId을 자동으로 추가합니다.
  또한, define할 때 사용된 첫번째 인자(테이블 이름)를 바탕으로 Project.prototype에 getUser와 setUser 메서드가 추가됩니다.
  underscore 스타일 옵션을 설정했다면, 추가된 컬럼은 projectId가 아니라, project_id로 명명됩니다.

  이제 외래 키는 users 테이블에 존재합니다.

  이미 존재하던 테이블에 대하여 외래 키를 추가하고 싶다면, 그것도 가능합니다. (하단 참조)
*/

Project.hasOne(User, { foreignKey: 'initiator_id' })

MySQL의 경우, 이미 정의가 완료되어 DB에 실제로 생성된 테이블에는 외래 키 설정을 통한 새로운 컬럼의 추가 등을 할 수 없습니다. 해당 테이블을 제거한 뒤 다시 Sequelize 코드를 실행해야 제대로 적용됩니다.

/*
  Sequelize는 접근자 메서드를 만들 때 모델의 이름(define할 때 사용된 첫번째 인자)을 활용하므로, hasOne을 위한 특별한 옵션값을 전달하는 것도 가능합니다. (하단 참조)
*/

Project.hasOne(User, { as: 'Initiator' })
// 이제 Project.getInitiater와 Project.setInitiator를 사용할 수 있습니다.

as 옵션은 연결 관계에서 사용할 별칭을 설정합니다. 이 별칭은 Sequelize가 제공하는 JOIN 관련 유틸리티 메서드의 이름으로 활용됩니다. 이 메서드를 사용하면 연결 관계를 활용하여 데이터에 접근할 수 있습니다. 이 메서드들은 source 모델 객체의 prototype 에 생성됩니다. console.log()로 확인해보세요.

위의 예시의 경우, Project 모델의 prototype에 3개 메서드(get-, set-, create-)가 추가됩니다.
위의 예시에서 project.setInitiator(user)를 실행하게 되면, 이것은 project와 user 간에 형성된 연결 관계에서 user 인스턴스를 Initiator로 설정한다 로 이해하면 됩니다. 그러면 project 인스턴스의 iduser 인스턴스의 InitiatorId(또는 initiator_id)에 대입됩니다.

직관적으로 바로 납득이 되지 않는 상황입니다. project에 대한 initiator는 user인데, user 인스턴스에 들어있는 InitiatorId는 project의 id를 가집니다. (??) 이것이 정 헷갈리면, foreignKey 설정을 사용해서 다른 이름으로 설정해주면 좀 낫겠죠? projectId 라든가...

더 알아보기: as 옵션과 has-, belongsTo 관계

더 나은 이해를 위해서 상황을 반대로 뒤집어보죠.

User.belongsTo(Project, { as: 'Initiator' })
user.setInitiator(project)

위의 코드에서는 User 모델에 InitiatorId 특성이 생성되고, 거기에 Project 인스턴스의 id가 들어갑니다. 그리고 user 인스턴스의 Initiator는 project 인스턴스입니다. 아직도 문맥이 이상하죠? 유틸리티 메서드도 관계가 뒤집혀져 있습니다.

그럼 이건 어떨까요?

Project.belongsTo(User, { as: 'Initiator' })
project.setInitiator(user)

여러 시행착오 끝에, 가장 자연스러운 상황이 되었네요. userproject에 대한 Initiator로서 존재하게 되었습니다. project 인스턴스가 가진 InitiatorId에는 user 인스턴스의 id가 들어가게 됩니다. 마지막으로 project의 유틸리티 메서드를 사용하여 어색하지 않은 방법으로 관계를 설정해줄 수 있게 되었습니다.

결론

  • as 별칭은 target 모델을 가리킨다.
  • has- 를 사용할 경우, 접근 유틸리티 메서드의 결과가 target 인스턴스에 반영되고, target 모델의 외래 키 이름이 별칭을 따른다.
  • belongsTo-를 사용할 경우, 접근 유틸리티 메서드의 결과가 source 인스턴스에 반영되고, source 모델의 외래 키 이름이 별칭을 따른다.
  • as로 인하여 컬럼 이름이 문맥과 어울리지 않고 헷갈리게 지어질 수 있으므로, 반드시 foreignKey 옵션과 함께 사용하자.
  • as 옵션은 has- 와는 가급적이면 사용하지 말자.

다행스러운 점은, 아래에 후술하겠지만 as가 자동으로 컬럼 이름을 짓는 일은 일대일 관계에서만 벌어집니다. 다대다 관계에서는 참조되는 테이블의 이름을 사용하여 짓습니다. 왜 이렇게 일관적이지 않은 동작을... ㅜㅜ

// 또는, 자기 자신과 연결 관계를 맺는 것도 가능합니다.
const Person = sequelize.define('person', { /* ... */})

Person.hasOne(Person, {as: 'Father'})
// 여기서 FatherId라는 컬럼이 Person에 추가됩니다.

// 이것도 가능합니다. (하단 참조)
Person.hasOne(Person, {as: 'Father', foreignKey: 'DadId'})
// 여기서 DadId라는 컬럼이 Person에 추가됩니다.

// 두 경우 모두 아래 메서드를 사용할 수 있게 됩니다.
Person.setFather
Person.getFather

as 옵션과 foreignKey 옵션이 동시에 사용될 경우, as 옵션의 값은 유틸리티 메서드의 이름을 정하는 데에만 사용됩니다. 실제 DB 상에서 외래 키 컬럼의 이름은 foreignKey 옵션의 값이 사용됩니다.
재귀 관계의 경우, set- 메서드를 썼을 때에 어떤 인스턴스에 있는 외래 키 값을 설정하는 것인지 헷갈릴 수 있습니다. 위에서 자세하게 다뤘듯, 연결 관계를 정의할 때에 belongsTohasOne 중 어떤 것을 사용했는지에 따라 메커니즘이 달라지니, 둘 다 해보시고 그 차이를 확인해보세요.

// 테이블을 2번 Join해야 한다면, 동일 테이블에 대하여 2번 Join할 수도 있습니다.
Team.hasOne(Game, {as: 'HomeTeam', foreignKey : 'homeTeamId'});
Team.hasOne(Game, {as: 'AwayTeam', foreignKey : 'awayTeamId'});
Game.belongsTo(Team);

위의 예시에서는 Game 모델에 2개의 외래 키가 생성됩니다.
외래 키의 이름 지정 규칙에 대하여 정리해볼게요.

(1) as만 사용

  • [as에 사용된 테이블 별칭 + 참조하는 테이블의 주요 키 이름]

(2) as 유무와 상관 없이 foreignKey를 사용

  • [foreignKey의 값]

위의 예시에서는 hasOne 연결 관계를 사용했지만, 대부분의 1:1 관계에서는 BelongsTo 관계를 사용하게 될 겁니다. 왜냐하면 BelongsTo는 source 모델(함수를 호출한 모델)에 외래 키를 추가하기 때문입니다. HasOne은 반대로 target 모델에 외래 키를 추가합니다.

제가 위에서 결론내린 것과 동일한 이야기를 도큐먼트에서도 하는군요! 그런데 이걸 이렇게 아무런 설명도 없이 적어주면... ㅜㅜ

HasOne과 BelongsTo의 차이

Sequelize에서 일대일 관계는 HasOne과 BelongsTo를 사용하여 설정할 수 있습니다. 둘 다 각각에 적절한 상황이 있습니다. 예시를 통해 알아봅시다.

PlayerTeam 이라는 두 테이블을 연결하는 상황을 봅시다. 우선 모델을 정의합시다.

const Player = this.sequelize.define('player', {/* attributes */})
const Team  = this.sequelize.define('team', {/* attributes */});

Sequelize에서 두 모델을 연결할 때에는 source 모델과 target 모델의 쌍으로 지칭할 수 있습니다. 아래와 같이 말이죠.

Playersource 이고, Teamtarget 일 때:

Player.belongsTo(Team);
// 또는
Player.hasOne(Team);

Teamsource 이고, Playertarget 일 때:

Team.belongsTo(Player)
// 또는
Team.hasOne(Player)

HasOne과 BelongsTo는 관계 키를 삽입하는 모델이 서로 다릅니다. HasOne은 관계 키를 target 모델에 삽입하지만, BelongsTo는 관계 키를 source 모델에 삽입합니다.

BelongsTo와 HasOne을 사용하는 경우의 보여드리죠.

const Player = this.sequelize.define('player', {/* attributes */})
const Coach  = this.sequelize.define('coach', {/* attributes */})
const Team  = this.sequelize.define('team', {/* attributes */});

Player 모델이 자신이 속한 팀의 정보를 teamId 컬럼으로 가진다고 가정해봅시다. 또한 각 팀의 Coach 에 대한 정보는 Team 모델에서 coachId 컬럼으로 저장됩니다. 앞의 두 경우는 각각 다른 종류의 일대일 관계를 필요로 하는데, 외래 키 관계가 여러 모델에서 동시에 발생하기 때문입니다.

관계 정보가 source 모델에 존재하는 경우 belongsTo를 사용하면 됩니다. 예시에서는 PlayerteamId를 가지므로, belongsTo를 사용하는 것이 적절합니다.

Player.belongsTo(Team)  // 'teamId' 는 Player(source 모델)에 추가됩니다.

관계 정보가 target 모델에 존재하는 경우 hasOne을 사용하면 됩니다. 예시에서는 TeamcoachId를 통하여 Coach에 대한 정보를 가지므로, CoachhasOne을 사용하는 것이 적절합니다.

Coach.hasOne(Team)  // 'coachId' 는 Team(target 모델)에 추가됩니다.

1:다 관계 (hasMany)

일대다 관계는 단일 source를 복수의 target 모델에 연결합니다. 이때 target 모델은 반드시 단 하나의 source 모델에만 연결됩니다.

const User = sequelize.define('user', {/* ... */})
const Project = sequelize.define('project', {/* ... */})

// 이제 조금 복잡해졌습니다. (사용자는 이를 전혀 모르겠지만요 :))
// 우선, hasMany 관계를 정의합니다.
Project.hasMany(User, {as: 'Workers'})

이렇게 하면, User 모델에 projectId 또는 project_id 특성이 추가됩니다. 또한 Project 인스턴스는 접근 유틸리티 메서드인 getWorkerssetWorkers를 가지게 됩니다.

일대일 관계 때와는 달리, target이 복수가 되면 as 옵션이 외래 키 이름에 영향을 주지 않습니다. 유틸리티 메서드 이름에만 영향을 끼칩니다.

레코드가 별도의 컬럼을 통하여 외래 키 관계를 형성할 수도 있는데, 이 때는 sourceKey 옵션을 사용합니다.

const City = sequelize.define('city', {
  countryCode: Sequelize.STRING
});
const Country = sequelize.define('country', { isoCode: Sequelize.STRING });

// Country와 City를 국가 코드(isoCode)를 바탕으로 연결할 수 있습니다.
Country.hasMany(City, {foreignKey: 'countryCode', sourceKey: 'isoCode'});
City.belongsTo(Country, {foreignKey: 'countryCode', targetKey: 'isoCode'});

위에서 Target Key 예제에서 사용한 것과 동일한 방식입니다. 물론 이 예제는 MySQL 기반의 DB를 사용할 경우 동일한 이유에 의하여 오류를 발생시킵니다. 제대로 작동하도록 하려면, Country 모델을 정의할 때에 isoCode를 주요 키로 설정해줘야 합니다.
위의 예시에서 foreignKey에 지정해준 컬럼 이름과 모델 정의시 만든 컬럼의 이름이 겹칩니다(countryCode). 이런 경우, Sequelize는 외래 키를 위한 컬럼을 새로 만들지 않고 해당 컬럼을 외래 키 컬럼으로 지정해줍니다. 만약 foreignKey 옵션이 정의되지 않으면 countryCode와 별개의 새로운 컬럼이 생성됩니다.

지금까지 일방향 관계만 다뤘지만, 그 이상도 할 수 있습니다! 다음 섹션에서는 다:다 관계를 다룹니다.

N:M 관계

다대다 관계는 다수의 source를 다수의 target과 연결할 때에 사용합니다. 또한 다수의 target은 다수의 source와 연결할 수 있습니다.

Project.belongsToMany(User, {through: 'UserProject'});
User.belongsToMany(Project, {through: 'UserProject'});

이렇게 하면, UserProject라는 모델이 생성되며, 여기에는 서로 동등한 자격의 외래 키 projectIduserId가 생성됩니다. 컬럼이 camelcase인지 snail_case인지는 두 모델의 설정에 따릅니다.

다대다 관계에서는 각 모델이 아니라 새로 생성된 모델에 특성이 추가된다는 점에 유의하세요.

여기서 through필수 옵션 입니다. 앞서 다룬 경우에서는 Sequelize가 알아서 모든 것을 처리해줬지만, 이것이 항상 논리적으로 가장 좋은 구성 방식은 아닙니다.

또한 이렇게 하고 나면 Project 객체에는 getUsers, setUsers, addUser, addUsers가 추가되고, User 객체에는 getProjects, setProjects, addProject, addProjects가 추가됩니다.

다대다 관계가 형성되면, 단수형 메서드와 복수형 메서드가 동시에 생성됩니다.

연결 관계 내에서 모델을 부를 때 원본과 다르게 부르고 싶을 수도 있죠? as 옵션을 사용해서 usersworkers로, projectstasks로 새로운 이름을 지어줄 수도 있습니다. 또한 직접 외래 키를 지정할 수도 있습니다.

User.belongsToMany(Project, { as: 'Tasks', through: 'worker_tasks', foreignKey: 'userId' })
Project.belongsToMany(User, { as: 'Workers', through: 'worker_tasks', foreignKey: 'projectId' })

foreignKey를 사용하면 through 로 정의된 새로운 모델 내에서 source 모델 을 가리키는 외래 키 컬럼의 이름을 지정해줄 수 있습니다. otherKey를 사용하면 through 로 정의된 새로운 모델 내에서 target 모델 을 가리키는 외래 키 컬럼의 이름을 지정해줄 수 있습니다.

// 아래 코드와 위의 코드는 동일한 결과를 만들어낸다
User.belongsToMany(Project, { as: 'Tasks', through: 'worker_tasks', foreignKey: 'userId', otherKey: 'projectId'})

다대다 관계 정의에서는 관계 설정하는 메서드를 호출하는 모델through 모델 을 중심으로 생각하면 됩니다. foreignKey가 가리키는 모델은 전자입니다. otherKey는 그 반대편의 모델(위의 코드에서는 Project a.k.a. Tasks)을 가리키는 것이겠죠?

belongsToMany로 재귀 관계를 정의할 수도 있습니다.

Person.belongsToMany(Person, { as: 'Children', through: 'PersonChildren' })
// PersonChildren 모델이 생성되고, Person 레코드의 id 2개로 이루어진 데이터를 갖게 된다

자기 참조를 하게 되면 through 테이블에 생기는 2개 외래 키를 구분할 수 있어야겠죠? 이때 as 옵션이 사용됩니다. 한 Person 인스턴스는 personId, Children 별칭의 인스턴스는 ChildId로 구분됩니다.

조인 테이블(through를 통하여 생성된 테이블)에 별도의 컬럼을 추가하고 싶다면, 관계를 정의하기 앞서서 조인 테이블을 먼저 정의하면 된다. 그 후 해당 테이블을 belongsToMany 관계 형성을 위하여 사용하면 된다.

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 })

through 키의 값이 '문자열'이 아니라 변수인 것에 주목해주세요.

user 인스턴스에 새로운 project 인스턴스를 추가하면서 status 값을 설정하려면, setter 함수(여기서는 addProject)에 option.through라는 추가적인 값을 전달하면 된다. 여기서 option.throughthrough 테이블의 컬럼을 위한 값을 제공해준다.

// 각 테이블에서 user 레코드와 project 레코드를 찾고, 'started' status를 중심으로 연결해준다
user.addProject(project, { through: { status: 'started' }})

위의 코드를 실행하면, JOIN 연산에 기반하여 userProjects 테이블에 항목을 추가하는데, 이때 status 특성의 값도 'started'로 함께 변경해줍니다.

위의 코드는 기본값으로 projectIduserIduserProjects 테이블에 추가하되, 만약 이미 동일한 주요 키로 이루어진 레코드가 존재한다면 이를 덮어쓴다. UserProjects 테이블의 항목은 원본 테이블(이 예시에서는 UserProject)의 주요 키 쌍으로서 구분되므로, 그 밖의 주요 키가 또 존재할 필요가 없기 때문이다. UserProjects 모델에 대하여 주요 키를 별도로 정의할 수도 있다.

const UserProjects = sequelize.define('userProjects', {
  id: {
    type: Sequelize.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  status: DataTypes.STRING
})

원래는 원본 테이블의 두 주요 키의 조합이 userProjects의 주요 키였지만, 이제는 id가 별도로 정의되었으므로 두 주요 키 조합은 외래 키의 역할을 갖게 됩니다. 이제는 두 주요 키 조합의 값이 새로 추가되더라도 덮어쓰기 되는 일 없이 새로운 항목이 추가될 겁니다.

belongsToMany 관계에서는 through 테이블 에 기반한 쿼리로 특정 컬럼값만을 취할 수도 있습니다. 아래의 예에서는 through와 함께 findAll를 사용합니다.

User.findAll({
  include: [{
    model: Project,
    through: {
      attributes: ['createdAt', 'startedAt', 'finishedAt'],
      where: {completed: true}
    }
  }]
});

N:M 관계인 User와 Project, 그리고 그 가운데 through 테이블인 userProjects까지 3개의 테이블에 대한 쿼리입니다. 여러 가지 실습을 해볼 수 있도록 코드를 재작성해봤습니다.

const User = sequelize.define('user',{})
const Project = sequelize.define('project')
User.belongsToMany(Project, { through: 'userProjects'})
Project.belongsToMany(User, { through: 'userProjects'})

sequelize.sync()
  .then(() => {
    return User.findAll({
      include: [{
        model: Project,
        through: {
          attributes: ['createdAt']
        }}]})})
  .then((users) => {
    users.map((user) => {
      console.log(JSON.stringify(user))
      console.log()
    })})

/*
{
  "id":2,
  "createdAt":"2019-01-06T08:52:23.000Z",
  "updatedAt":"2019-01-06T08:52:23.000Z",
  "projects": [
    {
      "id":1,
      "createdAt":"2019-01-06T08:52:32.000Z",
      "updatedAt":"2019-01-06T08:52:32.000Z",
      "userProjects": {
        "createdAt":"2019-01-06T08:54:20.000Z"
      }
    }
  ]
}
*/

through 속성 내에서 정의된 내용은 through 테이블의 데이터에만 영향을 주었다는 점을 주목해주세요. 즉, attributes 속성에는 'createdAt' 만이 포함되었는데, JSON 결과를 보면 다른 데이터와는 달리 "userProjects" 에 대한 데이터는 "createdAt"만이 포함되어있다는 것을 알 수 있습니다. 그 외에도 옵션값을 수정하며 다양하게 실습하며 개념을 확인해보세요.

// 또다른 예시 쿼리
User.findAll({
  attributes: ['id'],
  where: {
    id: 2
  },
  include: [{
    model: Project,
    attributes: ['updatedAt'],
    through: {
      attributes: ['createdAt']
    }
  }]
})

through 테이블에 주요 키가 존재하지 않을 경우, 고유 키(unique key)가 자동으로 생성됩니다. 고유 키의 이름은 uniqueKey 옵션으로 지정해줄 수 있습니다.

Project.belongsToMany(User, { through: UserProjects, uniqueKey: 'my_custom_unique' })

코멘트

며칠동안 저를 고민에 빠트렸던 as와 연결 관계에 대한 수수께끼가 점점 풀려가고 있습니다! 이제는 고구마먹은 것마냥 답답한 심정 없이 Sequelize를 쓸 수 있게 되었...겠죠? 대체 왜 이런 것들을 도큐먼트는 잘 알려주지 않는 걸까요... 이렇게 스스로 깨우치라는 개발자들의 깊은 통찰인 걸까요? 😂😂

다음 편은 7. Association (하)로 이어집니다.