Skip to content

Sequelize ORM by 윤현우

Hyeonu Yun edited this page Oct 28, 2020 · 1 revision

작성자 : J131 윤현우

최종수정일 : 2020.10.28(수)

첨언 : 블로그에 쓰려고 정리하다가 시간이 부족해서 아직 완성하지 못한 글입니다. 조원분들께 1이라도 도움이 될까 하여 업로드합니다.


Sequelize

  • mySQL 기반의 ORM
  • 비동기식 API이므로 then, catch, finally, async, await 모두 이용 가능합니다.
  • 처음부터 완성된 소스코드를 기재한 후 설명하기보다는 윤곽부터 잡아가며 흐름을 설명하는 글입니다. 시간이 부족하신 분들은 가장 하단에 최종 소스코드를 참고해주시면 될 것 같습니다.
  • 하지만 그 최종 소스코드 또한 제 소스코드이지, 보시는 분들이 필요한 내용이 없을 수도 있습니다. 되도록 어떤 코드가 왜 들어가게 되었는지 흐름을 파악한 후, 자기 것으로 만드시길 권장드립니다.

개발환경

  • server

    • dev 환경 : Ubuntu 20.04 (WSL2)
    • production 환경 : Ubuntu 16.04
  • DB

    • dev 환경 : MySQL 8.0.21
    • production 환경 : MySQL
  • Node

    • dev 환경, production 환경 : v12.18.4
  • Package

    • express : v4.17.1
    • sequelize : v6.3.5
    • mysql2 : v2.2.5

들어가며

  • node express 기반의 웹 서비스를 만든다고 가정하고, 최상단 디렉토리에는 app.js가 위치합니다.

  • 같은 디렉토리 레벨에는 models라는 디렉토리가 존재하며, 해당 디렉토리 내부에서 model과 관련된 전체적인 작업이 이루어집니다.

  • 가계부를 만든다고 생각하고 테이블을 '사용자(user)', '결제수단(payment)', '내역(history)', '내역 카카테고리(category)'로 두었습니다. N:M 관계로 인해 파생되는 테이블은 제외했습니다. 이 내용을 인지하시고 설명을 보면 더욱 이해가 잘 되실 것 같습니다.

설치

npm install sequelize
npm install mysql2

데이터베이스와 연결

models라는 디렉토리 내부에 index.js 파일을 생성한 후 아래와 같이 기재해줍니다.

디렉토리명이나 파일명은 당연히 달라도 되지만, 컨벤션에 따르는 것을 권장합니다.

models
    └── index.js
// index.js

const Sequelize = require('sequelize');

const sequelize = new Sequelize(
    'database', // 데이터베이스 이름
    'username', // 유저명
    'password', // 비밀번호
    {
        host: 'host', // 데이터베이스 호스트. 기본값은 'localhost'
        dialect: 'mysql' // 사용할 데이터베이스 종류
    }
);
// 나의 index.js
const Sequelize = require("sequelize");
const config = require("../config/db.config");

const sequelize = new Sequelize(
    config.database,
    config.username,
    config.password,
    {
        host: config.host,
        port: config.port,
        dialect: "mysql",
    }
);
  • 주의할 점
    • Sequelize는 라이브러리 자체를 지칭합니다.
    • sequelize는 하나의 데이터베이스에 대한 연결을 나타내는 Sequelize의 인스턴스입니다.
    • 물론 변수명을 다르게 지을 수 있지만, 대부분의 documentation과 참고 자료에서 해당 convention을 유지합니다.

연결되었는지 테스트

sequelize
  .authenticate()
  .then(function(err) {
    console.log('Connection has been established successfully.');
  })
  .catch(function (err) {
    console.log('Unable to connect to the database:', err);
  });

연결 끊기

Sequelize는 기본적으로 연결을 유지한 채 모든 쿼리에 대해 동일한 연결을 사용합니다. 만약 연결을 종료해야 하는 경우 sequelize.close()를 사용하시면 됩니다. 비동기식으로 Promise를 return하게 되는데, 사실 그럴 일은 없으므로 패스하겠습니다.


모델 정의

index.js 내부에서 모델을 생성해도 되지만 모듈을 분리하는 것이 여러모로 좋습니다. 따라서, 동일한 디렉토리 내에 각 테이블 별로 js파일을 만들어줍니다.

