Web/[JS] BackEnd

Sequelize Join

ihl 2021. 3. 27. 16:41

Database Schema

  프로젝트에서 테이블들을 조인해야하는 2가지 상황이 발생하였다. 이를 내가 어떻게 해결하였는지 기록으로 남겨본다.

 

1. Users와 Lists

  가장 먼저 Users테이블과 Lists 테이블을 조인해야 했다. 그 이유는 사용자가 마이 페이지에서 자신의 개인정보 뿐만 아니라 나의리스트 이름목록까지 바로 보여지도록 구조를 짰기 때문이다.

 

  Users와 Lists는 1대N 관계이다. 하나의 유저가 여러개의 노래 목록을 가질 수 있기 때문이다. 이를 위해서 먼저 Lists 테이블에 UserId라는 Foreign Key를 만들자.

 

//migration/20210317040801-add_lists_fk.js
'use strict';
module.exports = {
  up: async (queryInterface, Sequelize) => {
    // field 추가
    await queryInterface.addColumn('Lists', 'UserId', Sequelize.INTEGER);

    // foreign key 연결
    await queryInterface.addConstraint('Lists', {
      fields: ['UserId'],
      type: 'foreign key',
      name: 'user_list_fk',
      references: {
        table: 'Users',
        field: 'id'
      },
      onDelete: 'cascade',
      onUpdate: 'cascade'
    });
  },

  down: async (queryInterface, Sequelize) => {
    await queryInterface.removeConstraint('Lists', 'user_list_fk');
    await queryInterface.removeColumn('Lists', 'UserId');
  }
};

  Database에 변경사항이 생긴 것이므로 migration 파일을 생성해주고 안에 다음과 같이 Foreign Key를 설정해준다. 이후 migration 파일을 up 상태로 만들면 진짜 데이터데이터베이스에 Foreign Key가 생긴 것을 확인할 수 있다.

 

//models/index.js

//생략...
const {User, List, Song, List_Song} = sequelize.models;
List.belongsTo(User);
User.hasMany(List);

  나는 데이터베이스에 직접 쿼리를 날려 데이터를 조작하는 것이 아니라 Sequelize Model을 이용하여 조작할 것이므로 Sequelize Model에 User와 List가 관계가 있다는 것을 알려주어야 한다. 이는 Sequelize init할 때 models/ 폴더에 자동으로 생성되는 index.js 파일에 위와 같은 코드를 밑에 추가하면 된다.

 

const { User: USERModel, List: LISTModel, Song: SONGModel } = require('./models');

//! UserId로 Lists 이름 및 사용자 정보 가져오기 예시
USERModel.findAll({
  where: { 
    id: 2,
  },
  include: [
    {
      model: LISTModel,
      required: true,
      attributes: ['name'],
    }
  ]
})
  .then((result) => {
    console.log(result[0].Lists);
  });

  이제 Join을 해보자. 위 코드는 UserId로 사용자 정보 및 리스트의 이름을 가져오는 코드이다. UserModel이 기준이므로 해당 모델에 findAll 함수호출한다. 이 때 함수의 인자로 객체를 받게되는데 객체의 where 속성은 Join의 조건이고 include는 Join테이블이다. Join테이블 객체에는 Join 테이블에 해당하는 모델과 어떤 조인을 수행할 것인지, 조인해서 가져올 컬럼이 무엇인지를 명시한다.

 

  Join 테이블 객체의 required는 조인의 종류로 true로 주면 innerJoin을 수행한다. attributes는 Lists와 조인하여 가져올 Lists의 컬럼을 의미한다.

 

2. Lists와 Song

  이번엔 Lists와 Song 테이블을 조인해보자. Lists와 Song은 서로 M 대 N 관계를 갖고 있다. 따라서 List_Song이라는 새로운 테이블을 만들어 이 테이블에 리스트와 노래의 관계데이터를 저장하는 구조로 만들어야 한다. 

 

//migrations/20210317041522-create_list_song_association.js
'use strict';
module.exports = {
  up: async (queryInterface, Sequelize) => {
    queryInterface.createTable('List_Song', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      ListId: {
        type: Sequelize.INTEGER,
        references: {model: 'Lists', key: 'id'}
      },
      SongId: {
        type: Sequelize.INTEGER,
        references: {model: 'Songs', key: 'id'}
      }
    });
  },

  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('List_Song');
  }
};

  List_Song을 위해 위와 같은 migration 파일을 생성해주었다. List_Song은 리스트와 노래를 연결해 주는 테이블이므로 ListId와 SongId를 Foreign Key로 갖는다.

 

//models/index.js

const {User, List, Song, List_Song} = sequelize.models;

List.belongsToMany(Song, {through: 'List_Song', foreignKey: 'ListId', as: 'Song'});
Song.belongsToMany(List, {through: 'List_Song', foreignKey: 'SongId', as: 'List'});

  이제 Sequelize Model에 Song과 Lists가 관계가 있다는 것을 알려주자.

 

const {User, List, Song, List_Song} = sequelize.models;

List.belongsToMany(Song, {through: 'List_Song', foreignKey: 'ListId', as: 'Song'});
Song.belongsToMany(List, {through: 'List_Song', foreignKey: 'SongId', as: 'List'});

List_Song.belongsTo(List, {
  foreignKey: 'ListId', 
  as: 'List' 
});

List_Song.belongsTo(Song, {
  foreignKey: 'SongId', 
  as: 'Song' 
});

  나는 Song과 Lists의 M 대 N 관계 데이터를 관리하기 위해 List_Song이라는 테이블도 추가적으로 사용하고 있으므로 이 또한 Sequelize Model에 명시해주어야 한다.

 

//models/list_song.js
'use strict';
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class List_Song extends Model {
    static associate(models) {
      // define association here
    }
  }
  List_Song.init({
  }, {
    sequelize,
    modelName: 'List_Song',
    createdAt: false,
    updatedAt: false,
    tableName: 'List_Song'
  });
  return List_Song;
};

  Sequelize로 테이블을 생성할 경우 createAt과 updateAt이라는 속성이 자동으로 생성된다. 그러나 List_Song이라는 테이블에는 createAt과 updateAt이라는 속성이 필요 없기 때문에 위와 List_Song모델 파일에서 위와 같은 코드를 추가해준다.

 

const { User: USERModel, List: LISTModel, Song: SONGModel } = require('./models');
//! ListId로 소속된 노래 정보 가져오기 예시
LISTModel.findAll({
  where: {
    id: 1
  },
  include: [{
    model: SONGModel,
    required: true,
    attributes: ['songNum', 'title'],
    through: {
      attributes: ['ListId', 'SongId']
    }
  }]
})
  .then((result) => {
    console.log(result[0].Songs);
  });

  이제 Join을 해보자. Users와 Lists 조인 때와 비슷하지만 through라는 속성이 새로 추가된 것을 볼 수 있다. 해당 속성은 List_Song 테이블을 의미하는 것으로 해당 테이블에서 어떤 속성들을 이용하여 List테이블과 Song테이블을 연결할 것인지 명시해주면 된다.