시퀄라이즈(Sequelize)란
시퀄라이즈란 노드에서 MySQL 작업을 쉽게 할 수 있도록 도와주는 라이브러리이다.
시퀄라이즈는 ORM(Object-Reational Mapping)으로 분류된다.
여기서 ORM은 자바스크립트 객체와 데이터베이스의 릴레이션을 매핑해주는 도구이다.
시퀄라이즈의 가장 큰 장점은 자바스크립트 구문을 알아서 SQL로 바꿔주는 부분이다.
SQL 언어를 직접 사용하지 않아도 자바스크립트만으로도 MySQL을 조작할 수 있다.
아래 예제에서는 users 테이블과 comments 테이블을 만들어 MySQL 데이터베이스에 저장하는 예제이다.
테이블 관계 정의하기
먼저 테이블 관계를 보면 일대다(1:N), 일대일(1:1), 다대다(N:N) 관계가 있다.
게시판을 예로 들때 아래와 같이 정의할 수 있다.
- 일대다(1 : N) : 사용자 1, 댓글 N
- 일대일(1 : 1) : 사용자1, 사용자에 대한 정보1
- 다대다(N : N) : 게시글 N, 해시태그 N
MySQL에서는 JOIN이라는 기능으로 여러 테이블 간의 관계를 파악해 결과를 도출한다.
시퀄라이즈는 JOIN 기능을 알아서 구현하지만, 테이블 간 어떠한 관계가 있는지 시퀄라이즈에게 알려야 한다.
일대다 관계
시퀄라이즈에서 일대다 관계를 hasMany라는 메서드로 설정한다.
그리고 가상의 user 모델과 comments 모델이 있다고 가정한다.
예를 들어 user 테이블의 로우 하나를 불러올 때 연결된 comments 테이블의 로우도 같이 불러올 수 있다.
반대로 belongsTo 메서드도 있는데, comments 테이블의 로우를 불러올 때 연결된 user 테이블의 로우를 가져온다.
// user.js
static associate(db) {
db.User.hasMany(db.Comment, { foreignKey: 'commenter', sourceKey: 'id' });
}
// comment.js
static associate(db) {
db.Comment.belongsTo(db.User, { foreignKey: 'commenter', targetKey: 'id' });
}
시퀄라이즈는 Comment 모델에 foreignKey(외래 키)인 commenter 컬럼을 추가하고, Commenter 모델의 외래 키 컬럼은 commenter고, User 모델의 id 컬럼을 가리키고 있다.
hasMany 메서드에서는 sourceKey 속성에 id를 넣고, belongsTo 메서드에서는 targetKey 속성에 id를 넣었다.
sourceKey의 id와 targetKey의 id 모두 User 모델의 id이다.
즉, hasMany에서는 sourceKey를 쓰고, belongsTo 에서는 targetkey를 쓴다고 생각할 수 있다.
일대일 관계
일대일 관계에서는 hasOne 메서드를 사용한다.
사용자 정보를 담고 있는 가상의 Info 모델이 있을 경우 아래와 같이 표현할 수 있다.
db.User.hasOne(db.Info, { foreignKey: 'UserId', sourceKey: 'id' });
db.Info.belongsTo(db.User, { foreignKey: 'UserId', targetKey 'id' });
일대일 관계라고 해도 belongsTo와 hasOne이 반대면 안된다.
belongsTo를 사용하는 Info 모델에 user Id 컬럼이 추가되기 때문이다.
다대다 관계
시퀄라이즈는 다대다 관계를 표현하기 위해 belongsToMany 메서드를 지원한다,
게시글 정보를 담고 있는 가상의 Post 모델과 해시태그 정보를 담고 있는 가상의 Hashtag 모델이 있을 때 아래와 같이 표현한다.
db.Post.belongsToMany(db.Hashtag, { through: 'PostHastag' });
db.Hashtag.belongsTomany(db.Post, { through: 'PostHashtag' });
양쪽 모델에 모두 belongsToMany 메서드를 사용하며, 다대다 관계의 특성상 새로운 모델이 생성된다.
through 속성에 그 이름을 지정하면 되며 새로 생성된 PostHashtag 모델에는 게시글과 해시태그의 아이디가 저장된다.
다대다 관계에서는 데이터를 조회할 때 여러 단계를 거치며 자동으로 만들어진 모델에 접근할 땐 아래와 같이 접근한다.
db.sequelize.models.PostHashtag
시퀄라이즈 쿼리
시퀄라이즈로 CRUD 작업을 하기 위해 먼저 시퀄라이즈 쿼리를 알아야 한다.
SQL문을 자바스크립트로 생성하는 것이라 정해진 시퀄라이즈의 방식이 있다.
쿼리는 프로미스를 반환하므로 then을 붙여 결괏값을 받을 수 있으며, async/await 문법을 같이 사용할 수도 있다.
로우 생성 쿼리
먼저 아래 예제는 로우를 생성하는 쿼리 예제이다.
// SQL 문
INSERT INTO testmysql.users (name, age, married, comment) VALUES ('One', 24, 0, '첫 번째');
// 자바스크립트
const { User } = require('../models');
User.create({
name: 'One',
age: 24,
married: false,
comment: '첫 번째',
});
models 모듈에서 User 모델을 불러와 create 메서드를 사용하면 된다.
여기서 데이터를 넣을 때 MySQL의 자료형이 아닌 시퀄라이즈 모델에 정의한 자료형대로 넣어야 한다.
로우 조회 쿼리
생성된 쿼리를 조회하기 위해 findOne 메서드를, 여러 개를 조회할 때 findAll 메서드를 사용한다.
// SQL 문
SELECT * FROM testmysql.users LIMIT 1;
// 자바스크립트
User.findOne({});
또한 attributes 옵션을 사용해 원하는 컬럼만 가져올 수 있다.
// SQL 문
SELECT name, married FROM testmysql.users;
// 자바스크립트
User.findAll({
attributes: ['name', 'married'],
});
where 옵션을 이용해 조건들을 나열 할 수 있다.
// SQL문
SELECT name, age FROM testmysql.users WHERE married = 1 AND age > 30;
const { Op } = require('sequelize');
const { User ] = require('../models');
User.findAll({
attributes: ['name', 'age'],
where: {
married: true,
age: { [Op.gt]: 30 },
}.
});
MySQL에서는 undefined라는 자료형을 지원하지 않으므로 where 옵션에 undefined가 들어가면 안된다.
빈 값을 넣고자 할 때는 null을 이용해야 한다.
Op 부분을 보면 특수한 Op.gt 와 같은 특수한 연산자가 있다.
Sequelize 객체 내부의 Op 객체를 불러와 사용할 수 있다.
아래 리스트는 자주 쓰이는 연산자이다.
- Op.gt : 초과
- Op.gte : 이상
- Op.lt : 미만
- Op.lte : 이하
- Op.ne : 같지 않음
- Op.or : 또는
- Op.in : 배열 요소 중 하나
- Op.notIn : 배열 요소와 모두 다름
아래와 같이 Op 연산자를 사용할 수 있다.
Op.or 속성에 OR 연산을 적용할 쿼리들을 배열로 나열하면 된다.
// SQL 문
SELECT id, name FROM users WHERE married = 0 OR age > 30;
// 자바스크립트
const { Op } = require('sequelize');
const { User } = require('../models');
User.findAll({
attributes: ['id', 'name'],
where: {
[Op.or]: [{ married: false }, { age: { [Op.gt]: 30 } }],
},
});
시퀄라이즈의 정렬 방식이며 order 옵션으로도 가능하다.
이 부분은 배열안에 배열이 있을 때 사용할 수 있는데, 정렬은 컬럼 여러 개의 기준으로도 할 수 있기 때문이다.
// SQL 문
SELECT id, name FROM users ORDER BY age DESC;
// 자바스크립트
User.findAll({
attributes: ['id', 'name'],
order: [['age', 'DESC']],
});
조회할 로우 개수를 설정할 수도 있으며 LIMIT 1인 경우 findAll 대신 fineOne 메서드를 사욯알 수 있다.
또한 limit 옵션을 사용할 수도 있다.
// SQL문
SELECT id, name FROM users ORDER BY age DESC LIMIT 1;
// 자바스크립트
User.findAll({
attributes: ['id', 'name'],
order: [['age', "DESC']],
limit: 1,
});
OFFSET 속성도 offset 옵션으로 구현할 수 있다.
// SQL문
SELECT id, name FROM users ORDER BY age DESC LIMIT 1 OFFSET 1;
// 자바스크립트
User.findAll({
attributes: ['id', 'name'],
order: [['age', "DESC']],
limit: 1,
offset: 1,
});
로우 수정 쿼리
로우를 수정하기 위해 update 메서드를 사용한다.
첫 번째 인수는 수정할 내용이고, 두 번째 인수는 어떤 로우를 수정할지에 대한 조건이다.
조건은 where 옵션에 조건들을 적는다.
// SQL문
UPDATE testmysql.users SET comment = '바꿀 내용' WHERE id = 2;
// 자바스크립트
User.update({
comment: '바꿀 내용',
}, {
where: { id: 2 },
});
로우 삭제 쿼리
로우를 삭제하기 위해 destory 메서드를 사용한다.
삭제할 조건을 where 옵션에 적는다.
// SQL문
DELETE FROM testmysql.users WHERE id = 2;
// 자바스크립트
User.destory({
where: { id: 2 },
});
관계 쿼리
findOne이나 findAll 메서드를 호출할 때 프로미스의 결과로 모델을 반환한다.
const user = await User.findOne({});
console.log(user.name); // 사용자 이름
User 모델의 정보에도 바로 접근할 수 있지만 더 편리한 점은 관계 쿼리를 지원하는 것이다.
MySQL의 JOIN 기능과 비슷한 기능이다.
가상의 User, Comment 모델이 있다고 가정하고, 각 모델은 hasMany-belongsTo 관계가 맺어져 있다.
만약 특정 사용자를 가져오면서 그 사람의 댓글까지 모두 가져오고 싶다면 include 속성을 사용한다.
const user = await User.findOne({
include: [{
model: Comment,
}]
}];
// 사용자 댓글
console.log(User.Comments);
어떤 모델과 관계가 있는지를 include 배여렝 넣어주면 되는데, 여기서 배열인 이유는 다양한 모델과 관게가 있을 수 있기 때문이다.
위의 경우를 아래와 같이 접근할 수도 있다.
const user = await User.findOne({});
const comments = await user.getComments();
// 사용자 댓글
console.log(comments);
관계를 설정했다면 4가지의 메서드를 지원하며 동사 뒤에 모델의 이름이 붙는 형식으로 지원한다.
- getComments : 조회
- setComments : 수정
- addComment : 하나 생성
- addComments : 여러 개 생성
- removeComments : 삭제
동사 뒤에 모델 이름을 바꾸고 싶다면 관계 설정 시 as 옵션을 사용한다.
// 관계 설정 시 as 옵션으로 등록
db.User.hasMany(db.Comment, { foreignKey: 'commenter', sourceKey: 'id', as: "answers' });
// 쿼리 시
const user = await User.findOne({});
const comments = await user.getAnswers();
// 사용자 댓글
console.log(comments);
as를 설정하면 include 시 추가되는 댓글 객체도 user.Answers로 바뀐다.
include나 관계 쿼리 메서드에도 where나 attributes 같은 옵션을 사용할 수 있다.
const user = await User.findOne({
include: [{
model: Comment,
where: {
id: 1,
},
attributes: ['id'],
}]
});
// 또는
const comments = await user.getComments({
where: {
id: 1,
},
attributes: ['id'],
});
관계 쿼리 시 수정, 생성, 삭제는 조금 다른 부분이 있다.
관계 쿼리 메서드의 인수로 추가할 모델을 넣거나 아이디를 넣는다.
const user = await User.findOne({});
const comment = await Comment.create();
await user.addComment(comment);
// 또는
await user.addComment1(comment.id);
// 배열의 경우
const user = await User.findOne({});
const comment1 = await Comment.create();
const comment2 = await Comment.create();
await user.addComment([comment1, comment2]);
예제 코드
환경 설정
먼저 작업할 Sequelize 폴더를 만들고 init 작업을 진행한다.
npm init
// package.json
{
"name": "sequelize",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
그리고 시퀄라이즈에 필요한 패키지를 설치한다.
npm i express morgan nunjucks sequelize sequelize-cli mysql2
sequelize-cli 패키지는 시퀄라이즈 명령어를 실행하기 위핸 패키지 이다.
mysql2는 MySQL과 시퀄라이즈를 이어주는 드라이버 패키지이다.
설치 완료 후 작업 폴더에서 시퀄라이즈 init 작업을 진행한다.
전역 설치 없이 명령어로 사용하려면 앞에 npx를 붙여 사용한다.
npx sequelize init
Sequelize CLI [Node: 16.14.2, CLI: 6.5.2, ORM: 6.25.8]
Created "config\config.json"
Successfully created models folder at "D:\Nodej.s\Sequelize\models".
Successfully created migrations folder at "D:\Nodej.s\Sequelize\migrations".
Successfully created seeders folder at "D:\Nodej.s\Sequelize\seeders".
작업 폴더 확인 시 config, models, migrations, seeders 폴더가 생성된 것을 볼 수 있다.
먼저 models 안에 index.js 파일을 보면 sequelize-cli가 자동으로 생성해주는 코드가 있다.
필요 없는 부분이 많으므로 수정한다.
const Sequelize = require('sequelize');
// 현재 환경에 따른 데이터베이스 정보 설정
const config = require('../config/config')['development'];
const db = {};
// 시퀄라이즈 객체 생성
const sequelize = new Sequelize(config.database, config.username, config.password, config);
db.sequelize = sequelize;
module.exports = db;
Sequelize는 시퀄라이즈의 패키지이자 생성자로, config/config.json에서 데이터베이스 설정을 불러온 후 new Sequelize를 통해 MySQL 연결 객체를 생성한다.
그리고 config 폴더 안의 config.json 정보를 수정해야 한다.
config.json 파일의 정보는 MySQL과 연동할 때 사용되는 정보로, "development"의 값을 MySQL 설정으로 변경한다.
{
"development": {
"username": "root",
"password": "0000",
"database": "testmysql",
"host": "127.0.0.1",
"dialect": "mysql"
...
}
MySQL 연결하기
먼저 루트 폴더에서 app.js를 생성하고 익스프레스와 시퀄라이즈를 연결한다.
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const nunjucks = require('nunjucks');
const { sequelize } = require('./models');
const indexRouter = require('./routes');
const usersRouter = require('./routes/users');
const commentsRouter = require('./routes/comments');
const app = express();
app.set('port', 3000);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
// foce 옵션은 서버 실행 시마다 테이블을 재생성 할지의 여부
sequelize.sync({ force: false })
.then(() => {
console.log("데이터베이스 연결 성공");
})
.catch((err) => {
console.error(err);
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('./comments', commentsRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트 시작');
});
모델 정의하기
MySQL에서 정의한 테이블을 시퀄라이즈에서도 정의해주어야 한다. 여기서 테이블은 시퀄라이즈의 모델과 대응된다.
일반적으로 시퀄라이즈의 모델 이름은 단수형, 테이블 이름은 복수형으로 사용한다.
const Sequelize = require('sequelize');
const User = require('./user');
const Comment = require('./comment');
// 현재 환경에 따른 데이터베이스 정보 설정
const config = require('../config/config')['development'];
const db = {};
// 시퀄라이즈 객체 생성
const sequelize = new Sequelize(config.database, config.username, config.password, config);
db.sequelize = sequelize;
db.User = User;
db.Comment = Comment;
User.init(sequelize);
Comment.init(sequelize);
User.associate(db);
Comment.associate(db);
module.exports = db;
// user.js
const Sequelize = require('sequelize');
// User 모델 생성
module.exports = class User extends Sequelize.Model {
static init(sequelize) {
return super.init({
// 데이터베이스 테이블 설정
name: {
type: Sequelize.STRING(20),
allowNull: false,
unique: true,
},
age: {
type: Sequelize.INTEGER.UNSIGNED,
allowNull: false,
},
married: {
type: Sequelize.BOOLEAN,
allowNull: false,
},
comment: {
type: Sequelize.TEXT,
allowNull: true,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
},
}, {
// 테이블 옵션 설정
sequelize,
timestamps: false,
underscored: false,
modelName: 'User',
tableName: 'users',
paranoid: false,
charset: 'utf8',
collate: 'utf8_general_ci',
});
}
// 모델과의 관계 설정
static associate(db) {}
//static associations(db) {}
};
// comment.js
const Sequelize = require('sequelize');
// Comment 모델 생성
module.exports = class Comment extends Sequelize.Model {
static init(sequelize) {
return super.init({
// 데이터베이스 테이블 설정
comment: {
type: Sequelize.STRING(100),
allowNull: false,
},
create_at: {
type: Sequelize.DATE,
allowNull: true,
defaultValue: Sequelize.NOW,
},
}, {
sequelize,
timestamps: false,
modelName: 'Comment',
tableName: 'comments',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
//static associations(db) {}
static associate(db) {}
};
User의 모델을 만들고 모듈로 exports 했다. User 모델은 Sequelize.Model을 확장한 클래스로 선언한다.
모델은 크게 static init 메서드와 static associate 메서드로 나뉜다.
init 메서드는 테이블에 대한 설정을 하고, associate 메서드에서는 다른 모델과의 관계를 적는다.
먼저 init 메서드를 보면 super.init 메서드의 첫 번째 인수가 테이블 커럼에 대한 설정이고,
두 번째 인수가 테이블 자체에 대한 설정이다.
시퀄라이즈는 알아서 id를 기본 키로 연결하므로 id 컬럼은 적어줄 필요가 없다.
나머지 컬럼의 스펙을 입력하는데, MySQL 테이블의 컬럼 내용과 정확하게 일치해야 대응된다.
여기서 User 모델의 내용을 보면 MySQL의 자료형과 다르다.
MySQL | Sequelize |
VARCHAR(100) | STRING(100) |
INT | INTEGER |
TINYINT | BOOLEAN |
DATETIME | DATE |
INT UNSIGNED | INTEGER.UNSIGNED |
NOT NULL | allowNull: false |
UNIQUE | unique: true |
DEFAULT now() | defaultValue: Sequelize.Now |
super.init 메서드의 두 번째 인수는 테이블 옵션이다.
옵션 | 설명 |
sequelize | static init 메서드의 매개변수와 연결되는 옵션으로 db.sequelize 객체를 넣어야 함 |
timestamps | true로 설정 시 createAt과 updateAt 컬럼이 추가되며 각각 로우가 생성될 때와 수정될 때의 시간이 자동으로 입력됨 |
underscored | 캐멀 케이스(EcreatedAt)에서 스네이크 케이스(create_at)로 바꾸는 옵션 |
modelName | 모델 이름을 설정 |
tableName | 실제 데이터베이스의 테이블 이름으로 보통 소문자 및 복수형으로 만듦 (User -> users) |
paranoid | true로 설정 시 deleteAt이라는 컬럼이 생기는데, 로우를 삭제할 때 완전히 지워지지 않고 deleteAt에 지운 시간이 기록됨 |
charset | 문자 코드에 대한 집합으로 한글을 사용하기 위해 utf8로 설정해야 함 |
collate | 문자 정렬에 대한 집합으로 한글을 사용하기 위해 utf8_general_ci로 설정해야 함 |
실행 결과
전체 코드
https://github.com/JeHeeYu/NodeJ.S/tree/main/Sequelize
출처 : Node j.s 교과서
'Web > Node.js' 카테고리의 다른 글
[Node.js] MySQL CRUD 예제 (0) | 2022.11.27 |
---|---|
[Node.js] MySQL 예제 (0) | 2022.11.27 |