테스트주도개발(TDD)로 만드는 NodeJS API 서버 - 006
데이터베이스
데이터베이스 소개
SQL
- MySQL, PostgreSQL, Aurora, Sqlite
NoSQL : json 형식
- MongoDB, DynamoDB
In Memory DB
- Redis, Memcashed
ORM 소개
쿼리
- insert table(‘name’) value(‘alice’);
- select * from users;
- update users set name=’beck’ where id=1;
- delete from users where id=1;
ORM(Object Relational Mapping)
- 데이터베이스를 객체로 추상화해 놓은 것
- 쿼리를 직접 작성하는 대신 ORM 의 메소드로 데이터 관리할 수 있음
- 노드에서 SQL ORM은 시퀄라이져(Sequelize)가 있음
노드의 ORM 시퀄라이져
쿼리문 대신 메소드를 사용
- insert table(‘name’) value(‘alice’);
-> User.create({name: ‘alice’}) - select * from users;
-> User.findAll(); - update users set name=’beck’ where id=1;
-> User.update({name: ‘beck’}, {where:{id:1}}); - delete from users where id=1;
-> destory({where:{id:1}});
User
는 모델임
모델
- 데이터베이스 테이블을 ORM 으로 추상화한 것
- User 모델
- sequelize.define(): 모델 정의
- sequelize.sync() : 데이터베이스 연동
모델 정의
sequelize, sqlite3 설치
# sequelize, sqlite3 설치
$ npm i sequelize sqlite3 --save
node-api/models.js
// node-api/models.js
const { Sequelize, Model, DataTypes } = require('sequelize');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './db.sqlite'
});
class User extends Model { }
User.init({
name: DataTypes.STRING,
}, { sequelize, modelName: 'user' });
module.exports = { Sequelize, Model, User, sequelize };
데이터베이스 - ORM 동기화
node-api/bin/sync-db.js
const { Sequelize, Model, User, sequelize } = require('../models');
// 원래의 DB를 삭제하고 새로 생성
sequelize.sync({ force: true });
module.exports = () => {
return sequelize.sync({ force: true });
}
sync-db 실행
$ node bin/sync-db.js
node-api/bin/www.js
const app = require('../index');
const syncDb = require('./sync-db');
syncDb().then( () => {
console.log('Sync database!');
app.listen(3000, () => {
console.log('Server is running on 3000 port');
});
});
데이터베이스와 index 컨트롤러 연동 1
API 로직인 user.ctrl.js 에서 모델을 연동하여 디비를 연결
describe, it 에 only() 함수로 해당 테스크 테이스만 실행할 수 있음
node-api/api/user/user.spec.js
// node-api/api/user/user.spec.js
const app = require('../../index.js');
const request = require('supertest');
const should = require('should');
const { User, sequelize } = require('../../models');
describe('GET /users는', () => {
describe('성공시', () => {
before(() => {
return sequelize.sync({ force: true }); // create table
});
it.only('유저 객체를 담은 배열로 응답한다', (done) => {
...
})
})
})
node-api/api/user/user.ctrl.js
// node-api/api/user/user.ctrl.js
const { User } = require('../../models');
const index = (req, res) => {
req.query.limit = req.query.limit || 10;
const limit = parseInt(req.query.limit, 10);
if (Number.isNaN(limit)) {
return res.status(400).end();
}
User.findAll({
limit: limit
}).then(users => {
res.json(users);
});
}
...
데이터베이스와 index 컨트롤러 연동 2
데이터베이스 초기화 (샘플 데이터 등록)
only 이동
node-api/api/user/user.spec.js
...
const app = require('../../index');
const models = require('../../models');
describe.only('GET /users는', () => {
describe('성공시', () => {
const users = [
{ name: 'alice' },
{ name: 'beck' },
{ name: 'chris' },
]
before(() => {
return sequelize.sync({ force: true }); // create table
});
before(() => {
return User.bulkCreate(users);
});
it('유저 객체를 담은 배열로 응답한다', (done) => { // only 로 해당 테스크 테이스만 실행할 수 있음
...
})
})
})
node-api/api/user/user.ctrl.js
// node-api/api/user/user.ctrl.js
const { User } = require('../../models');
const index = function(req, res) {
req.query.limite = req.query.limit(2);
const limit = parseInt(req.query.limit, 10);
if(Number.isNaN(limit)) {
return res.status(400).end();
}
User.findAll({
limit: limit
})
.then(users => {
res.json(users);
})
}
테스트 결과에 쿼리문 로그를 숨김
node-api/models.js
...
const sequelize = new Seqielize({
dialect: 'sqlite',
storage: './db.sqlite',
logging: false, //console.log
})
...
데이터베이스와 show 컨트롤러 연동
only 이동
node-api/api/user/user.spec.js
...
describe.only('GET /users/:id는', () => {
...
})
...
node-api/api/user/user.ctrl.js
// node-api/api/user/user.ctrl.js
const { User } = require('../../models');
const index = function(req, res) {
...
}
const show = (req, res) => {
const id = parseInt(req.params.id, 10);
if (Number.isNaN(id)) {
return res.status(400).end();
}
User.findOne({
where: {
id: id
}
}).then(user => {
if (!user) return res.status(404).end();
res.json(user);
})
}
...
데이터베이스와 destroy 컨트롤러 연동
only 이동
node-api/api/user/user.spec.js
...
describe.delete('DELETE /users/:id는', () => {
...
})
...
node-api/api/user/user.ctrl.js
// node-api/api/user/user.ctrl.js
const { User } = require('../../models');
const index = function(req, res) {
...
}
const show = function(req, res) {
...
}
const destroy = (req, res) => {
const id = parseInt(req.params.id, 10);
if (Number.isNaN(id)) {
return res.status(400).end();
}
User.destroy({
where: {
id: id
}
}).then(() => {
res.status(204).end();
});
}
...
데이터베이스와 create 컨트롤러 연동
only 이동
각 상위 describe 에 before 복사
node-api/api/user/user.spec.js
// node-api/api/user/user.spec.js
...
describe.only('POST /users', () => {
const users = [
{name: 'alice'},
{name: 'beck'},
{name: 'chris'},
]
before(( ) => {
return models.sequelize.sync({force: true}); // create table
});
before((done) => {
return models.User.bulkCreate(users); // insert data
});
...
})
...
user.name 에 unique 설정
node-api/models.js
// node-api/models.js
const { Sequelize, Model, DataTypes } = require('sequelize');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './db.sqlite',
logging: false,
});
class User extends Model { }
User.init({
name: {
type: DataTypes.STRING,
unique: true
}
}, { sequelize, modelName: 'user' });
module.exports = { Sequelize, Model, User, sequelize };
node-api/api/user/user.ctrl.js
// node-api/api/user/user.ctrl.js
const models = require('../../models');
const index = function(req, res) {
...
}
const show = function(req, res) {
...
}
const destroy = (req, res) => {
...
}
const create = (req, res) => {
const name = req.body.name;
if (!name) {
return res.status(400).end();
}
User.create({ name }).then((user) => {
return res.status(201).json(user);
}).catch((err) => {
return res.status(409).end();
});
}
...
데이터베이스와 update 컨트롤러 연동
마지막 테스트케이스이므로 only 삭제
before 복사
node-api/api/user/user.spec.js
// node-api/api/user/user.spec.js
...
describe('PUT /users/:id', () => {
const users = [
{name: 'alice'},
{name: 'beck'},
{name: 'chris'},
]
before(( ) => {
return models.sequelize.sync({force: true}); // create table
});
before((done) => {
return models.User.bulkCreate(users); // insert data
});
...
});
...
node-api/api/user/user.ctrl.js
// node-api/api/user/user.ctrl.js
const models = require('../../models');
const index = function(req, res) {
...
}
const show = function(req, res) {
...
}
const destroy = (req, res) => {
...
}
const create = (req, res) => {
...
}
const update = (req, res) => {
const id = parseInt(req.params.id, 10);
if (Number.isNaN(id)) return res.status(400).end();
const name = req.body.name;
if (!name) return res.status(400).end();
User.findOne({
where: { id }
}).then((user) => {
if (!user) return res.status(404).end();
user.name = name;
user.save().then(() => {
res.json(user);
}).catch((err) => {
if (err.name === 'SequelizeUniqueConstraintError') {
return res.status(409).end();
}
});
});
}
module.exports = {
index: index,
show: show,
destroy: destroy,
create: create,
update: update,
};
끗
마무리
실제 데이터 확인
# 기존 데이터 삭제
$ rm db.sqlite
postman 을 통해 테스트 진행
데이터베이스가 test 상에서만 강제로 초기화되도록 설정
node-api/sync-db.js
// node-api/sync-db.js
const models = require('../models');
module.exports = () => {
const options = {
force: process.env.NODE_ENV === 'test' ? true : false
};
return models.sequelize.sync(options);
}