기존 데이터를 json형태로 service로직과 함께 저장하였다면 이번엔 파일형식 데이터베이스인 sqlite를 이용해 데이터베이스 관리를 진행할 예정
시퀄라이즈는 객체를 통해 데이터베이스에 대한 C.R.U.D 액세스 권한을 가질 수 있도록 하는 모듈이다.
ORM : Object - Relational - Mapping
시퀄라이즈 객체를 데이터베이스 테이블과 매핑, 객체 매서드를 이용해 데이터베이스를 관리할 수 있는 방식을 의미한다.
// terminal
npm i sequelize
// source code
// Sequelize 클래스
const Sequelize = require('sequelize');
// sequelize 객체 : 생성자 이용
const sequelize = new Sequelize(database: string, username: string, password: string, options: object)
/* constructor.options
-options.host : 데이터베이스 호스트
-options.port : 데이터베이스 포트
-options.username : 데이터베이스의 인증된(authenticated) 사용자
.
.
.
-options.dialect : 연결하려는 데이터베이스. sqlite || mysql || mssql
-options.storage : 파일동작 방식인 sqlite에서만 사용함. 데이터가 저장될 파일생성 위치를 의미함
(default=memory)
.
.
.
-options.logging : 로깅을 찍을지 말지 결정. (default=true)
... 등등 많다.
*/
doc 참조 : sequelize.org
// 상기 내용의 연장
// db.sync
sequelize.sync({ options... });
/* sync.options
-force : 기존 테이블을 덮어 쓴다(totally new)
-alter : 기존 테이블을 정의된 모델(테이블)에 맞게 수정한다.
-match : 일종의 세이프티 옵션. 필터링 역할을 해준다.
*/
sequelize.sync({ force: true });
sequelize.sync({ alter: true });
// 파일명 끝이 _test로 끝나는 것만 필터링
sequelize.sync({ force: true, match: /_test$/ });
// db.drop
// 마찬가지 파일명 끝이 _test로 끝나는 것만 필터링
sequelize.drop({ match: /_test$/ });
sequelize.drop 결과
파일이 삭제되진 않고 비워졌다. empty!!!!!!!!!
// 상기 내용의 연장
// db.define
const MyTable = sequelize.define(tablename, { field options... })
// options detail
const MyTable = sequelize.define(tableName, {
field1: {
type: Sequelize.STINRG || INTEGER ...
primaryKey: true || false(default)
autoIncrement: true || false
allowNull : true || false
.
.
.
}
field2: ...
field3: ...
.
.
.
});
시퀄라이즈 클래스 인스턴스를 통해
를 하였다. 한마디로 표현하면 '특정 스키마(형식)를 가진 파일형식의 데이터테이블에 접근권한을 갖게 됨'이 되겠다.
그렇다면 접근은 어떻게 해야할까?
handler.js
const models = require('./models');
// 생성 핸들러
exports.createSqlite = async function(tableName, data) {
const result = await tableName.create(data);
return result;
};
// 겟 핸들러(단일 로우)
exports.getSqlite = async function(tableName, queryParams, atr ) {
const result = await tableName.findOne({ where: queryParams, attributes: atr});
return result;
};
// 겟 핸들러(다수 로우)
exports.getSqlites = async function(tableName, atr, lim, queryParams) {
const result = await tableName.findAll({attributes: atr, limit: lim, where: queryParams });
return result;
};
// 업데이트 핸들러
exports.updateSqlite = async function(tableName, newData, queryParams) {
await tableName.update(newData, { where: queryParams });
const result = await tableName.findOne({ where: queryParams });
return result;
};
// 삭제 핸들러
exports.deleteSqlite = async function(tableName, queryParams) {
await tableName.destroy({ where: queryParams });
return true;
};
해당 핸들러를 이용해 우리가 정의한 데이터베이스 테이블에 접근하여 생성 / 삭제 / 추가 / 수정의 작업을 진행 할 수 있다. 이를 일전 만들어 둔 tdd의 service로직에 적용하여 보자.
service.js
// 기존 더미데이터를 가볍게 삭제해준다.
const models = require('../../models');
// sqlite DB handler
const sqliteHandler = require('../../handler');
// service_method : getPlayer
exports.getPlayers = async(req, res) => {
if (req.query.limit === undefined) limit = players.length;
else limit = parseInt(req.query.limit, 10);
if(Number.isNaN(limit)) return res.status(400).end();
res.status(200);
const attributes = ['club', 'name', ['age', 'how old is he?']]; // field값 제한
const result = await sqliteHandler.getSqlites(models.Player, attributes, limit)
res.json(result).end();
};
// service_method : getMvpPlayer
exports.getMvpPlayer = async (req, res) => {
const final_mvp = parseInt(req.params.final_mvp, 10);
console.log(final_mvp);
if (Number.isNaN(final_mvp)) return res.status(400).end();
const queryParams = {
final_mvp
}
const attributes = ['club', 'name', ['final_mvp', `who's the best?`]];
// const player = players.filter( player => player.final_mvp === mvp);
const result = await sqliteHandler.getSqlite(models.Player, queryParams, attributes);
res.json(result).end();
};
// service_method : deletePlayer
exports.deleteMvpPlayer = async (req, res) => {
const mvp = parseInt(req.params.mvp, 10);
if (Number.isNaN(mvp)) res.status(400).end();
queryParams = {
mvp
}
const result = await sqliteHandler.deleteSqlite(models.Player, queryParams);
res.status(204).json(result).end();
};
// service_method : createPlayer
exports.createPlayer = async (req, res) => {
const { club, name, age, salary, final_mvp } = req.body;
if( !name ) {
console.log('파라미터가 부족합니다');
res.status(400).end();
}
const data = {
club,
name,
age: Number(age),
salary: Number(salary),
final_mvp: Number(final_mvp)
};
const queryParams = {
name
}
const NameConflictPlayer = await sqliteHandler.getSqlite(models.Player, queryParams);
if (NameConflictPlayer) return res.status(409).end();
const result = await sqliteHandler.createSqlite(models.Player, data);
res.status(201);
res.json(result).end();
};
// service_method : editPlayer
exports. editPlayer = async (req, res) => {
const { id } = req.params;
const findQueryParams = {
id
}
const { club, name, age, salary, final_mvp } = req.body;
const nameQueryParams = {
name
}
const data = {
club,
name,
age,
salary,
final_mvp
}
const tableName = models.Player;
if (typeof club != 'string' || !name) return res.status(400).end(); // 이름이 없거나 클럽명이 숫자인 경우
const AlreadyExistPlayer = await sqliteHandler.getSqlite(tableName, findQueryParams); // key값에 해당하는 데이터 존재여부 파악
if ( !AlreadyExistPlayer ) return res.status(404).end(); // 데이터 미존재
const CheckNameConflict = await sqliteHandler.getSqlite(tableName, nameQueryParams); // key값에 해당하는 데이터 존재여부 파악
if ( CheckNameConflict ) return res.status(409).end(); // 이름 이미 존재하는 경우
const result = await sqliteHandler.updateSqlite(tableName, data, findQueryParams);
console.log(result);
res.json(result).end();
};
// 데이터베이스 테이블을 ORM으로 추상화한 것을 모델이라 하며 이 모델을 통해 각종 매서드들에 접근하는 것이다.
// 모델을 정의하고(sequelize.define()) -> 정의된 모델과 데이터베이스를 연동한다. (sequelize.sync())
// create
definedModelName.create(data);
// 테이블에 데이터 내용 로우 생성
// update
definedModelName.update(editData, { where: queryParams });
// 조건에 해당하는 로우들을 editData로 수정한다.
// delete
definedModelName.delete({ where: queryParams })
// get
definedModelName.findOne( options: { where: queryParams, attribute: attr})
// 조건에 해당하는 하나의 로우의 attribute조건에 해당하는 필드만 겟한다.
definedModelName.findAll( options: { attribute: attr, limit: lim, where: queryParams})
// 조건에 해당하는 하나 이상의 로우의 attribute조건에 해당하는 필드를, 데이터 수 가 최대limit개수 이하가 되게끔 겟한다.
tdd test code
.spec.js
const request = require('supertest');
const app = require('../../server');
const should = require('should')
const models = require('../../models');
describe('GET : /players는', () => {
before( () => models.sequelize.sync({}));
describe('성공 시', () => {
it('player list를 반환한다.', (done) => {
request(app)
.get('/players')
.end((err, res) => {
if(err) throw err;
res.body.should.be.instanceOf(Array);
done();
});
});
it('최대 limit갯수만큼 응답한다.', (done) => {
request(app)
.get('/players?limit=3')
.end((err, res) => {
if (err) throw err;
res.body.should.have.lengthOf(3)
done();
});
});
});
describe('실패 시', () => {
it('limit이 숫자형이아니면 400을 응답한다.', (done) => {
request(app)
.get('/players?limit=three')
.expect(400, done);
/* .end((err, res) => {
if (err) throw err; // expect에서 발생하는 error가 end매서드로 던져진 것. 따라서 throw처리를 하지 않으면 에러가 처리되지 않음.
done();
});
*/
});
});
});
describe('GET : /players:mvp 는', () => {
describe('성공 시', () => {
it('mvp가 2인 객체를 반환한다.', (done) => {
request(app)
.get('/players/2')
.end((err, res) => {
if(err) throw err;
res.body.should.have.property(`who's the best?`, 2);
done();
});
});
});
});
describe('DELETE : /players/:mvp는', () => {
before( () => models.sequelize.sync({}));
describe('성공 시', () => {
it('상태코드 204를 반환한다.', (done) => {
request(app)
.delete('/players/6')
.expect(204)
.end((err, res) => {
done();
});
});
});
describe('실패 시', () => {
it('mvp가 숫자가 아니면 상태코드 400을 반환한다.', async () => {
await request(app)
.delete('/players/four')
.expect(400)
});
});
});
describe('POST : /players 는', () => {
before( () => models.sequelize.sync({}));
describe('성공 시', () => {
let body;
before(done => {
request(app)
.post('/players')
.send({
club: 'pistons',
name: 'cunningham',
age: 19,
salary: 10,
final_mvp: 0
})
.expect(201)
.end((err, res) => {
if (err) throw err;
body = res.body;
done();
});
});
it('입력한 이름을 반환한다.', () => {
body.should.have.property('name', 'cunningham');
});
it('생성된 유저객체를 반환한다.', () => {
body.should.be.instanceOf(Object); // 객체를 반환한다.
});
});
describe('실패 시', () => {
it('이름 누락 시 400을 반환한다.', done => {
request(app)
.post('/players')
.send({
club: 'cleveland',
age: 33,
salary: 24,
final_mvp: 0
})
.expect(400)
.end(done);
});
it('name이 중복일 경우 409를 반환한다.', done => {
request(app)
.post('/players')
.send({
club: 'celtics',
name: 'durant',
age: 33,
salary: 33,
final_mvp: 0
})
.expect(409)
.end(done);
});
});
});
describe.only('PUT : /players 은', () => {
before( () => models.sequelize.sync({})); // sequelize와 동기화(return 으로 비동기 처리)
describe('성공 시', () => {
data1 = {
club: 'jazz',
name: 'ingles',
age: 29,
salary: 20,
final_mvp:0,
}
it('변경된 정보를 응답한다.', done => {
request(app)
.put('/players/1')
.send(data1)
.end((err, res) => {
if (err) throw err;
res.body.should.have.property('name', 'ingles');
done();
});
});
});
describe('실패 시', () => {
data2 = {
club: 3,
name: 'Russ',
age: 32,
salary: 42,
final_mvp: 0
}
it('문자열이 아닌 club일 경우 400을 응답', done => {
request(app)
.put('/players/1')
.send(data2)
.expect(400)
.end(done);
});
data3 = {
club: 'lakers',
age: 23,
salary: 30,
final_mvp: 0
}
it('name이 없을 경우 400 응답', done => {
request(app)
.put('/players/1')
.send(data3)
.expect(400)
.end(done);
});
data4 = {
club: 'GSW',
name: 'curry',
age: 33,
salary: 25,
final_mvp: 0
}
it('조건에 맞는 선수가 없을 경우 404 응답', done => {
request(app)
.put('/players/100')
.send(data4)
.expect(404)
.end(done);
});
data5 = {
club: 'lakers',
name: 'doncic',
age: 31,
salary: 25,
final_mvp: 0
}
it('이름이 중복일 경우 409 응답', done => {
request(app)
.put('/players/2')
.send(data4)
.expect(409)
.end(done);
});
});
});
테스트 코드의 내용은 앞선 포스트와 완전이 일치하니 참고하길 바람.