[SOPT] # 4th Seminar - 데이터 베이스, Sequelize ORM

✨New Wisdom✨·2020년 11월 10일
3

📗 Nodejs 📗

목록 보기
20/20
post-thumbnail

데이터 베이스

관련성을 가지며 중복이 없는 데이터의 집합을 의미한다.
이런 데이터 베이스를 관리하는 시스템을 DBMS(Database Management System)라고 부른다.

관계형 데이터 베이스 (Relational Database)

key와 value들의 간단한 관계를 테이블화 시킨 데이터 베이스

🚩 데이터 베이스 용어 정리

Table
Attribytes (속성)
column 값을 의미한다.
Tuple (행)
row 값을 의미한다.
Primary Key (기본키)
다른 데이터와 구별할 수 있는 식별자
한 테이블에는 하나 혹은 그 이상의 Primary Key가 있어야 한다.
Not null, unique 한 값이다.
Index
데이터의 색인 기능
관계형 데이터 베이스에서 검색 속도를 높이기 위한 도구이다.
unique 해서 특정 데이터를 대표하는데 사용한다.
(주로 기본키로 사용한다)
Foreign Key (외래키)
한 테이블의 column 중 다른 테이블의 Row를 식별할 수 있는 Key

🚩 Relation (관계)

1 : 1 관계 (일대일 관계)
한 쪽 대체가 다른 쪽 개체가 반드시 하나로 매핑되어 있어야 한다.
ex) 이름과 주민번호
1 : N 관계 (일대다 관계)
한 쪽 개체가 관계를 맺은 개체 쪽의 여러 객체를 가질 수 있는 것을 의미한다.
🚫데이터 중복 가능성이 있다🚫
ex) 이름과 이메일
M : N 관계 (다대다 관계)
양쪽 개체 모두에서 1 : N 관계가 존재할 때 나타나는 모습.
즉 서로가 서로를 1 : N 관계로 보고 있다.
🚫데이터 중복 가능성이 있다🚫
ex) 이름과 스터디

🚩 정규화

관계형 데이터 베이스를 논리적이고 직관적으로 만드는 과정

정규화를 하는 목적

  • 불필요한 데이터 제거
  • 데이터 저장을 논리적으로
  • 이상 현상 방지
    🤔 이상 현상?
  • 삽입 이상 : 새 데이터를 삽입하기 위해 불필요한 데이터도 함께 삽입해야 하는 문제
  • 갱신 이상 : 중복 튜플 중 일부만 변경하여 데이터가 불일치하게 되는 모순의 문제
  • 삭제 이상 : 튜플을 삭제하면 꼭 필요한 데이터까지 함께 삭제되는 데이터 손실의 문제

1차 정규화

각 로우마다 컬럼의 값이 1개씩만 있어야 한다.
= 컬럼이 원자 값을 갖는다.
아직 이상현상이 발생할 수 있다.

2차 정규화

1차 정규화를 만족하고 테이블의 모든 컬럼이 완전 함수적 종속을 만족하는 것
기본키 중에 특정 컬럼에만 종속된 컬럼(부분적 종속)이 없어야 한다.
모든 테이블이 기본키에 함수적 종속
아직 이상현상이 발생할 수 있다.

3차 정규화

2차 정규화를 만족하고 이행적 함수 종속을 제거

역 정규화

정규화된 스키마는 입력, 삭제, 수정할 때 관계를 맺고 있는 다른 테이블에서 JOIN을 이용한다.
저장된 자료가 많으면 JOIN 시 성능이 떨어진다.
역 정규화라는 시스템의 퍼포먼스를 위해 정규화에 위배되는 행위를 하는 것

Sequelize ORM

NodeJS에서 자바스크립트의 객체와 데이터베이스의 릴레이션을 매핑해주는 라이브러리
SQL을 사용하지 않고 자바스크립트로 데이터베이스에 접근하여 데이터를 Access
SQL을 사용하면 커넥션풀을 생성해서 SQL으로 CREATE를 사용하지만
Sequelize는 ORM을 사용해서 CREATE한다.

