본문 바로가기
Javascript/Node.js

ORM(Sequelize) & 데이터베이스

by 모스키토끼 2020. 6. 15.

ORM이란?

  • 데이터베이스를 객체로 추상화한 것
  • 쿼리를 직접 작성하지 않고 ORM의 메서드로 데이터를 관리할 수 있음
  • 노드에서의 SQL ORM은 시퀄라이져(Sequelize)

메서드 예시)
- insert users ('name') values ('alice');
  User.create({name:'alice');

- select * from users;
 → User.findAll();

- update users set name = 'bek' where id = 1;
 → User.update({name:'bek'}, {where: {id:1}});

- delete from users where id = 1;
 → destroy({where: {id: 1}});

모델

  • 데이터베이스 테이블을 ORM으로 추상화한 것
  • 모델 정의: sequelize.define()
  • 데이터베이스 연동: sequelize.sync()

모델 정의 및 DB 연동 과정

  1. npm i sequelize sqlite3
    (ORM과 DB 모듈 추가) - 참고로 sqlite3는 경로에 한글이 포함되어 있으면 설치가 안됨
  2. models.js 파일 추가
        const Sequelize = require('sequelize');
        const sequelize = new Sequelize({
            dialect: 'sqlite',
            storage: './db.sqlite'
        });
    
        const User = sequelize.define('User', {
            name: Sequelize.STRING //varchar 255
        });
    
        module.exports = {Sequelize, sequelize, User};
    
    • require('sequelize')로 sequelize 모듈을 가져옴
    • sequelize = new Sequelize로 객체를 만듦 -> 디비 연동에 필요한 속성 정보 입력
    • sequelize.define으로 User라는 모델을 정의
    • 사용할 모델(User), sequelize, Sequelize exports!
  3. sync-db.js 추가
        const models = require('../models');
    
        module.exports = () =>{
            return models.sequelize.sync({force: true});
        }
    • 모델을 가져와 디비와 sync 하는 코드
    • 속성 값 force를 true를 주게 되면 기존에 DB가 존재하더라도 지우고 새로 만든다는 의미
  4. DB sync 시점 잡기
    • index.js가 starting point인 경우 index.js에서 sync-db를 실행해야 함
    • index.js가 app 역할을 하고 www.js에서 서버를 돌려주도록 한 경우 starting point는 www.js
      const app = require('../');
      //서버를 돌리기 전에 db 연동
      const syncDb = require('./sync-db');
      
      syncDb().then(_=>{
          console.log('Sync database!');
          app.listen(3000, ()=>{
              console.log("Server is running on 3000 port")
          });
      });
      

API - DB 연동

예시 코드

user.spec.js

//test 코드
const request = require('supertest');
const should = require('should');
const app = require('../../');
const models = require('../../models');

describe.only('GET /users는', () => {
    describe('성공시', () => {
        const users = [{name: 'alice'}, {name: 'bek'}, {name: 'chris'}];
        before(() => models.sequelize.sync({force: true}));
        before(() => models.User.bulkCreate(users));
        it('유저 객체를 담은 배열로 응답한다 ', (done) => {
            request(app)
                .get('/users')
                .end((err, res) => {
                    res
                        .body
                        .should
                        .be
                        .instanceOf(Array);
                    done();
                });
        });

        it('최대 limit 갯수만큼 응답한다 ', (done) => {
            request(app)
                .get('/users?limit=2')
                .end((err, res) => {
                    res
                        .body
                        .should
                        .have
                        .lengthOf(2);
                    done();
                });
        });
    });
    describe('실패시 ', () => {
        it('limit이 숫자형이 아니면 400을 응답한다', (done) => {
            request(app)
                .get('/users?limit=two')
                .expect(400)
                .end(done);
        });
    });

});
  • before(mocker)를 사용하여 db-sync를 해줌
    (before: it 실행되기 전에 실행되는 후커(hooker) 함수)
  • before에 done을 가지고 있는 콜백 함수를 넣어준 이유:
    - db-sync는 비동기로 작동하기 때문(파일 접근)
  • model.sequelize.sync({force: true}). then(_=> done());
    -> mocker에서는 Promise를 리턴하면 자동으로 비동기의 완료를 보장해줌
    -> before(()=> models.sequalize.sync({force: true}));
  • bulkCreate로 샘플데이터를 입력해줌
  • it.only: 이 테스트 메서드만 실행

user.ctrl.js

const models = require('../../models');

const index = function (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();
    }
    
    models.User
          	.findAll({
            	limit: limit
        	})
        	.then(users=>{
            	res.json(user);
        	})

    //res.json(users.slice(0, limit));
};
  • 파라미터로 받은 limit를 findAll의 limit 속성에 넣어줌 -> select 개수 제한
  • User테이블에 있는 정보를 모두 가져와 json형태로 응답