models
    ├── category.js
    ├── history.js
    ├── index.js
    ├── payment.js
    └── user.js

모델을 정의하는 방법은 두 가지가 있는데, 그중에서 sequelize.define 를 이용하고자 합니다.

sequelize.define(modelName, attributes, options)

//user.js
module.exports = (sequelize, DataTypes) => {
    const user = sequelize.define(
        "user",
        {
            uid: {
                type: DataTypes.STRING,
                allowNull: false,
                primaryKey: true,
            },
            nickname: {
                type: DataTypes.STRING,
                allowNull: false,
            },
            password: {
                type: DataTypes.STRING,
                allowNull: false,
            },
        },
        {
            tableName: "users",
            underscored: true,
        }
    );
    
    return user;
};

자바스크립트 내의 user라는 객체에 sequelize.define 메서드를 이용하여 실제 데이터베이스 내 테이블과 매핑하는 작업을 하는 과정입니다. 자세히 풀어써본다면, 객체 user"user"라는 모델을 정의할 것인데, 그 모델의 속성값에 대한 내용은 두 번째 인자에 있는 object에 담겨있다는 것입니다. 또한 추가적으로 option까지 줬는데, option은 다양하기 때문에 여기를 참고해주시면 될 것 같습니다.

또한, 모듈을 분리했기 때문에 module.exports로 감싸줘야 합니다. 관계까지 정의하고 나서 각 모델을 어떤 식으로 활용하는지 안내할 예정인데, index.js에서 각 모델을 활용하기 위해서는 module.exports를 해줘야 합니다.

모델의 속성에 대해 더욱 자세하게 살펴볼까요?
  • object형식의 key-value 구조로 구성되어 있습니다. key는 "user" 모델에 만들고자 하는 column을 의미하고, value은 해당 column과 관련한 옵션을 의미합니다.

  • value 안에는 다양한 옵션들이 존재하는데 공부하면서 자주 썼던 것들에 대해 간략히 기재하겠습니다.

    • type : 데이터 타입 (자세한 설명은 여기를 참고해주세요)
    • allowNull : null을 허용할 것인지에 대한 옵션입니다. 기본값은 true입니다.
    • primaryKey : 기본키로 설정한 것인지에 대한 옵션입니다. 기본값은 false입니다.
    • autoIncrement : mySQL의 AUTO_INCREMENT와 동일한 것으로, 자동으로 증가하는 INTEGER로 설정한 것인지에 대한 옵션입니다. 기본값은 false입니다.
  • 자세한 내용 : 여기를 들어가서 Ctrl+F를 눌러 검색창을 키신 후 'public static init' 을 입력하시면 하단에 테이블이 보입니다. 테이블 내에 존재하는 attributes.column. 로 시작하는 파라미터들이 전부 column 속성에 대한 옵션입니다.

sequelize.define 메서드에는 모델의 속성 말고 모델에 대한 옵션을 설정할 수 있습니다.
  • 위 소스코드에서 저는 두 개의 옵션을 부여했는데, 그에 대한 설명과 함께 timestamps라는 옵션에 대해서도 간략히 기재하겠습니다.
    • tableName : 직접 테이블명을 설정하는 옵션입니다. 따로 설정해주지 않는다면 Sequelize 내부적으로 테이블명을 설정하게 되는데, 모델명으로부터 정해진 규칙에 의해 변형을 일으킨 후 테이블명을 설정해줍니다. 자세한 내용은 여기를 참고해주시면 됩니다.
    • underscored : 테이블 내 컬럼의 네이밍 컨벤션을 스네이크 케이스로 변경해주는 설정입니다. 기본값은 false입니다. 데이터베이스 네이밍 컨벤션에 따르면 테이블의 이름은 스네이크 케이스가 옳다고 판단되어 true로 설정해줬습니다.
    • timestamps : 기본적으로 Sequelize는 모든 모델에 자동으로 createdAtupdateAt 컬럼을 추가합니다. 두 컬럼의 데이터 타입은 DataTypes.DATE 이며, createdAt 컬럼에는 레코드가 만들어진 시점이 기록되고, updateAt 컬럼에는 레코드가 수정된 시점이 기록됩니다. 기본값은 true이며 불필요하다고 여겨지신다면 timestamps : false 옵션을 통해 제거해줄 수도 있습니다. 공식문서를 참고하면 더욱 다양한 활용을 할 수 있으니 필요하시다면 참고바랍니다.
  • 자세한 내용 : 여기를 들어가서 Ctrl+F를 눌러 검색창을 키신 후 'public static init' 을 입력하시면 하단에 테이블이 보입니다. 테이블 내에 존재하는 options. 로 시작하는 파라미터들이 전부 column 옵션에 관한 내용입니다.