ORM

객체와 관계형 데이터베이스의 데이터를 자동으로 매핑(연결)해주는 것을 말한다.
객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이터베이스는 테이블을 사용한다.

🚩 실습해봅시다

초기 설정

모듈 설치

npm install -g sequelize-cli sequelize mysql2

Sequelize 프로젝트 생성

sequelize init

구조 뜯어보기 🛠

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"
  }
}
  • 데이터 베이스의 정보를 담는 공간
  • 개발, 테스트, 프로적션 3가지 배포 환경에 따라 정의하고 사용이 가능하다.

model/index.js

'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;
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);
}

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;
  });

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;
  • 데이터 베이스의 테이블을 정의하는 곳
  • 실제 MySQL 테이블과 Sequelize의 모델은 대응된다.

Migrations

테이블 컬럼을 추자/제거 할 때 Migrations 파일을 통해서 실제 DB에 반영한다.
Migrations 파일을 통해 Rollback도 가능하다.

Seeders

시드 데이터를 생성하는 곳, 주로 샘플 데이터를 생성하고 지울 때 (테스트 환경) 사용한다.

Config 설정

"development": {
    "username": "admin",
    "password": "~비밀번호~",
    "database": "SOPT_27",
    "host": "db-sopt-server.ctmbn9ymizbs.ap-northeast-2.rds.amazonaws.com",
    "dialect": "mysql"
  },

models/index.js 수정

처음에 생성된 내용 지우고 다음 내용을 적는다.

const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config.json')[env];
const db = {};
let 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.sequelize = sequelize; db.Sequelize = Sequelize;
module.exports = db;

app.js 추가

const { sequelize } = require('./models');
sequelize.sync({ alter: false })
.then(() => {
  console.log('데이터베이스 연결 성공.');
})
.catch((error) => {
  console.error(error);
})

성공시

Sequelize 모델 정의

Sequelize에서 모델이란?
데이터 베이스 테이블의 스키마를 표현하는 수단!
sequelize.define('모델이름',{/*스키마*/},{/*스키마옵션*/});
위를 이용해서 정의한다!

models/user.js

const { sequelize } = require(".");

module.exports = (sequelize, DataTypes) =>{
    return sequelize.define('User',{
        // 모델의 attributes (column)
        email : {
            type : DataTypes.STRING(30),
            unique : true,
            allowNull : false,
        },
        userName :{
            type: DataTypes.STRING(20),
            allowNull: false,
        },
        password: {
            type: DataTypes.STRING(200),
            allowNull: false,
        },
        salt: {
            type: DataTypes.STRING(200),
            allowNull: false,
        },
    },{
        // 모델의 옵션
        freezeTableName : true, // true면 모델명과 테이블 이름을 동일하게 설정함.
        timestamps : false,
    })
}

데이터 타입 표기법

MySQLSequelize
VARCHAR(100)STRING(100)
INTINTEGER
TINYINTBOOLEAN
DATETIMEDATE
NOT NULLallowNull : false
UNIQUEunique : true
DEFAULT now()defaultValue : Sequelize.NOW

❗️Sequelize에서 PK를 지정해 주지 않으면 자동으로 id라는 컬럼의 PK 값이 생성된다❗️

여러 모델 옵션

  • freezeTableName : true면 모델명과 테이블 이름을 동일하게 설정함. Sequelize는 기본적으로 모델 이름은 단수, 테이블 이름은 복수로 설정한다.
  • timestamps : default 값이 true이다. 모델을 정의하고 테이블이 생성되면 자동적으로 createdAt, updatedAt 컬럼이 생성된다.
  • tableName: 실제 데이터 베이스의 테이블 이름을 설정한다.
  • paranoid: true로 설정하면 deletedAt 이라는 컬럼이 생김. 로우를 삭제하게되면 실제 데이터는 삭제되지 않고 deletedAt 컬럼에 지운 시간이 기록되고 deletedAt 컬럼에 시간이 기록되면 Select할때는 집계되지 않는다.
  • underscored: 시퀄라이즈의 테이블명과 컬럼 명은 기본값이 카멜케이스(createdAt)다. true일 때 스네이크 케이스(created_at)로 변경된다.

