Sequelize.js 다루기


이 포스트에서 사용한 코드는 ES2015의 문법을 다소 사용하고 있으므로 가급적 Node.js v4(LTS) 이상의 버전을 권장한다.


Sequelize.js
Node.js는 대개 MongoDB + Mongoose 조합과 함께 사용하는 경우가 많지만 가끔 RDB의 필요성을 느끼기도 한다. 물론 DB에 연결해서 쿼리를 직접 만들어 날려도 되지만, ORM을 사용하면 수 배 이상의 생산성을 가질 수 있을 것이다.

Sequelize.js는 Node.js 기반의 ORM(Object-Releational-Mapping)이다. 공식적으로 PostgreSQL, MySQL, MariaDB, SQLite, MS-SQL을 지원한다.

Sequelize.js는 npm으로 다음과 같이 쉽게 설치할 수 있다.

1
npm install sequelize

Sequelize는 node 패키지로 node-mysql을 포함하고 있지 않으므로 node-mysql도 수동으로 설치해주어야 한다.

1
npm install mysql

연결하기

설치한 다음에는 다음과 같이 DB에 연결할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
const Sequelize = require('sequelize');

const sequelize = new Sequelize(
'practice', // 데이터베이스 이름
'username', // 유저 명
'password', // 비밀번호
{
'host': 'localhost', // 데이터베이스 호스트
'dialect': 'mysql' // 사용할 데이터베이스 종류
}
);

물론 DB에 연결하기 위해서는 (당연하지만) 사용하려는 DBMS가 설치되어 있어야 하고 해당 이름의 DB와 유저 등이 이미 존재하고 있어야 한다.

모델 만들기

자바스크립트 객체와 실체 테이블을 매핑하기 위해서는 모델을 정의해야한다. Sequelize에서는 다음과 같이 모델을 정의할 수 있는 메소드를 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sequelize.define('Memo', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
title: {
type: Sequelize.STRING,
allowNull: true
},
body: {
type: Sequelize.TEXT,
allowNull: true
}
});

Sequelize는 모델을 정의하면 편리하게도 createdAt, updatedAt 필드를 자동으로 만들어주며, 테이블에 레코드를 생성할 때, 업데이트 할 때 자동으로 관리해주기도 한다.

아무튼 위와 같이 모델을 정의하면 실제 DB 테이블은 다음과 같은 형태가 된다.

Table: Memo
| Field | Type | Allow Null | Key |
|———–|—————|————|—–|
| id | INT | false | PRI |
| title | VARCHAR(255) | true | - |
| body | TEXT | true | - |
| createdAt | DATETIME | false | - |
| updatedAt | DATETIME | false | - |

Sequelize.js는 보통 RDB에서 사용하게 되는 데이터 타입은 모두 지원한다. 이 링크에서 데이터 타입의 목록을 확인할 수 있다.

관계 정의하기

관계를 정의하기 위해 다음과 같이 모델을 하나 더 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
const Label = sequelize.define('Label', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: Sequelize.STRING,
allowNull: false,
unique: true
}
});

Label과 Memo 사이에 N:M 관계를 만들기 위해서 belongsToMany() 라는 메소드를 사용할 수 있다.

1
2
Memo.belongsToMany(Label, { through: 'MemoLabel' });
Label.belongsToMany(Memo, { through: 'MemoLabel' });

이렇게 N:M 관계를 정의하면 흔히 사용하던 것처럼 교차테이블이 생성된다. 교차 테이블의 이름은 위와 같이 through라는 프로퍼티로 정해줄 수 있다. 그러면 실제 DB 테이블은 다음과 같이 생성된다.

Table: MemoLabel
| Field | Type | Allow Null | Key |
|———–|—————|————|———|
| LabelId | INTEGER | false | PRI |
| MemoId | INT | false | MUL |
| createdAt | DATETIME | false | - |
| updatedAt | DATETIME | false | - |

그리고 관계는 다음과 같이 만들어진다.

Relation for table: MemoLabel
| Name | Columns | FK Table | FK Columns | On Update | On Delete |
|——————|———|———-|————|———–|———–|
| memolabel_ibfk_1 | LabelId | Labels | id | CASCADE | CASCADE |
| memolabel_ibfk_2 | MemoId | Memos | id | CASCADE | CASCADE |

이 뿐만 아니라 1:1 관계를 위한 hasOne(), belongsTo(), 1:M 관계를 위한 hasMany() 메소드도 지원한다. 참고로 DELETE 시에 CASCADE되는 것은 N:M 관계일 때 뿐이며, 1:1, 1:M 관계에서는 SET NULL이다. 관계에 대한 자세한 내용은 여기를 참조.

