[시퀄라이즈] DB 관계 설정하기

양찌·2021년 12월 28일
1

ProjectDiary

목록 보기
5/5
post-thumbnail

시작 전에..



코드스테이츠 팀 프로젝트에서 백엔드를 담당하였을 때, 다른 백엔드 팀원이 DB관계 설정하는데에 어려움을 겪었었다. 그때 나도 같이 해봤는데 반나절동안 문제를 풀지 못한 기억이 있다. 결국에 담당 백엔드 팀원이 문제를 풀었었다.

그래서 이번 개인 프로젝트를 진행할 때도 DB관계 설정하는데 꾀 오랜 시간이 걸릴 것이라고 예상하였다.

하지만 예상외로 한 2시간??정도 했더니 관계설정에 성공하였고, 2시간 더 추가해서 코드를 정리하면서 여러 테이블의 관계 설정을 할 수 있었다.

지난번에 DB 스키마를 작성하였다. 그리고 오늘은 DB 관계 설정을 맞치고 그 과정을 기록으로 남기려 한다.



시퀄라이즈 마이그레이션(migration) 기초

시퀄라이즈 프레임워크를 사용하여 서버 DB에 스키마를 심는데는 별로 어려움이 없다.
시퀄라이즈 공식홈페이지에 있는 Migrations내용을 처음부터 Running Migrations 이 부분까지 천천히 따라하기만 하면 별문제 없이 된다.

오늘의 핵심은 시퀄라이즈 DB 관계를 설정하는 것임으로 마이그레이션 내용은 생략하겠다.






관계(Associations)

DB관계를 설정하기 위해서는 이론적인 관계개념 대해서 알고 있어야 실제로 코드로 관계 설정하기가 쉽다.

맨처음에 나는 공부는 나중에 하자고 생각하고 관계설정을 했다가 헷갈려서 다시 공부를 하고 코드를 작성하여 시간을 더 소비했다.

참고 사이트

How to define Sequelize associations using migrations

설명을 시작하기 전에 이해를 돕자면,
source는 연결을 시작하는 위치이고, target은 연결을 받는 위치라고 생각하면 좋을 것 같다.


belongsTo

  • add 외래키

  • 단일 연결 믹스인(Mixin) to the source
    mixin은 다른 클래스의 부모클래스가 되지 않으면서 다른 클래스에서 사용할 수 있는 메서드를 포함하는 클래스이다.

    order : customer = source : target

👉 주문은 고객에 속해 있다.
(customer belongsTo customer.)
👉 외래키는 source에 더해진다.



hasOne

  • source는 유일무이하다.
  • 외래키는 target에 더해진다.

    payment : order = = source : target


👉 한 주문은 단 한개의 결제를 갖는다.
(order hasOne payment.)
👉 주문 테이블(the target)에 결제 아이디가 있다.



hasMany

  • 1 : N 관계에 사용한다.
  • 외래키는 target에 더해진다.

order : products = source : target

👉 주문은 많은 상품을 갖는다.
(oreder hasMany products.)



belongsToMany

  • N : M 관계에 사용한다. (조인 테이블)

products : tags

👉 상품은 많은 태그를 가진다.
(products belongsToMany tags.)
👉 태그도 많은 상품을 가진다.
(tags belongsToMany produts.)






코드 작성

내가 참고한 사이트에 따르면, 코드 작성 순서는 다음과 같다.

1. 각 테이블의 속성을 작성해 마이그레이션 db:migrate을 한다.(외래키 필드와 조인 테이블을 제외)

2.외래키 설정을 한다.(다른 마이그레이션 파일 사용)

1) models 폴더 안에 있는 파일에서 관계설정
2) migrations 폴더 안에서 2-1을 토대로 up, down 파일만들어 주기.

3. N:M관계인 조인테이블 설정을 한다.(다른 마이그레이션 파일 사용)


외래키 설정과 조인테이블 설정 migration 파일을 따로 만들어주는 이유?
외래키, 조인테이블 설정은 migration에 있는 up, down 함수에 의하며 처음 migration된 순서에 영향을 받는다.
예를 들어, db:migrate 해줄때 내가 user, region, class순으로 migrate 해주었다면 migration 폴더 안에도 그 순서대로 파일이 생성되고 실행될 때도 만든 순서에 의해 코드가 읽힌다. region파일 안에서 up, down 함수에서 class 값을 불러오지 못하여 생성 오류가 발생하게 된다.
모델이 생성된 순서에 의하여 migration이 의존적이지 않게 하기 위해서 테이블 생성 후 외래키, 조인 테이블 키 파일 생성을 따로 한다.