결과

※ sequalize의 logging 속성을 false로 주면 더 깔끔하게 볼 수 있다.

const sequelize = new Sequelize({
    dialect: 'sqlite',
    storage: './db.sqlite',
    logging: false,
});

그 외의 API 테스트

컨트롤러

//api 로직
const models = require('../../models');

const index = function (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();
    }

    models
        .User
        .findAll({limit: limit})
        .then(users => {
            res.json(users);
        })

    //res.json(users.slice(0, limit));
};

const show = function (req, res) {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) 
        return res.status(400).end();
    
    //const user = users.filter(user => user.id === id)[0];
    models
        .User
        .findOne({
            where: {id}
        })
        .then(user => {
            if (!user) 
                return res
                    .status(404)
                    .end();
            
            res.json(user);
        })

}

const destory = (req, res) => {
    const id = parseInt(req.params.id, 10);
    if (Number.isNaN(id)) 
        return res.status(400).end();

    //users = users.filter(user => user.id !== id);
    models.User.destroy({
        where: {id}
    }).then(() => {
        res.status(204).end();
    });
}

const create = (req, res) => {
    const name = req.body.name;
    if (!name) 
        return res.status(400).end();
    
    // const isConflict = users.filter(user => user.name === name).length
    // if (isConflict) 
    //     return res.status(409).end();
    
    // const id = Date.now();
    // const user = {
    //     id,
    //     name
    // };
    // users.push(user);
    models.User.create({name})
    .then(user =>{
        res.status(201).json(user);
    })
    .catch(err=>{
        if(err.name === 'SequelizeUniqueConstraintError'){
            return res.status(409).end();
        }
        res.status(500).end();
    })

}

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();
    
    // const isConflict = users.filter(user => user.name === name).length
    // if (isConflict) return res.status(409).end();
    
    // const user = users.filter(user => user.id === id)[0];
    // if (!user) return res.status(404).end();
    
    //user.name = name;

    models.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();
                    }
                    res.status(500).end();
                });
        });
}

module.exports = {
    // index:index, show:show, destory:destory, create:create, update:update,
    //ES6 문법
    index,
    show,
    destory,
    create,
    update
};

테스트 코드

//테스트 코드
const request = require('supertest');
const should = require('should');
const app = require('../../');
const models = require('../../models');