레코드 다루기 (CRUD)

Sequelize.js는 기본적인 CRUD 오퍼레이션 외에도 편의를 위한 다양한 기능을 제공한다. 모든 오퍼레이션은 bluebird를 기반으로 한 Promise를 리턴한다.

레코드 만들기

레코드를 만들려면 정의한 모델의 create() 메소드를 사용하면 된다.

1
2
3
4
Memo.create({
title: 'Practice of Sequelize.js',
body: 'Sequelize.js is ORM for Node.js.'
});

관계가 있는 모델의 교차테이블에 레코드를 만드는 것은 다음과 같이 시맨틱하게 할 수 있다.

1
memo.addLabel(label);

addLabel()은 관계를 정의하면 테이블명을 이용해서 자동적으로 만들어지는 메소드이다. 이 외에도 배열을 추가할 수 있는 addLabels() 메소드와 getLabels(), setLabels() 등이 자동으로 생성된다. 다대다 관계이므로 Label 모델에도 동일하게 addMemo()와 같은 메소드가 생성된다.

레코드 조회하기

Sequelize.js는 레코드 조회를 위해서 findOne() 혹은 findAll()과 같은 메소드를 제공한다.

1
2
3
4
5
6
Memo.findOne({
where: { title: 'Practice of Sequelize.js' }
})
.then((memo) => {
console.log('Memo: ', memo.dataValues);
});

다만, 이렇게 얻어온 레코드의 실제 값은 dataValues라는 프로퍼티 안에 있으므로 유의해야 한다.
또한, 조회 시 사용할 수 있는 여러가지 오퍼레이터가 존재하는데 그 목록은 여기서 확인 가능하다.

교차테이블의 레코드를 조회하는 것도 시맨틱하게 할 수 있다.

1
2
3
4
memo.getLabels()
.then((labels) => {
console.log('Memo\'s Label: ', labels[0].dataValues);
});

include라는 프로퍼티를 사용하면 조인된 상태의 데이터를 가져오는 것도 가능하다.

1
2
3
4
5
6
7
Memo.findOne({
where: { title: 'Practice of Sequelize.js' },
include: { model: Label }
})
.then((memo) => {
console.log('Joined Memo: ', memo.dataValues);
});

findOrCreate() 라는 메소드를 사용하면 조회 후에 없는 값이면 생성하고, 있는 값이면 그 값을 가져오는 형태의 오퍼레이션도 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
Memo.findOrCreate({
where: { title: 'Mongoose.js' },
defaults: {
body: 'Mongoose.js is ODM for Node.js.'
}
})
.spread((memo, created) => {
if (created) {
console.log('New Memo: ', memo.dataValues);
} else {
console.log('Old Memo: ', memo.dataValues);
}
});

다만 반환하는 값이 두 가지여서 그런지 then()을 사용하면 안되고 spread()를 사용해야 한다.

레코드 갱신하기

모델 인스턴스는 업데이트 메소드를 가지고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
Memo.update({
title: 'Updated Memo'
}, {
where: { id: 1 }
})
.then(() => {
return Memo.findOne({
where: { title: 'Updated Memo' }
});
})
.then((memo) => {
console.log('Updated Memo: ', memo.dataValues);
});

혹은 레코드 자체의 업데이트 메소드를 활용할 수 있다.

1
2
3
4
memo.update({ body: 'This memo is updated.' })
.then((memo) => {
console.log('Updated Memo2: ', memo.dataValues);
});

참고로, Primary Key를 업데이트하는 것은 불가능하다.

레코드 삭제하기

마찬가지로 모델 인스턴스의 destroy() 메소드를 사용하면 된다.

1
2
3
4
5
6
7
8
9
Memo.destroy({
where: { title: 'Updated Memo' }
})
.then(() => {
return Memo.findOne({ where: { title: 'Updated Memo' } });
})
.then((memo) => {
console.log('Destroyed Memo? :', memo); // null
});

아니면 레코드의 업데이트 메소드를 쓸 수도 있다.

1
2
3
4
5
6
7
Memo.findById(2)
.then((memo) => {
return memo.destroy();
})
.then(() => {
console.log('Memo is destroyed.');
});

마치며

이외에도 Node.js ORM은 bookshelf.js, node-orm2 같은 것들이 있다. Sequelize.js는 이 중 GitHub 레포지토리 스타 수도 가장 많고 활성화된 사용자 수가 가장 많은 만큼 앞으로 프로젝트를 새로 시작한다면 가장 선택할만한 ORM일 것이다.

참고링크