User Model을 Sequezlie 객체에 연결

models/index.js

const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config.json')[env];
const db = {};
let 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.sequelize = sequelize; 
db.Sequelize = Sequelize;
db.User = require('./user')(sequelize,Sequelize);  // 추가
module.exports = db;


정의한 User 모델이 실제 데이터 베이스에 대응되어 테이블이 생성된다.
app.jssequelize.sync({ alter: false })부분에서 모델 동기화가 일어난다.

  • sequelize.sync({}) : 테이블이 존재하지 않는 경우 생성한다.(이미 존재하면 아무 작업 수행 ❌)
  • sequelize.sync({force : ture}) : 테이블이 생성되고 이미 존재하는 경우 먼저 삭제 후 생성한다.
  • sequelize.sync({alter : true}) : 데이터 베이스에 있는 테이블의 현재 상태(컬럼, 데이터 유형)을 확인한 후 테이블에서 필요한 변경을 수행하여 모델과 일치시킨다.

회원가입 - POST users/signup

router.post('/signup', async (req, res) => {
    console.log("dsf")
    // 1. req.body에서 데이터 가져오기
    const { email, password, userName } = req.body; 
    // 2. request data 확인하기, email, password, userName data가 없다면 NullValue 반환
    if(!email || !password || !userName){
        console.log('필요한 값이 없습니다.');
        return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE));
    }
    try {
        //3. 존재하는 이메일인지 확인하기. 이미 존재하는 이메일면 ALREADY ID 반환
        const alreadyEmail = await User.findOne({
            where : {
                email : email,
            }
        });
        if(alreadyEmail){
            console.log('이미 존재하는 이메일 입니다.');
            return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST,responseMessage.ALREADY_ID));
        }
        //4. salt 생성
        const salt = crypto.randomBytes(64).toString('base64');
        //5. 2차 세미나때 배웠던 pbkdf2 방식으로 (비밀번호 + salt) => 암호화된 password
        const hashedPassword = crypto.pbkdf2Sync(password,salt,10000,64,'sha512').toString('base64');
        //6. User email, 암호화된 password, salt, userName 생성!
        const user = await User.create({
            email : email,
            password : hashedPassword,
            userName : userName,
            salt : salt,
        });
        console.log(user)
        //7. status: 200 message: SING_UP_SUCCESS, data: id, email, userName 반환! (비밀번호, salt 반환 금지!!)
        return res.status(statusCode.OK).send(util.success(statusCode.OK,responseMessage.SIGN_IN_SUCCESS, {id : user.id, email,userName}));
    } catch(error){
        console.error(error);
        return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR,responseMessage.SIGN_IN_FAIL));
    }
})

로그인 - POST users/signin