필자 같은 경우도 처음에 순서와 상관이 없는 줄 알고 외래키 설정을 막했더니 다음과 같은 오류가 났었다.

ERROR: Cannot add foreign key constraint




파일 생성 순서에 유의하여 관계 설정을 시작해보자!.

1. db:migrate

1) 시퀄라이즈 공식홈페이지에서 알려주는 것처럼 config, migrations, models폴더를 다 만든다.

그 후 아래 커멘드로

npx sequelize-cli model:generate --name User --attributes  login_id:integer,password:string,name:string

첫모델을 생성한다.
그러면 models폴더에 user.js 파일과
migration 폴더에 202324234xx-create-user.js파일이 생성된다.

2) 위 과정대로 다른 모델도 생성한다.
(단, 외래키를 갖고 있는 필드와 조인 테이블은 작성하지 않는다.)



2. 외래키 설정

1) models 폴더

👉 static associate(models) { ... } 안에서 belongsto, hasmany, hasone, belongstomany 등의 관계 설정을 해준다.

예를 들어 클래스 테이블에는 teacher_id와 region_id가 외래키로 있다.

  • 한 클래스는 오직 한 선생님만에 의해 개설됨.따라서 teacher_id는 belongsTo user_id이다.
    belongsTo는 source에 외래키가 생성되기 때문에 models > class.js 에서 관계를 설정해준다.
  • 한 지역에서 여러 클래스가 열린다. 따라서 region hasMany class이다.
    hasMany는 target 테이블에 외래키가 생성되기 때문에 models > regin.js에서 관계를 설정해준다.

코드는 다음과 같다.
models > class.js 파일

'use strict';
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class Class extends Model {

    static associate(models) {
      // 🎈이곳에서 관계를 설정합니다
      this.belongsTo(models.User); 
    }
  };
  
  // 아래 코드는 db:migrate 커맨드에서 입력한 속성과 타입입니다.
  Class.init({
    name: DataTypes.STRING,
    price: DataTypes.INTEGER,
    type: DataTypes.STRING,
    score: DataTypes.DECIMAL,
    discount: DataTypes.INTEGER,
    img_url: DataTypes.STRING,
    contents: DataTypes.STRING,
  }, {
    sequelize,
    modelName: 'Class',
  });
  return Class;
};

models > region.js 파일

'use strict';
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class Region extends Model {

    static associate(models) {
      // 🎈 이 곳에서 관계를 설정합니다
      this.hasMany(models.Class);
    }
  };
  
  // 아래 코드는 db:migrate 커맨드에서 입력한 속성과 타입입니다.
  Region.init({
    name: DataTypes.STRING
  }, {
    sequelize,
    modelName: 'Region',
  });
  return Region;
};



2) migrations 폴더

👉 up, down 함수 작성.
up 함수는 migration을 실행할 때 실행되고, down은 취소할 때 실행되는 함수라고 이해했다.

👉 주의해야할 점은 migrations 폴더 안에서 참고하는 테이블 명을 작성할 때는 복수형으로 작성한다.


아래의 커맨드를 사용하여 관계설정을 위한 새로운 마이그레이션 파일을 만든다.
sequelize migration:generate --name add-associations


그러면 20211xxx-add-associations.js 같은 파일명으로 migrations에 새 파일이 생성된다.

migrations > 20211xxx-add-associations.js 파일

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    // Class Table
    // 🎈 클래스 테이블에 teacherId 필드를 생성합니다.
    await queryInterface.addColumn(
      'Classes', 
      'teacherId', 
      {
        type: Sequelize.INTEGER,
        allowNull: true,
        references: {
          model: 'Users', // Users 모델에서
          key: 'id', // 그 아이디 값을 참고합니다.
        },
        onUpdate: 'CASCADE',
        onDelete: 'SET NULL',
      }
    );
    // 🎈 클래스 테이블에 regionId 필드를 생성합니다.
    await queryInterface.addColumn(
      'Classes', 
      'regionId', 
      {
        type: Sequelize.INTEGER,
        allowNull: true,
        references: {
          model: 'Regions', // Regions 모델에서
          key: 'id', // 그 아이디 값을 참고합니다.
        },
        onUpdate: 'CASCADE',
        onDelete: 'SET NULL',
      }
    );
   
  },

  down: async (queryInterface, Sequelize) => {
    // 🎈 위에서 생성한 필드를 제거합니다.
    await queryInterface.removeColumn(
      'Classes', // name of Source model
      'teacherId' // key we want to remove
    );
    await queryInterface.removeColumn(
      'Classes', 
      'regionId', 
    );
    
};

