Object Relational Mapping의 약자이다. Model을 기술하는 도구이다.
개발 환경에서 데이터베이스에 접근할 수 있도록 하는 중간자 역할이라고 생각하자.
비동기 기반의 Node.js ORM이다. 실제로 async
, await
비동기가 기본으로 쓰인다.
나는 이번 Sprint를 통해서,
등을 해보았다.
1) 공식문서에 나와있는 대로, 먼저 sequelize를 설치한다.
// Sequlize를 설치하자
npm install --save sequelize
2) Migration을 하려면 sequelize-cli도 설치해야 한다.
우리가 작성한 모델이 실제로 database에 존재하도록 하려면 migration 작업이 꼭 필요하다.
npm install --save-dev sequelize-cli
1) 프로젝트를 시작하기 전, 아래 명령어를 통해 우리는 여러 준비 파일을 생성할 수 있다.
npx sequelize-cli init
2) 자동으로 생성되는 4개의 파일 중 config 폴더에는 작업 환경에 대한 정보가 담겨 있다. 기본적으로 development
환경이 설정이 되어있고, mysql
을 사용하는 것으로 되어 있다.
// config/config.json 파일
{
"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"
}
}
// models/index.js에 보면 아래처럼 development가 기본으로 쓰이고 있다는 걸 알 수 있다.
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
3) 데이터 베이스 생성하기
// 아래 명령어를 입력하면 현재 기본으로 설정된 데이터베이스(database_development)가 자동으로 생성된다.
db:create
1) url 모델을 먼저 생성하자. model:generate
를 통해 모델과 마이그레이션 파일을 만들 수 있다.
// name과 attribute를 어떻게 설정하는지 관찰하자
npx sequelize-cli model:generate --name url --attributes
url:string,title:string,visits:integer
// 필드의 다른 속성을 추가할 수 있다. 변경 전,
visits: DataTypes.INTEGER
// 변경 후,
visits: {
type: DataTypes.INTEGER,
defaultValue: 0
}
point: dataValues를 따로 설정해 주지 않으면 각 컬럼의 기본값은 NULL
이다.
2) 마이그레이션 파일을 살펴보자.
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('urls', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
url: {
type: Sequelize.STRING
},
title: {
type: Sequelize.STRING
},
visits: {
type: Sequelize.INTEGER,
defaultValue: 0
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('urls');
}
};
테이블 이름이 우리가 모델에서 설정한 url
과 달리 urls
로 복수형이다. 아래 내용 참고하자.
A model in Sequelize has a name. This name does not have to be the same name of the table it represents in the database. Usually, models have singular names (such as
User
) while tables have pluralized names (such asUsers
), although this is fully configurable.
3) 마이그레이션 해보자.
마이그레이션을 진행하면 실제 mysql의 database-development
데이터베이스에 url이라는 테이블이 생성되는 것을 확인할 수 있다. (undo를 진행하면 테이블 사라진다)
// # 마이그레이션 진행하기
npx sequelize-cli db:migrate
// # 마이그레이션 파일 수정이 필요한 경우, 마이그레이션 해제하기
npx sequelize-cli db:migrate:undo
1) app.js 파일에 router 파일을 연결하자.
const express = require('express');
const logger = require('morgan');
const indexRouter = require('./routes/index');
const linksRouter = require('./routes/links');
const app = express();
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/', indexRouter);
app.use('/links', linksRouter);
module.exports = app;
2) routes/links.js 파일에 라우터 내용을 작성하자. 각 endpoint에 따라 분기할 메소드의를 연결해 주어야 한다.
const express = require('express');
const router = express.Router();
const controller = require('../controllers/links');
router.get('/', controller.get); // get 메소드 연결
router.post('/', controller.post); // post 메소드 연결
router.get('/:id', controller.getId) // get 메소드 중 redirect를 위한 연결
module.exports = router;
controller는 작성되어 있는 url
model를 통해 데이터베이스의 정보를 조회 또는 작성할 예정이다.
그럼 우선 model를 가져오자.
const model = require("../../models").url;
point: 그런데 왜 const model = require("../../models/url")
이라고 작성하면 안 되는 걸까?
사실 url은 ‘models/index.js’ 즉, Model이라는 클래스의 하나의 instance이다.
{
"url": "https://www.github.com"
}
{
"id": 1,
"url": "https://www.github.com",
"title": "The world’s leading software development platform · GitHub",
"visits": 1,
"createdAt": "2020-07-25T20:07:15.000Z",
"updatedAt": "2020-07-25T20:07:15.000Z"
}
여기에서 id, createdAt, updatedAt은 자동으로 생성된다. url, title, visits를 레코드로 urls 테이블에 전달해야 한다.
1) url은 req.body를 통해 획득할 수 있으니 pass
2) title은 다른 방법으로 만들어야 한다. sprint에 힌트가 나와있다.
modules/utils.js에 이미 구현되어 있는 두 메소드를 활용할 예정이다.
const {getUrlTitle, isValidUrl} = require('../../modules/utils')
isValidUrl
의 경우 url 검사를 한번 해주는 것 같고, getUrlTitle
은 title을 생성해 주는 것 같다.
const request = require('request');
const rValidUrl = /^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i;
exports.getUrlTitle = (url, cb) => {
request(url, function (err, res, html) {
if (err) {
console.log('Error reading url heading: ', err);
return cb(err);
} else {
const tag = /<title>(.*)<\/title>/;
const match = html.match(tag);
console.log(match.length)
const title = match ? match[1] : url;
return cb(err, title);
}
});
};
exports.isValidUrl = url => {
return url.match(rValidUrl);
};
// controller에 작성한 post 요청 작업의 식은 아래와 같다.
// 1. 먼저 url 유효성 검사를 하고, 유효하지 않은 경우 400을 반환하자
// 2. 유효한 url의 경우 가져온 getUrlTitle 메소드를 활용하여 얻은 title을 반환하자
post: async (req, res) => {
const url = req.body.url;
if (!isValidUrl(url)) return res.stauts(400).send('Not Found')
// getUrlTitle 두 번째 인자는 콜백함수였다. 콜백함수에 우리가 작업하고자 하는 식을 넣어 주자.
getUrlTitle(url, async (err, title) => {
if (err) return res.sendStatus(400);
// sequelize API 문서를 통해 "조회 또는 생성"하는 문법을 찾았다.
// title 값을 받아옴
// title을 url 모델의 title 값으로 => 레코드 생성
// 만약에 이미 해당 url이 데이터베이스에 존재한다면 => 레코드 조회
model.findOrCreate({
where: {
url
},
defaults: {
title
}
})
.then(([result, created]) => {
if (created) {
return res.status(201).json(result); // created
} else {
return res.status(201).json(result); // find
}
})
.catch((err) => req.sendStatus(500))
})
}
model.findOrCreate는 특정 옵션에 해당하는 엔트리를 찾지 못한 경우, 옵션 내용 + defaults에 들어간 내용으로 레코드를 생성하게 된다. 그것이 result 객체로 생성이 된다. (다른 곳에서 생성한 getUrlTitle 함수를 가져와서 그 인자에 콜백함수 형식으로 쿼리문을 실행하는 것을 이해하기 굉장히 오래 걸렸다)
3) visits에는 기본값이 있으니 pass
post보다 어렵지 않았다. findAll()
은 SQL syntax인 select * from
과 동일하게 작동한다.
get: async (req, res) => {
try {
const result = await model.findAll();
return res.status(200).json(result)
} catch (err) {
return res.sendStatus(500);
}
}
1) model.findByPk를 통해 params로 전달된 id값으로 레코드를 찾는다.
2) 방문 횟수 증가를 위해 model.increment를 사용한다. { {늘리고자 하는 필드: 늘리는 수의 크기} , {where : 조건}}
3) redirect를 구현하기 위해 Express 문법을 사용한다.
getId: async (req, res) => {
// id라는 파라미터
// 해당 파라미터를 이용해서 url 모델엘 조회를 해야함
const id = req.params.id;
const target = await model.findByPk(id);
await model.update({visits: visits + 1};
if (!target) {
res.sendStatus(204);
} else {
const url = target.url;
target.update({
visits: target.visits + 1
})
try {
res.status(302).redirect(url)
} catch (err) {
res.sendStatus(500)
}
}
npx sequelize-cli model:generate --name user --attributes
name:string,email:string,age:integer
migration skeleton
파일을 하나 생성하여 기존 테이블을 업데이트할 수 있다
npx sequelize-cli migration:generate --name url-skeleton
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('urls','userId',Sequelize.INTEGER);
{
type: Sequelize.INTEGER,
references: {
model: 'users',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
}
)
},
down: async (queryInterface, Sequelize) => {
return queryInterface.removeColumn(
'urls',
'userId'
)
}
};