router.post('/signin', async (req, res) => {
    // 1. req.body에서 데이터 가져오기
    const { email, password } = req.body; 
    // 2. request data 확인하기, email, password, userName data가 없다면 NullValue 반환
    if (!email || !password){
        console.log('필요한 값이 없습니다.');
        return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE));
    }
    try{
        // 3. 존재하는 아이디인지 확인하기. 존재하지 않는 아이디면 NO USER 반환
        const alreadyEmail = await User.findOne({
            where : {
                email : email,
            },
        });
        console.log(alreadyEmail);
        if (!alreadyEmail){
            console.log('없는 이메일 입니다.');
            return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST,responseMessage.NO_USER));
        }
        // 4. 비밀번호 확인하기 - 로그인할 email의 salt를 DB에서 가져와서 사용자가 request로 보낸 password와 암호화를 한후 디비에 저장되어있는 password와 일치하면 true
        // 일치하지 않으면 Miss Match password 반환
        const {id, userName, salt, password :hashedPassword} = alreadyEmail;
        const inputPassword = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('base64');

        if (inputPassword !== hashedPassword){
            console.log('비밀번호가 일치하지 않습니다.');
            return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.OK, responseMessage.MISS_MATCH_PW));
        }
        // 5. status: 200 ,message: SIGN_IN_SUCCESS, data: id, email, userName 반환
        return res.status(statusCode.OK).send(util.success(statusCode.OK,responseMessage.SIGN_IN_SUCCESS,{id, email, userName}));
    } catch(error){
        console.error(error);
        return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR,responseMessage.SIGN_IN_FAIL));
    }
})

성공

모든 사용자 조회 - GET user/

router.get('/', async (req, res) => {
    //1. 모든 사용자 정보 (id, email, userName ) 리스폰스!
    // status: 200, message: READ_USER_ALL_SUCCESS, data: id, email, userName 반환
    try {
        const users = await User.findAll({
            attributes : ['id', 'email', 'userName'],
        });
        console.log(users);
        return res.status(statusCode.OK).send(util.success(statusCode.OK,responseMessage.READ_USER_ALL_SUCCESS,users));
    } catch(error){
        console.error(error);
        return res.status(statusCode.INTERNAL_SERVER_ERROR).send(statusCode.INTERNAL_SERVER_ERROR,responseMessage.READ_USER_ALL_FAIL);
    }
})

특정 사용자 조회 - GET user/:id

router.get('/:id', async (req, res) => {
    // 1. parameter로 id값을 받아온다! (id값은 인덱스값)
    const {id} = req.params;
    // 2. id값이 유효한지 체크! 존재하지 않는 아이디면 NO_USER 반환
    try {
        const user = await User.findOne({
            where : {
                id : id,
            },
            attributes : ['id','email','userName'],
        });
        if(!user){
            console.log('존재하지 않는 아이디 입니다.');
            return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST,responseMessage.NO_USER));
        }
        // 3. status:200 message: READ_USER_SUCCESS, id, email, userName 반환
        return res.status(statusCode.OK).send(util.success(statusCode.OK,responseMessage.READ_USER_SUCCESS,user));
    } catch(error){
        console.error(error);
        return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, responseMessage.READ_USER_ALL_FAIL));
    }
})
  • where : 특정한 조건들을 만족하는 로우만 뽑아오는 옵션
  • attributes : 원하는 컬럼 값만 뽑아오는 옵션

Sequelize 쿼리

시퀄라이즈 쿼리를 이용해서 CRUD 작업을 진행한다.

ActionSQLSequelize Query
생성INSERTcreate
조회SELECTfind
수정UPDATEupdate
삭제DELETEdestroy
  • Sequelize는 자체적으로 promise를 반환하게 된다. 따라서 async/await 사용 가능

find

Sequelize 객체 내부의 Op 객체를 불러와 특수한 연산 가능
where 옵션 안에서 사용

  • Op.gt : 초과
  • Op.gte : 이상
  • Op.lt : 미만
  • Op.lte : 이하
  • Op.ne : 같지 않음
  • Op.or : 또는
  • Op.in : 배열 요소 중 하나
  • Op.notln : 배열 요소와 모두 다름

update

Model.update({/*수정할 내용*/}, {/*어떤 로우를 수정할지*/});

destroy

Model.destroy({/*삭제할 로우*/})
📚Sequelize 공식 문서📚

profile
🚛 블로그 이사합니다 https://newwisdom.tistory.com/

1개의 댓글

comment-user-thumbnail
2020년 11월 10일

냥이에 이끌려 왔다 ㅠㅠ

답글 달기