Backend/Node.js

Node.js - ORM 및 Sequelize 개념, 사용 방법, 문법 정리

둉이 2021. 10. 10. 17:39
ORM(Object Relational Mapping) 이란?

객체를 통해 간접적으로 데이터베이스를 다루는 방식
직접 SQL 쿼리를 작성하지 않고 프로그래밍 언어를 이용하여 DB에 접근할 수 있음

 

- 장점

1. 개발 코드와 DB의 종속성 분리

2. 생산성 향상 및 유지보수 용이

3. 특정 DBMS에 종속적이지 않아 Object에만 집중 가능

(= 다른 DBMS간에도 문법이 호환되므로 프로젝트 진행시 다른 종류의 DBMS로 교체할 경우 편리)

 

- 단점

1. 커스터마이징 및 디버깅이 어려움

2. 실행 속도가 raw 방식보다 느림

3. 복잡한 쿼리 작성이 어려움

 

- Express에서 MySQL ORM 모듈로는 Sequelize와 TypeORM, Knex 등이 있음

① Sequelize

→ PostgreSQL, MySQL, MariaDB, SQLite, MSSQL 지원

 

② TypeORM

→ MySQL, MariaDB, PostgreSQL, CockroachDB, SQLite, MSSQL, Oracle, sql.js, MongoDB 지원

 

③ Knex

→ PostgreSQL, MySQL, SQLite3 지원

 

Sequelize란?

ORM의 일종으로, 자바스크립트 객체와 데이터베이스의 릴레이션을 매핑해주는 도구
프로그래밍 언어를 사용하여 DB에 접근할 수 있으므로 SQL 문법을 몰라도 된다는 장점을 가짐

 

Sequelize의 작동 원리

출처: https://www.hanumoka.net/2018/11/23/node-20181123-express-setting-sequelize/

1. Express 내부의 시퀄라이저가 js 파일 실행

2. js 파일 내의 내부 entity 정보를 읽어서 sequelize에 적재

3. Express에서 sequelize를 이용하여 DB에 접근 (동기/비동기)4. 접근한 DB 스키마에 CRUD 실행

 

Express - Sequelize ORM 연동

1. sequelize 및 sequelize cli 설치

# npm i sequelize
# npm i -g sequelize-cli

 

2. sequelize 명령어를 이용하여 폴더 생성

# sequelize init

 

3. 생성된 config 폴더에 있는 config.json을 config.js으로 변경 후 아래와 같이 내용 수정

: dotenv를 사용하기 위해 *.js로 수정해야 함

import dotenv from 'dotenv'
dotenv.config()

const development = {
   dialect: 'mysql',
   host : process.env.DB_HOST,
   port: process.env.DB_PORT,
   username : process.env.USER_NAME,
   password : process.env.USER_PASSWD,
   database : process.env.DB_NAME
}

const production = {
   dialect: 'mysql',
   host : process.env.DB_HOST,
   port: process.env.DB_PORT,
   username : process.env.USER_NAME,
   password : process.env.USER_PASSWD,
   database : process.env.DB_NAME
}

const test = {
   dialect: 'mysql',
   host : process.env.DB_HOST,
   port: process.env.DB_PORT,
   username : process.env.USER_NAME,
   password : process.env.USER_PASSWD,
   database : process.env.DB_NAME
}

export default { development, production, test }

 

4. models 폴더 내 index.js에서 config.json → config.js로 수정

 

5. MySQL의 이미 생성된 스키마 목록을 읽어오기 위해 sequelize-auto 설치

: sequelize를 이용하여 직접 테이블을 모델링하는 방법도 있지만, 나는 이미 생성된 테이블을 불러와서 사용하고 싶어서 이 방식을 사용했다.

# npm i sequelize-auto

 

6. 아래 내용으로 js 파일 생성 후 실행

const SequelizeAuto = require('sequelize-auto')
const auto = new SequelizeAuto('DB 이름', '사용자 이름', '사용자 비밀번호', {
   host: '115.85.182.54',  // DB Host 주소
   port: '3306',  // 포트 번호
   dialect: 'mysql'  // 사용하는 DBMS 종류
})