describe('GET /users는', () => {
    const users = [{name: 'alice'}, {name: 'bek'}, {name: 'chris'}];
    before(() => models.sequelize.sync({force: true}));
    before(() => models.User.bulkCreate(users));

    describe('성공시', () => {
        it('유저 객체를 담은 배열로 응답한다 ', (done) => {
            request(app)
                .get('/users')
                .end((err, res) => {
                    res
                        .body
                        .should
                        .be
                        .instanceOf(Array);
                    done();
                });
        });

        it('최대 limit 갯수만큼 응답한다 ', (done) => {
            request(app)
                .get('/users?limit=2')
                .end((err, res) => {
                    res
                        .body
                        .should
                        .have
                        .lengthOf(2);
                    done();
                });
        });
    });
    describe('실패시 ', () => {
        it('limit이 숫자형이 아니면 400을 응답한다', (done) => {
            request(app)
                .get('/users?limit=two')
                .expect(400)
                .end(done);
        });
    });

});
describe('GET /users/1는', () => {
    const users = [{name: 'alice'}, {name: 'bek'}, {name: 'chris'}];
    before(() => models.sequelize.sync({force: true}));
    before(() => models.User.bulkCreate(users));

    describe('성공시 ', () => {
        it('id가 1인 유저 객체를 반환한다', (done) => {
            request(app)
                .get('/users/1')
                .end((err, res) => {
                    res
                        .body
                        .should
                        .have
                        .property('id', 1);
                    done();
                });
        });
    });
    describe('실패시 ', () => {
        it('id가 숫자가 아닐 경우 400으로 응답한다.', (done) => {
            request(app)
                .get('/users/one')
                .expect(400)
                .end(done)
        });
        it('id로 유저를 찾을 수 없는 경우 404로 응답한다.', (done) => {
            request(app)
                .get('/users/999')
                .expect(404)
                .end(done)
        });
    });
});
describe('DELETE /users/1', () => {
    const users = [{name: 'alice'}, {name: 'bek'}, {name: 'chris'}];
    before(() => models.sequelize.sync({force: true}));
    before(() => models.User.bulkCreate(users));

    describe('성공시', () => {
        it('204를 응답한다', (done) => {
            request(app)
                .delete('/users/1')
                .expect(204)
                .end(done)
        });
    });
    describe('실패시', () => {
        it('id가 숫자가 아닌 경우 400으로 응답한다', (done) => {
            request(app)
                .delete('/users/one')
                .expect(400)
                .end(done)
        });
    });
});
describe('POST /users', () => {
    const users = [{name: 'alice'}, {name: 'bek'}, {name: 'chris'}];
    before(() => models.sequelize.sync({force: true}));
    before(() => models.User.bulkCreate(users));

    describe('성공시', () => {
        let name = 'bigring';
        let body;
        before(done => {
            request(app)
                .post('/users')
                .send({name})
                .expect(201)
                .end((err, res) => {
                    body = res.body;
                    done();
                })
        })
        it('생성된 유저 객체를 반환한다 ', () => { //비동기 테스트가 아니므로 done 필요x
            body
                .should
                .have
                .property('id');
        });
        it('입력한 name을 반환한다 ', () => {
            body
                .should
                .have
                .property('name', name)
        });
    });
    describe('실패시', () => {
        it('name 파라미터 누락시 400을 반환한다', (done) => {
            request(app)
                .post('/users')
                .send({})
                .expect(400)
                .end(done)
        });
        it("name이 중복일 경우 409를 반환한다", done => {
            request(app)
                .post('/users')
                .send({name: 'bigring'})
                .expect(409)
                .end(done)
        });
    });
});
describe('PUT /users/:id', () => {
    const users = [{name: 'alice'}, {name: 'bek'}, {name: 'chris'}];
    before(() => models.sequelize.sync({force: true}));
    before(() => models.User.bulkCreate(users));

    describe('성공시', () => {
        it('변경된 name을 응답한다', (done) => {
            const name = 'den';
            request(app)
                .put('/users/3')
                .send({name})
                .end((err, res) => {
                    res
                        .body
                        .should
                        .have
                        .property('name', name);
                    done();
                });
        });
    });

    describe('실패시', () => {
        it('정수가 아닌 id일 경우 400을 응답한다 ', (done) => {
            request(app)
                .put('/users/one')
                .expect(400)
                .end(done);
        });
        it('name이 없는 경우 400을 응답한다 ', (done) => {
            request(app)
                .put('/users/1')
                .expect(400)
                .end(done);
        });
        it('없는 유저일 경우 404을 응답한다 ', (done) => {
            request(app)
                .put('/users/999')
                .send({name: 'foo'})
                .expect(404)
                .end(done);
        });
        it('이름이 중복일 경우 409을 응답한다 ', (done) => {
            request(app)
                .put('/users/3')
                .send({name: 'bek'})
                .expect(409)
                .end(done);
        });
    });
});

모델

const Sequelize = require('sequelize');
const sequelize = new Sequelize({
    dialect: 'sqlite',
    storage: './db.sqlite',
    logging: false,
});

const User = sequelize.define('User', {
    name: {
        type: Sequelize.STRING, //varchar 255
        unique: true
    }
});

module.exports = {Sequelize, sequelize, User};
  • name속성에 unique를 주어 이름이 중복인 경우에 catch로 빠지게 하였다.

 

Reference

https://www.inflearn.com/course/%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%A3%BC%EB%8F%84%EA%B0%9C%EB%B0%9C-tdd-nodejs-api/

 

테스트주도개발(TDD)로 만드는 NodeJS API 서버 - 인프런

javascript 언어로 백엔드 개발을 할 수 있는 NodeJS 를 학습해 봅니다. 테스트 주도 개발 (TDD) 방법으로 NodeJS를 이용해 API 서버 개발을 배워보고 학습할 수 있는 강좌입니다. NodeJS의 대표적인 웹프레�

www.inflearn.com

 

'Javascript > Node.js' 카테고리의 다른 글

passport 적용  (2) 2020.07.08
SuperTest 연습  (0) 2020.06.11
테스트 주도 개발(TDD)  (0) 2020.06.10
익스프레스JS  (0) 2020.06.10
NodeJS의 특징  (0) 2020.06.09

댓글