이렇게 models,migrations 폴더에서 값을 설정해 주었다면 아래 커맨드로 up, down 함수가 잘 작성되었는지 확인한다.
마이그레션과 마이그레이션 취소를 해주면 된다.

npx sequelize-cli db:migrate
npx sequelize-cli db:migrate:undo

아무 문제 없다면 외래키 관계설정이 잘 된 것이다. mysql에 접속하여 MUL이 적혀져 있는지 확인해보자!
외래키 만들기 성공!!


3. 조인 테이블

조인 테이블은 다대다 관계를 설정할 때 만든다.
조인 테이블인 경우 앞서 언급했다시피 기본 모델을 마이그레이션 후 작성한다.

1) models 폴더

조인테이블도 마찬가지로 관계설정을 해줘야한다.
단, N:M의 관계 임으로 관계를 같는 모델에 모두 관계를 설정해준다.

예를 들어 클래스 찜 같은 경우에는 N:M의 관계를 갖는다.
한 유저는 다수의 클래스를 좋아하고, 한 클래스는 다수의 유저로부터 찜 당할 수 있기 때문이다.

이때는 외래키 설정과 달리 through 라는 속성을 추가해준다.
through 값은 조인테이블 명이 된다.

models > class.js 파일

'use strict';
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class Class extends Model {

    static associate(models) {
      
      this.belongsTo(models.User);
      
      // 🎈 조인 테이블을 위한 관계 설정이다.
      this.belongsToMany(models.User, { through: 'like_classes'});
    }
  };
  Class.init({
    name: DataTypes.STRING,
    price: DataTypes.INTEGER,
    type: DataTypes.STRING,
    score: DataTypes.DECIMAL,
    discount: DataTypes.INTEGER,
    img_url: DataTypes.STRING,
    contents: DataTypes.STRING,
  }, {
    sequelize,
    modelName: 'Class',
  });
  return Class;
};

models > User.js 파일

'use strict';
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class User extends Model {

    static associate(models) {
      
      // 🎈 조인 테이블을 위한 관계 설정이다.
      this.belongsToMany(models.Class, { through: 'like_classes'})
    }
  };
  User.init({
    login_id: DataTypes.STRING,
    password: DataTypes.STRING,
    nick_name: DataTypes.STRING,
    name: DataTypes.STRING,
    birth: DataTypes.STRING,
    gender: DataTypes.STRING,
    profile_url: DataTypes.STRING,
    admin: DataTypes.STRING,
    info: DataTypes.STRING
  }, {
    sequelize,
    modelName: 'User',
  });
  return User;
};



2) migrations 폴더

조인 테이블도 마찬가지로 up, down 함수를 작성하여 필드가 생성되고, 삭제될 수 있도록 한다.

아래 커맨드로 조인 테이블을 위한 새로운 마이그레이션 파일을 만든다.
sequelize migration:generate --name associate-like-class

그러면 2021xxx-associate-like_class 와 같은 파일이 migrations 폴더에 생성된다.
migrations > 2021xxx-associate-like_class 파일

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable(
       'like_classes', 
      {
        createdAt: {
          allowNull: false,
          type: Sequelize.DATE
        },
        UserId: {
          type: Sequelize.INTEGER,
          primaryKey: true,
        },
        ClassId: {
          type: Sequelize.INTEGER,
          primaryKey: true,
        },
      }
    );
  },

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

여기는 외래키 생성과 달리 through 값에 맞는 테이블을 생성해야 함으로 queryInterface.createTable()queryInterface.dropTable()을 사용한다.

외래키 생성에서와 마찬가지로
npx sequelize-cli db:migrate
npx sequelize-cli db:migrate:undo
커맨드를 통해 조인테이블 생성 설정을 잘 하였는지 확인한다.

이 조인 테이블은 직접적으로 migrations 폴더 안에 파일에서 테이블 참조 설정을 하지 않았다.
(참고 레퍼런스를 보고 그대로 따라했지만) 그래서 정말 잘 되는지는 더미 데이터를 생성해보고 알 것 같다.




인제 설정은 그만하고 진짜 코딩을 하러가자!!😂🙌👏👍

profile
신입 개발자 입니다! 혹시 제 글에서 수정이 필요하거나, 개선될 부분이 있으면 자유롭게 댓글 달아주세요😊

0개의 댓글