auto.run()

 

6번까지 수행하면 자동으로 생성된 sequelize 모델들을 얻을 수 있다!

 

7. router에서 sequelize를 사용하여 테이블 접근 및 사용

: router 파일을 열고 다음과 같이 사용하면 된다.

const express = require('express')
const router = express.Router()
const model = require('../database/models')

const findAllUsers = async () => await models['tb_user'].findAll()

router.get('/test', async (req, res) => {
  res.json(await findAllUsers())
})

module.exports = router

 

Sequelize 문법 정리

1. SELECT(findOne, findAll)

- findAll은 조건을 만족하는 모든 데이터를 가져옴

- findOne은 조건을 만족하는 첫 번째 데이터만 가져옴(= limit 1)

- where절 조건 지정 가능

: Op 모듈을 import해서 and와 or 연산자를 사용할 수 있음

const { Op } = require('sequelize')

model['table'].findAll({
   where: {
      column: 123  // select * from table where column = 123
   }
})

model['table'].findAll({
   where: {
      [Op.or]: [
         {column: 123},
         {column: 456},
      ]  // select * from table where column = 123 or column = 456
   }
})

 

2. 조인

기본적으로 left join(required: false)이며, inner join을 사용하려면 required: true 옵션 추가

  여러 테이블을 조인하려면 include 안에 {} 형태로 여러 개 넣어주면 된다.

models['table1'].findAll({
   include: [{
      model: models['table2'],
      required: true,
      as: 'alias'  // alias 지정 가능
      attributes: ['title', 'content']  // select할 컬럼 선택
   }]
})

 

3. limit / offset 설정

: limit은 한 번에 가져올 row의 개수, offset은 가져오려는 row의 시작 인덱스 값

  페이지네이션에 사용 가능

model.findAll({
   where: { ... },
   limit: 10,
   offset: 0
})

 

4. 정렬(order by)

: order 속성 이용

model.findAll({
  order: [
      ['age', 'desc']  // 정렬할 컬럼명과 오름차순/내림차순 구분
   ],
   where: { ... }
})

 

5. 특정 컬럼 선택

: attributes 속성 이용

model.findAll({
   attributes: [ 'name' ],  // 선택할 컬럼 목록 입력
   where: { ... }
})

 

6. 시퀄라이즈 결과값에서 dataValues 부분만 가져오기

: raw 속성 이용

await models['tb_category'].findAll({
   raw: true,  // raw 값이 true이면 데이터 속성만 리턴
})

 

 

cf) Op 모듈

: Op 모듈을 import해서 and, or, eq, ne 등의 다양한 연산자를 사용할 수 있음

 

① and / or

: where절에서 [Op.and]: [{ a: 5 }, { b: 6 }] 형태로 사용 가능

[Op.and]: [{ a: 5 }, { b: 6 }]  // a = 5 and b = 6
[Op.or]: [{ a: 5 }, { b: 6 }]  // a = 5 or b = 6

// 혹은 이렇게도 사용 가능
await models['tb_category'].count({
   where: {
      a: {
         [Op.or]: [5, 6]  // a in [5, 6]
      }
   }
   raw: true,
})

 

② eq / ne

[Op.eq]: 3  // = 3
[Op.ne]: 20  // != 20

 

③ not

[Op.not]: true  // IS NOT true

 

④ is

[Op.is]: null  // IS NULL

 

⑤ gt / gte

: gt는 초과, gte는 이상을 나타냄

[Op.gt]: 6  // > 6
[Op.gte]: 6  // >= 6

 

⑥ lt / lte

: lt는 미만, lte는 이하를 나타냄

[Op.lt]: 6  // < 6, 미만
[Op.lte]: 6  // <= 6, 이하

 

⑦ between / notBetween

[Op.between]: [6, 10]  // BETWEEN 6 AND 10
[Op.notBetween]: [11, 15]  // NOT BETWEEN 11 AND 15

 

