2022. 5. 8. 09:53ㆍ책/Node.js 교과서
1. ORM이란? (Object Relational Mapping, 객체-관계 매핑)
OOP(Object Oriented Programming)에서 쓰이는 객체와 RDB(Relational DataBase)에서 쓰이는 테이블을 자동으로 매핑(연결)하는 것을 뜻한다. 애초에 둘은 서로 호환될 것을 염두에 두고 만든 것이 아니기 때문에, ORM을 통해 객체 간의 관계를 바탕으로 SQL문을 자동으로 생성하여 간접적으로 데이터베이스를 조작하는 것이다.
npm i sequelize sequelize-cli mysql2 패키지를 설치하고 본격적으로 시작해보자.
※ sequelize-cli
패키지를 설치하다보면 ○○○○-cli 라는 모듈도 설치할 때가 있다. 이러한 애들은 명령줄 인터페이스(CLI; Command Line Interface) 기반으로 동작하는 노드 프로그램인데, 쉽게말해 콘솔 창에서 명령어를 입력해서 프로그램을 수행하는 모듈이라고 생각하면 된다.
그리고 sequelize-cli는 시퀄라이즈로 만들어준 DB세팅을 '실제로' 만들어내는 명령어 모음집이라고 생각하면 될 듯.
Sequelize CLI [Node: 10.21.0, CLI: 6.0.0, ORM: 6.1.0]
sequelize <command>
Commands:
sequelize db:migrate Run pending migrations
sequelize db:migrate:schema:timestamps:add Update migration table to have timestamps
sequelize db:migrate:status List the status of all migrations
sequelize db:migrate:undo Reverts a migration
sequelize db:migrate:undo:all Revert all migrations ran
sequelize db:seed Run specified seeder
sequelize db:seed:undo Deletes data from the database
sequelize db:seed:all Run every seeder
sequelize db:seed:undo:all Deletes data from the database
sequelize db:create Create database specified by configuration
sequelize db:drop Drop database specified by configuration
sequelize init Initializes project
sequelize init:config Initializes configuration
sequelize init:migrations Initializes migrations
sequelize init:models Initializes models
sequelize init:seeders Initializes seeders
sequelize migration:generate Generates a new migration file [aliases: migration:create]
sequelize model:generate Generates a model and its migration [aliases: model:create]
sequelize seed:generate Generates a new seed file [aliases: seed:create]
Options:
--version Show version number [boolean]
--help Show help [boolean]
Please specify a command
2. 시퀄라이즈 시작하기
2.1 프로젝트 시작(npx sequelize init)
노드프로그램을 시작할 때 npm init으로 기본 설정을 세팅한것처럼, 시퀄라이즈를 실행하기 위한 기본 설정을 세팅한다. 명령어를 돌리면 4개의 폴더가 생성되는데 각각 아래와 같은 역할을 한다.
- config : 데이터베이스 설정 파일, 사용자 이름, DB 이름, 비밀번호 등의 정보 들어있다.
- migrations : git과 비슷하게 데이터베이스의 변화 과정을 추적해나가는 정보로, 실제 데이터베이스에 반영할 수도 있고 변화를 취소할 수도 있다.
- models : 데이터베이스 각 테이블의 정보 및 필드타입을 정의하고 하나의 객체로 모은다.
- seeders : 테이블에 기본 데이터를 넣고 싶은 경우에 사용한다.
2.2 DB 기본 설정 (config.json 설정)
데이터베이스를 사용하는데 필요한 가장 기본적인 정보들을 모아둔 파일이다. 최초 생성시 아래와 같은 상태이며, 각자 상황에 맞는 정보들로 채워넣으면 된다. 참고로 .json파일은 ESM 모듈에서 그대로 import가 불가능하다 (Node.js 17버전 이후부턴 asertion을 통해 되는 것 같지만 현재 작성자는 16버전을 쓰고있다)
{
"development": {
"username": "root",
"password": null,
"database": "database_development",
"host": "127.0.0.1",
"dialect": "mysql"
},
"test": {
"username": "root",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql"
}
}
만약 기본 상태처럼 환경설정 값들이 그대로 노출되는게 싫다면 확장자를 변경 후 dotenv를 사용해 은닉화할 수 있다. 작성자는 아래 형태로 진행할 예정.
import 'dotenv/config'
export default {
development: {
"username": process.env.MYSQL_USERNAME,
"password": process.env.MYSQL_PASSWORD,
"database": process.env.MYSQL_DATABASE,
"host": process.env.MYSQL_HOST,
"dialect": "mysql"
},
test: {
...
},
production: {
...
}
};
2.3 DB 모델 정의(models 폴더 설정)
models 폴더는 DB모델, 그러니까 테이블과 그 안의 필드들을 정의한 파일들을 모아두는 폴더이다 (시퀄라이즈에서 모델이란 MySQL의 테이블과 대응되는 개념)
모델을 따로 정의해줘야 하는 이유는 JS에 MySQL을 투영해 DB작업을 수행하기 위함이다. 즉, DB라는 분리된 공간에 있는 테이블과 동일한 모델을 만들어 JS상으로 투영, 연결하여 자바스크립트로 쿼리문을 수행할 수 있게 되는 것.
프로젝트 최초 시작시점에선 index.js 말고는 아무것도 없지만 이후 User.js, Comment.js 등 필요한 DB 모델들을 정의해 모아둘 예정이다. 그런데 index.js는 뭘까?
1) index.js
위에서 언급했지만 프로젝트를 진행하면서 User.js, Comment.js 등 필요한 DB모델들을 정의한다고 했다. 이렇게 따로 정의한 모델들을 모두 긁어모아 하나의 객체에 담아주고, 모델간 관계를 설정해 DB를 실제로 사용하기 위한 준비를 해준다.
특히, 아래 코드에서 sequelize객체를 생성하고 db.sequelize에 담아서 export하는데 이게 곧 DB연결을 위한 진입점 역할을 한다. 정리하면 model/index.js란 생성된 모델을 취합 및 연결하고 export해 DB접근을 위한 진입점 역할을 하는 파일이다. 참고로 최초 생성당시의 코드를 그대로 쓰기엔 무리가 있어서 따로 만들어준다.
# new Sequelize( database, username, password, options )
- databse : 연결할 데이터베이스명
- username : 데이터베이스 인증시 사용될 사용자이름
- password : 데이터베이스 인증시 사용될 비밀번호
- options : 기타 옵션 객체. 연결할 DB의 종류를 포함해 이거저거 설정해줄 수 있다. { dialect: "mysql" }처럼 연결할 DB의 종류(dialect)를 반드시 지정해줘야 한다. 말은 옵션이면서 사실상 필수 파라미터인셈 ㅡㅡ
import { Sequelize } from 'sequelize';
import config from '../config/config.js'
// 개발자모드로 환경설정
const env = process.env.NODE_ENV || 'development';
const db = {};
const sequelize = new Sequelize(
config[env].database,
config[env].username,
config[env].password,
config[env]
);
db.sequelize = sequelize;
export default db;
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
let sequelize;
// config.json에서 DB의 기본 설정들을 가져와 sequelize 객체를 생성한다
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
// 따로 만들어준 DB모델들을 가져와 하나의 객체안에 담아준다
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});
// DB모델간 관게를 연결한다
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
2.3.1 모델 정의하기
이제 진짜로 모델을 정의해보자. 모델을 정의할 때 하는 일은 크게 두 가지로, 테이블 생성과 모델 간 관계를 설정이다. 두 작업 모두 Model 클래스를 활용한다.
1) 테이블 생성
# Model.init( attributes, options )
- attributes : 테이블 설정을 위한 필드 정의
- options : 연결할 시퀄라이즈 객체와 기타 옵션(timestamps, underscored 등등)을 지정한다
DB상의 테이블을 나타내줄 모델을 생성 및 초기화한다.
테이블 생성시 Model 클래스를 extend하여 모델을 선언하고, static 메서드로 초기화 및 관계 설정을 해준다. 이 때 특이한 점이 3개 정도 있는데 한번 짚고 넘어가보자.
- 모델을 class 문법으로 만들지만 어떤 객체를 생성하기 위함은 아니다(즉, new User( ) 뭐 이런거 안함) 그저 만들어준 static메서드만 불러오면서 거의 일반 함수처럼 사용한다. (물론 인스턴스 메서드도 있는데 지금 당장엔 쓸 일이 없음)
- 시퀄라이즈에서 쓰는 자료형은 SQL의 자료형과 조금씩 다르므로 테이블 속성 지정시 주의해야 함.
- 컬럼 작성시 시퀄라이즈는 알아서 id를 기본 키로 연결하므로 id 컬럼은 굳이 작성할 필요가 없다.
model/user.js
import { DataTypes, Model } from "sequelize"
class User extends Model {
static init(sequelize) {
return super.init({
name: {
type: DataTypes.STRING(20),
allowNull: false,
unique: true,
},
age: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
},
married: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
comment: {
type: DataTypes.TEXT,
allowNull: true,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
}
}, {
sequelize,
timestamps: false,
underscored: false,
modelName: 'User',
tableName: 'users',
paranoid: false,
charset: 'utf8',
collate: 'utf8_general_ci',
});
}
// 모델 관계 설정. 뒤에서 설명예정
static associate(db) {
db.User.hasMany(db.Comment, { foreignKey: 'commenter', sourceKey: 'id' });
}
};
export default User;
※ 시퀄라이즈 자료형
자료형 선언시 필요한 것들은 DataTypes모듈에 다 담겨있음.
MySQL | 시퀄라이즈 |
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 |
※ 테이블 옵션
timestamps | ROW(레코드) 생성과 수정 시간을 기록하기 위한 속성. true로 설정시 createdAt과 updatedAt 컬럼을 추가해 해당 시간을 자동으로 기록한다. |
underscored | 테이블 생성시 카멜 케이스(camel case)를 스네이크 케이스(snake case)로 변환한다. (Default: false = 즉 테이블명과 컬럼명을 createdAt 과 같은 방식으로 저장함) |
modelName | 모델 이름 설정. 보통 대문자 단수형으로 작성 (ex: User) |
tableName | 실제 DB의 테이블 이름. 보통 소문자 복수형으로 작성 (ex: users) |
paranoid | 레코드 삭제 시간을 기록하기 위한 속성. true로 설정하면 deletedAt이라는 컬럼을 만들고 레코드 삭제시 실제로 제거하는게 아닌, 지운 시각을 기록해 추후 조회되지 않도록 함. |
이 외에도 진짜 엄청 많음;; |
2) 모델 관계 설정
말그대로 모델 간의 관계를 설정하는 것으로 1:1, 1:N, N:M 관계로 분류된다. SQL에선 JOIN이란 기능으로 여러 테이블 간의 관계를 파악하는데, 시퀄라이즈의 경우 테이블 간 관계를 설정해주면 JOIN기능을 알아서 구현해준다.
- 1:1
예를 들어, 유저 한 명과 그 정보를 담고있는 하나의 테이블과 같은 관계를 말한다
# hasOne( target [, options] )
belongsTo( target [, options] )
- target : 관계를 설정해줄 다른 모델
- options : 소스키, 외래키 등 관계 설정에 필요한 옵션| - 1:N
유저 한명이 남긴 여러개의 댓글과 같은 관계
# hasMany( target [, options] )
belongsTo( target [, options] ) - N:M
한 게시글에 달린 여러개의 해시태그, 한 해시태그에 연관된 여러개의 게시글
# belongsToMany( target [, options] )
belongsToMany( target [, options] )
model/user.js
class User extends Model {
static init(sequelize) {
...
}
static associate(db) {
db.User.hasMany(db.Comment, { foreignKey: 'commenter', sourceKey: 'id' });
}
};
model/comment.js
class Comment extends Model {
static init(sequelize) {
...
}
static associate(db) {
db.Comment.belongsTo(db.User, { foreignKey: 'commenter', targetKey: 'id' });
}
}
모델간 관계를 설정해주는 일은 매우 중요하다. 예를 들어, 인스타에 가서 '존잘남'을 검색했다고 치자. 애플리케이션은 '존잘남' 이란 유저 한명을 표현하기 위해 얼마나 많은 데이터들을 불러올까?
- '존잘남' 유저 1명이 쓴 여러개의 게시글
- '존잘남' 유저를 팔로잉하는 여러명의 유저들
- '존잘남' 유저가 팔로우하는 여러명의 유저들
- '존잘남' 유저가 쓴 여러개의 게시글
- 각 게시글에 달린 여러개의 해시태그
당장 생각나는 것만 적어도 꽤 많은 데이터들을 불러오고, 이 데이터들은 각자 나름대로의 관계를 가지고 있다. 따라서 모델간 관계를 잘 설정해줘야 필요한 정보들을 누락없이 불러올 수 있는 것이다.
관계 설정에 관한 부분은 따로 정리할테니 그 부분도 꼭 읽어보자.
2.3.2 모델 반영하기(index.js)
이제 모델을 위한 클래스도 다 만들었고, 그 안에 모델을 생성해주는 메서드(init)와 관계를 설정해주는 메서드(associate)도 다 만들어줬다. 이제 남은 일은 이 모델들을 실제로 '생성'하고 하나의 객체에 담아 사용할 준비를 끝마치기만 하면 된다.
즉, model/index.js 파일에서 선언한 db라는 객체에 담아주기만 하면 이후 서버파일에선 이 db를 import해와 입맛대로 쿼리를 수행할 수 있다는 뜻. 말은 길게 했지만 실제 작업은 매우 간단하다.
import { Sequelize } from 'sequelize';
import config from '../config/config.js'
import User from './user.js';
import Comment from './comment.js';
...
db.sequelize = sequelize;
// db객체에 각 모델들을 담고
db.User = User;
db.Comment = Comment;
// init메서드로 모델을 '실제로 생성'한 뒤
User.init(sequelize);
Comment.init(sequelize);
// associate메서드로 모델간 관계까지 설정해주면
// 진짜 준비 끝!
User.associate(db);
Comment.associate(db);
export default db;
2.4 express와 연결하기
드디어 모델생성과 반영이 끝났다. 남은 일은 서버(즉, express)와 db를 연결하기만 하면 끝이다!
# Sequelize.sync( options ) : db를 연결한다
import express from 'express';
import path from 'path';
import morgan from 'morgan';
import db from './models/index.js';
const app = express();
app.set('port', process.env.PORT || 3001);
// db를 연결하자!!
try {
await db.sequelize.sync({ force: false })
console.log('데이터베이스 연결 성공');
} catch (err) {
console.error(err);
}
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// 에러 핸들러는 서버코드의 맨 마지막에!!
app.use((req, res, next) => {
const err = new Error(`${req.method} ${req.url} 라우터가 없습니다`);
err.status = 400;
next(err);
});
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500);
res.send('error')
});
app.listen(app.get('port'), () => console.log(app.get('port'), '번 포트에서 대기 중'));
3. 쿼리 수행
모델을 만들고 사용하기 쉽게 db객체에 담아 export까지 했으니 남은 일은 실제로 DB에 데이터를 조작하는 일만 남았다. 즉 쿼리를 수행하면 된다는 뜻인데, CRUD작업시 SQL과의 문법 차이가 있으니 이를 먼저 살펴보자. (사실 SQL 문법을 그대로 갖다 쓸 수도 있는데 그럼... 굳이 시퀄라이즈를 쓰는 이유가 없잖아;;)
추가로 시퀄라이즈에서의 쿼리는 프로미스를 반환하므로 then이나 async/await 문법을 사용해 결과값을 받을 수 있다.
3.1 CRUD
3.1.1 C(reate)
# Model.create( [values] [, options] )
- values : 컬럼에 실제로 넣어줄 값
- options : fields, ignoreDuplicates, returning, validate 옵션이 있는데 나중에 필요해지면 직접 검색 ㄱㄱ
INSERT INTO noddej.users (name, age, married, comment) VALUES ('zero', 24, 0, '자기소개1');
import db from './models/index.js';
db.User.create({
name: 'zero',
age: 24,
married: false, //SQL에선 0이지만 시퀄라이즈 자료형 기준으론 boolean이니까
comment: '자기소개1',
});
3.1.2 R(ead)
# Model.findAll( [findOptions] ) : 조건에 맞는 모든 레코드를 검색
- findOptions : 조회시 필요한 조건들을 지정해주는 객체. 어떤 컬럼을 가져올 건지는 attributes 옵션을 사용하고, 조건을 설정할땐 where 옵션을 사용한다. 특히, SQL에서 지정할 수 있는 조건이 AND OR부터 시작해서 이거저거 많은데, 시퀄라이즈도 그 기능들을 수행하기 위해 Op라는 특별한 객체를 지원한다.
# Model.findOne( [findOPtions] ) : 조건에 맞는 하나의 레코드만 검색
[1] 전체 레코드 조회
SELECT * FROM nodejs.users;
db.User.findAll({ });
[2] 하나의 레코드 조회
SELECT * FROM nodejs.users LIMIT 1;
db.User.findOne({})
ㅇ특정 컬럼 조회(attribute)
SELECT name, married FROM nodejs.users;
db.User.findAll({
attributes: ['name', 'married'],
})
ㅇ조건에 맞는 특정 컬럼 조회(where)
SELECT name, married FROM nodejs.users WHERE married = 1 AND age > 30;
db.User.findAll({
attributes: ['name', 'married'],
where: {
married: true,
age: { [Op.gt]: 30 },
}
})
ㅇ조건에 맞는 특정 컬럼 조회(where, Op객체)
SELECT name, married FROM nodejs.users WHERE married = 0 OR age > 30;
db.User.findAll({
attributes: ['name', 'married'],
where: {
[Op.or] : [{ married: true }, { age: { [Op.gt]: 30 } }]
}
})
※ Op객체 알아보기
Op.gt | 초과 | Op.gte | 이상 | Op.lt | 미만 | Op.lte | 이하 |
Op.or | OR | Op.in | 배열요소 중 하나 | Op.notIn | 배열 요소와 모두 다름 |
ㅇ정렬(order)
SELECT id, name FROM users ORDER BY age DESC;
db.User.findAll({
attributes: ['id', 'married'],
order: [ ['age', 'desc'] ]
})
ㅇ정렬+limit+offset
SELECT id, name FROM users ORDER BY age DESC LIMIT 30 OFFSET 20;
db.User.findAll({
attributes: ['id', 'married'],
order: [ ['age', 'desc'] ],
limit: 30,
offset: 20,
})
3.1.3 U(pdate)
#Model.update( values [, options] )
- values : 수정할 컬럼과 값
- options : 정확히 어떤 레코드를 삭제할 것인지
UPDATE nodejs.users SET comment = '이렇게 바꾸고 싶은뎅' WHERE id=2;
db.User.update(
{ comment : '이렇게 바꾸고 싶은뎅', },
{ where: { id: 2 }, }
)
3.1.4 D(elete)
# Model.destroy( [options] )
DELETE nodejs.users WHERE id = 2;
db.User.destroy({
where: { id: 2 }
});
3.2 관계 쿼리
CRUD만으로도 매우 큰 기능이지만 시퀄라이즈는 관계 쿼리(SQL로 따지자면 JOIN) 기능까지도 제공한다. 예를 들어 위에서 User모델은 Comment 모델과 hasMany - belongsTo(1:N) 관계를 맺고 있다. 이 상황에서 특정 사용자를 가져오면서 그 사람의 댓글까지 모두 가져오고 싶다면?
# include
FindOptions 속성중 하나로, JOIN을 이용해 관계를 맺고있는 테이블에 접근할 수 있게 해준다.
// user테이블에서 관계를 맺고있는
// Comment 테이블 정보를 갖고오기
const user = await db.User.findOne({
// 관계맺고있는 테이블이 여러개일 수 있으니
// 배열로 전달해줌
include: [ { model: db.Comment } ]
});
console.log(user.Comments);
// 당연히 where, attributes같은 조건들도 넣어줄 수 있음
const user = await db.User.findOne({
include: [{
model: db.Comment,
attribues: [ 'id', 'comment' ], // 'id'와 'comment'컬럼 정보를 가져올건데
where: { id: 1, }, // id가 1인 레코드를 가져와주셈
}]
});
근데 생각해보면 이렇게 할 필요가 없는게, User 모델을 정의하면서 associate메서드로 Comment 모델과의 관계를 지정해줬다. 이미 지정된 마당에 include고 나발이고 이런게 필요할까? 당연히 필요없다. 이전에 관계를 잘 정의해준게 있고, 그에 맞는 메서드도 제공하고 있으니 잘 쓰기만 하면 된다.
// model/user.js
class User extends Model {
...
// 여기서 이미 관계 다 정의해줬는데?
// include로 어떤 테이블이랑 관계가있는지 나발인지가 왜필요함 ㅡㅡ
static associate(db) {
db.User.hasMany(db.Comment, { foreignKey: 'commenter', sourceKey: 'id' });
}
};
// app.js
// 그냥 잘 정의된걸 갖다 쓰기만하면 된다.
const user = await db.User.findOne({});
const comments = await user.getComments();
console.log(comments);
이전에 관계를 잘 정의해뒀으면 아래 메서드들을 제공하는데, 그냥 갖다 쓰기만 하면 된다.
- getComments(조회)
- setComments(수정)
- addComment(1개 생성)
- addComents(여러개 생성)
- removeComments(삭제)
주의사항은 getComments 외 나머지는 메서드 사용시 살짝 형태가 다르긴한데, 지금 당장 알 필요는 없으니 넘어가기로 하자.
마지막으로 SQL문을 직접 쓸 수도 있다. 왠만하면 시퀄라이즈 쿼리를 사용하는게 낫겠지만, 시퀄라이즈 쿼리로 불가능한 경우엔 SQL문을 쓰도록 하자.
const [result, metadata] = await db.sequelize.query('SELECT * from comments');
console.log(result);
'책 > Node.js 교과서' 카테고리의 다른 글
[8장] mongoDB (0) | 2022.05.22 |
---|---|
[7장] MySQL/ORM 관계설정 (0) | 2022.05.22 |
[7장] MySQL (0) | 2022.05.07 |
[6장] Express / Router (0) | 2022.05.07 |
[6장] Express/multer (0) | 2022.05.06 |