관계 정의

sequelize에서는 아래와 같은 관계를 지원해줍니다.

  • One-To-One (1:1)
  • One-To-Many (1:N)
  • Many-To-Many (N:M)

위 관계를 정의하기 위해서는 sequelize에서 지원하는 관계 타입 HasOne, BelongsTo, HasMany, BelongsToMany를 사용하면 됩니다. 네 타입 모두 비슷한 방식으로 사용되므로, 사용 방법을 한 번만 제대로 익혀두시면 금방 활용할 수 있습니다.

One-To-One
One-To-Many

sequelize는 Many-To-One은 지원하지 않고, One-To-Many만 지원하고 있습니다. One-To-Many에서는 단방향 매핑을 하는 것보다 양방향 매핑을 하는 것이 관리 측면이나 성능 측면에서 더 좋습니다. sequelize에서도 One-To-Many의 양방향 매핑을 권장하고 있습니다.

본격적으로 관계를 정의하기 전에 기존에 생성한 user 모델과 관계를 맺을 모델 하나를 추가로 생산해봅시다.

// history.js

module.exports = (sequelize, DataTypes) => {
    const history = sequelize.define(
        "history",
        {
            hid: {
                type: DataTypes.INTEGER,
                allowNull: false,
                primaryKey: true,
                autoIncrement: true,
            },
            price: {
                type: DataTypes.INTEGER,
                allowNull: false,
            },
            content: {
                type: DataTypes.STRING,
            },
        },
        {
            tableName: "history",
            underscored: true,
        }
    );
    return history;
};

한 명의 사용자는 다수의 결제 내역을 가지고 있을 수 있으며, 하나의 결제 내역은 한 명의 사용자와 관계를 맺을 수 있습니다. 따라서 user테이블과 history테이블은 One-To-Many (1:N) 관계를 가진다고 할 수 있습니다.

이때 관계를 정의하는 것은 다음과 같습니다.

user.hasMany(history);
history.belongsTo(user);

또한, 관계를 정의할 때 옵션을 추가할 수 있는데 외래키on delete, on update를 설정할 수 있습니다. 기본적으로 One-To-One 관계와 One-To-Many 관계의 제약조건은 ON DELETE SET NULL, ON UPDATE CASCADE 입니다. 저는 다음과 같이 설정했습니다.

user.hasMany(history, {
    foreignKey: {name: }
});
history.belongsTo(user, {
    
});
Many-To-Many

본격적으로 관계를 정의하기 전에 기존에 생성한 user 모델과 관계를 맺을 모델 하나를 추가로 생산해봅시다.

// payment.js

module.exports = (sequelize, DataTypes) => {
    const payment = sequelize.define(
        "payment",
        {
            paymentName: {
                type: DataTypes.STRING,
                allowNull: false,
                primaryKey: true,
            },
        },
        {
            tableName: "payment",
            underscored: true,
        }
    );
    
    return payment;
};

한 명의 사용자는 다양한 결제 수단을 가지고 있을 수 있으며, 하나의 결제수단은 다양한 사용자에게 사용되어질 수 있습니다. 따라서 user테이블과 payment테이블은 Many-To-Many (N:M) 관계를 가진다고 할 수 있습니다.


🎃 데일리 스크럼 🎃

Week 1

Week 2

Week 3

🎈 데일리 회고 🎈

Week 1

Week 2

Week 3

💻 주간 개발 진행 💻

Week 1

Week 2

Week 3

🎏 스프린트 계획 회의 🎏

🧑🏻‍🤝‍🧑🏻 피어세션 🧑🏻‍🤝‍🧑🏻

📚 위클리 팀 회고 📚



Clone this wiki locally