⑧ in / not in

[Op.in]: [1, 2]  // IN [1, 2]
[Op.notIn]: [1, 2]  // NOT IN [1, 2]

 

⑨ startsWith / endsWith

[Op.startsWith]: 'hat'  // LIKE 'hat%'
[Op.endsWith]: 'hat'  // LIKE '%hat'

 

⑩ like / notLike

[Op.like]: 'hat%'  // LIKE 'hat%'
[Op.notLike]: '%hat'  // NOT LIKE '%hat'

 

⑪ substring(=LIKE)

: like 구문처럼 사용 가능

[Op.substring]: 'hat'  // LIKE '%hat%'

 

⑫ regexp / notRegexp

: MySQL과 PostgreSQL에서만 사용 가능

[Op.regexp]: '^[h|a|t]'  // REGEXP/~ '^[h|a|t]'
[Op.notRegexp]: '^[h|a|t]'  // NOT REGEXP/~ '^[h|a|t]'

 

→ https://codingmania.tistory.com/555 블로그를 참고하여 작성했습니다.

 

cf) fn 모듈

: sequelize의 fn 모듈을 import해서 sum, count, min, max 등의 다양한 집계 함수를 사용할 수 있음

 

① sum

const { Op, fn, col } = require('sequelize')

model.findAll({
   attributes: ['itemId', [sequelize.fn('sum', sequelize.col('amount')), 'total']],
   where: { ... }
})

 

② count, min / max 등 기타

: sum과 같은 방법으로 사용하면 됨

 

2. INSERT

model.create({
   컬럼명1: '값1',
   컬럼명2: '값2',
   컬럼명3: '값3',
   ...
})

 

3. UPDATE

model.update(
   {
      바꿀 컬럼명: '바꿀 값',
      ...
   }, {
      where: { ... }
   }
)

 

4. DELETE

model.destroy({
   where: { ... }
})

 

5. CREATE

model.define('tb_board', {  // 첫 번째 파라미터는 테이블명
    board_sn: {
      type: Sequelize.INTEGER,
      primaryKey: true,
      autoIncrement: true,
   },
   title: {
      type: Datatypes.TEXT,
      allowNull: false,
   },
   content: {
      type: Datatypes.TEXT,
      allowNull: true,
   },
   view_num: {
      type: Datatypes.INTEGER.SIGNED,
      allowNull: false,
   },
   {
      timestamps: true,  // 이 값을 true로 줄 경우, createAt과 updateAt 컬럼 자동 생성
   }
})

 

cf) literal

: sequelize의 fn 모듈을 import해서 SQL raw 쿼리를 날릴 수 있음

  where, attributes 등에 사용 가능

await models['tb_item'].findOne({
   attributes: [
      [literal(`(select AUTO_INCREMENT from information_schema.tables where table_name='tb_item')`), 'item_sn']
   ],
   raw: true,
})

 

not association 오류 해결

SequelizeEagerLoadingError: [테이블명1] is not associated to [테이블명2]

 

직접 모델을 생성한 경우에는 말 그대로 association을 선언하지 않아서 발생한 오류이고, sequelize-auto를 통해 시퀄라이즈 모델을 생성한 경우에는 자동 생성된 association을 시퀄라이즈가 인식하지 못해서 발생한다.

 

→ 해결 방법

1. sequelize-auto 사용 시

init-models.js에 있는 association 코드들을 index.js로 이동하면 됨

 

2. 직접 만든 경우

sequelize-auto를 사용하지 않고 직접 모델을 생성한 경우에는 index.js에 아래처럼 적절하게 association을 설정해 주면 된다.

Object.keys(db).forEach(modelName => {
   if (db[modelName].associate) {
      db[modelName].associate(db)
   }
})

// 여기에 작성
db['tb_like'].belongsTo(db['tb_user'], { foreignKey: "user_id"})
db['tb_user'].hasMany(db['tb_like'], { foreignKey: "user_id"})

db.sequelize = sequelize
db.Sequelize = Sequelize module.exports = db