ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Node.js 스터디 6주차
    BackEnd/Node.js 2022. 2. 22. 05:34

    Node.js 교과서 7장 MYSQL


    ✅ 데이터 베이스란? 

    - 관련성을 가지며 중복이 없는 데이터들의 집합

    - 데이터베이스를 관리하는 시스템 : DBMS

    - 관계형 데이터 베이스를 관리하는 시스템 : RDBMS

    - 관계형 데이터 베이스 : 데이터가 테이블 형태로 이루어져 있으며, 키와 값이라는 종속성을 관계로 표현하는 데이터베이스를 의미한다. Oracle, MySQL등이 대표적이다. 

     

    Create Schema `nodeStudy` DEFAULT CHARACTER SET utf8;
    use nodeStudy;

     

    📌 nodeStudy라는 이름의 데이터베이스를 형성하고, 사용하는 데이터베이스를 nodeStudy로 설정한다.

     

    📌 관련 명령어 

    - CREATE TABLE [데이터베이스명.테이블명] : 테이블 생성 명령어

    - DESC [테이블명] : 만들어진 테이블 확인

    - DROP TABLE [테이블명] : 테이블 제거

    - SHOW TABLES : 생성된 테이블 전체 확인

    CREATE TABLE nodeStudy.users ( 
        id INT NOT NULL AUTO_INCREMENT,
        name VARCHAR(20) NOT NULL, 
        age INT UNSIGNED NOT NULL,
        married TINYINT NOT NULL,
        comment TEXT NULL,
        created_at DATETIME NOT NULL DEFAULT now(),
        PRIMARY KEY(id),
        UNIQUE INDEX name_UNIQUE (name ASC))
        COMMENT = '사용자 정보'
        DEFAULT CHARACTER SET = utf8
        ENGINE = InnoDB;

    ✅ SQL 관련 자료형, 옵션 정리

    칼럼 관련 설정 테이블 자체 설정
    자료형 옵션 COMMENT : 테이블 관한
    보충설명, 필수 X
    INT : 정수형 NULL : NULL허용
    FLOAT , DOUBLE : 실수형 NOT NULL : NULL 허용 X
    VARCHAR(자릿수) : 가변 길이(0~자료형)  AUTO_INCREMENT : 숫자 자동 증가 DEFAULT CHARACTER SET : 한글 입력 위해 설정
    CHAR(자릿수) : 반드시 길이가 자릿수인 문자열 UNSIGNED : 음수 저장 X
    TEXT : 긴 글 (수백자 넘어가는 경우) ZEROFILL : 숫자의 자릿수 고정(0001처럼)
    TINYINT : 1바이트 정수, boolean처럼 사용 가능 PRIMARY KEY : 기본 키 설정 ENGINE : InnoDB
    DATETIME : 날짜 + 시간 UNIQUE INDEX : 고유한 값 설정
    DATE : 날짜, TIME : 시간 DEFAULT : 값이 없다면 기본값 자동 넣음

    🔍 InnoDB vs MyISAM

    트랜잭션이 많고 대용량 데이터 다룰 경우, 즉 데이터의 변화가 잦은 경우 InnoDB가 효율적

    트랜잭션 적고 read작업, 즉 select가 많은 경우 MyISAM이 효율적

     

    만들어진 테이블을 확인하면 위와 같다. 

     


    사용자의 댓글 저장용 테이블도 만든다. 

    CREATE TABLE nodeStudy.comments(
        id INT NOT NULL AUTO_INCREMENT,
        commenter INT NOT NULL,
        comment VARCHAR(100) NOT NULL,
        created_at DATETIME NOT NULL DEFAULT now(),
        PRIMARY KEY(id),
        INDEX commenter_idx (commenter ASC),
        CONSTRAINT commenter
        FOREIGN KEY (commenter)
        REFERENCES nodeStudy.users (id)
        ON DELETE CASCADE
        ON UPDATE CASCADE)
        COMMENT = '댓글'
        DEFAULT CHARSET=utf8mb4
        ENGINE = InnoDB;

    📌 FOREIGN KEY : 외래키

    - 다른 테이블의 기본 키를 저장하는 컬럼

    - CONSTRAINT [제약조건명] FOREIGN KEY [컬럼명] REFERENCES [참고하는 컬럼명]

     

    📌 CASCADE 옵션

    - ON UPDATE : 수정 되었을 경우, ON DELETE : 삭제 되었을 경우

    - CASCADE : 연결된 정보도 함께 수정 OR 삭제 (데이터 불일치를 방지하기 위해)

     

    만들어진 2개의 테이블 확인이 가능

    ✅ CRUD 작업하기

    📌 CREATE : 데이터를 생성해서 데이터베이스에 넣는 작업

    INSERT INTO nodestudy.users (name, age, married, comment) values ('zero', 24, 0, '자기소개1');
    INSERT INTO nodestudy.users (name, age, married, comment) values ('nero', 32, 1, '자기소개2');
    INSERT INTO nodestudy.comments (commenter, comment) values (1, '안녕하세요 zero의 댓글이에요');

     

    📌 READ : 데이터베이스 내의 데이터를 조회하는 작업

    1. 칼럼 전체 조회 (SELECT * )

    SELECT * FROM nodestudy.users;
    SELECT * FROM nodestudy.comments;

    2. 특정 칼럼 조회

    SELECT name, married FROM nodestudy.users;
    SELECT name, age FROM nodestudy.users WHERE married = 1 AND age > 30;

     

    3. 기타 옵션

    SELECT id, name FROM nodestudy.users ORDER BY age DESC;
    SELECT id, name FROM nodestudy.users ORDER BY age DESC LIMIT 1;

     

    📌 UPDATE : 데이터베이스의 데이터 수정

    - UPDATE [테이블명] SET [컬럼명 = 바꿀 값] WHERE [조건] 의 형태

    UPDATE nodestudy.users SET comment = '바꿀 내용' WHERE id = 2;

     

    📌 DELETE : 데이터베이스의 데이터 삭제

    - DELETE FROM [테이블명] WHERE [조건]

    DELETE FROM users WHERE id = 2;

     


    ✅ 시퀄라이즈 사용하기

    - ORM : 자바스크립트 객체와 데이터베이스의 릴레이션을 매핑해주는 도구

    - 사용 이유 : 자바스크립트 구문을 자동으로 sql문으로 바꿔주기 때문

     

    📌 squelize 및 모듈 설치 

    npm i express morgan nunjucks sequelize sequelize-cli mysql2
    npm i -D nodemon

    📌 models/index.js 수정

    const Sequelize = require('sequelize');
    const User = require('./user');
    const Comment = require('./comment');
    
    const env = process.env.NODE_ENV || 'development';
    const config = require('../config/config')[env]; 
    const db = {};
    
    const sequelize = new Sequelize(config.database, config.username, config.password, config);
    db.sequelize = sequelize;
    
    db.User = User;
    db.Comment = Comment;
    
    User.init(sequelize);
    Comment.init(sequelize);
    
    User.associate(db);
    Comment.associate(db);
    
    module.exports = db;

    📌 MYSQL 연결하기

    const express = require('express');
    const path = require('path');
    const morgan = require('morgan');
    const nunjucks = require('nunjucks');
    
    const { sequelize } = require('./models'); //models폴더 내 index.js 불러옴.
    
    const app = express();
    app.set('port', process.env.PORT || 3001);
    app.set('view engine', 'html');
    nunjucks.configure('views', {
        express : app,
        watch : true,
    });
    sequelize.sync({ force : false }) // sync 메서드 사용 ~ 실행시 mysql과 연동
        .then(() => {
            console.log('데이터 베이스 연결 성공');
        })
        .catch((err) => {
            console.error(err);
        });
    
    app.use(morgan('dev'));
    app.use(express.static(path.join(__dirname, 'public')));
    app.use(express.json());
    app.use(express.urlencoded({ extended : false }));
    
    app.use((req, res, next) => {
        const error = new Error(`${req.method} ${req.url} 라우터가 없습니다. `);
        error.status = 404;
        next(error);
    });
    
    app.use((err, req, res, next) => {
        res.locals.message = err.message;
        res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
        res.status(err.status || 500);
        res.render('error');
    });
    
    app.listen(app.get('port'), () => {
        console.log(app.get('port'), '번 포트에서 대기중');
    });

    📌 config.json 파일 수정 (비밀번호, db 이름 입력)

    {
      "development": {
        "username": "root",
        "password": "[root 비밀번호]
        "database": "nodestudy",
        "host": "127.0.0.1",
        "dialect": "mysql"
      },
     ...

    📌 시퀄라이즈 모델 정의

    - ❗ 기본적으로 모델 이름은 단수형, 테이블 이름은 복수형을 사용

    - user.js

    const Sequelize = require('sequelize');
    
    module.exports = class User extends Sequelize.Model {
        static init(sequelize) { // init : 테이블에 대한 설정
            return super.init({ // 테이블 칼럼에 대한 설정
                name : {
                    type : Sequelize.STRING(20),
                    allowNull : false, // null 값 허용 여부
                    unique : true, // unique(고유값) 여부
                },
                age : {
                    type : Sequelize.INTEGER.UNSIGNED,
                    allowNull : false, 
                },
                married : {
                    type : Sequelize.BOOLEAN,
                    allowNull : false,
                },
                comment : {
                    type : Sequelize.TEXT,
                    allowNull : true,
                },
                created_at : {
                    type : Sequelize.DATE,
                    allowNull : false,
                    defaultValue: Sequelize.NOW, // 기본값
                },
            }, { // 이 아래부터는 테이블 자체 설정
                sequelize, // db.sequelize 객체
                timestamps : false, // true일시 createdAt, updatedAt 칼럼 생성
                underscored : false, // 캐멀케이스(createdAt)를 스네이크케이스(created_at)로 변경
                modelName : 'User', // 모델 이름
                tableName : 'users', // 테이블 이름
                paranoid : false, // deletedAt 칼럼 생성(지운시간 기록), 후에 복원 가능
                charset: 'utf8',
                collate : 'utf8_general_ci', // 한글 입력 위한 설정
            });
        }
        static associate(db) {} // 다른 테이블과의 참조 관계
    };

    - comment.js

    const Sequelize = require('sequelize');
    
    module.exports = class User extends Sequelize.Model {
        static init(sequelize) {
            return super.init({
                comment : {
                    type : Sequelize.STRING(100),
                    allowNull : false,
                },
                created_at : {
                    type : Sequelize.DATE,
                    allowNull : true,
                    defaultValue: Sequelize.NOW,
                },
            }, {
                sequelize,
                timestamps : false,
                modelName : 'Comment',
                tableName : 'comments',
                paranoid : false, 
                charset: 'utf8mb4',
                collate : 'utf8mb4_general_ci',
            });
        }
        static associate(db) {}
    };

    - index.js에 연결 코드 추가

    db.User = User;
    db.Comment = Comment;
    
    User.init(sequelize);
    Comment.init(sequelize);
    
    User.associate(db);
    Comment.associate(db);

    📌 시퀄라이즈와 mysql 차이 간단 정리

    MYSQL 시퀄라이즈
    VARCHAR(100) STRING(100)
    INT INTEGER
    TINYINT BOOLEAN
    DATETIME DATE
    INT UNSIGNED INTEGER.UNSIGNED
    NOT NULL allownull : false
    UNIQUE unique : true
    DEFAULT now() defaultValue : Sequelize.NOW

    ✅ 테이블 간의 관계 정의

    - 📌 1 : N  = hasMany : User 1명은 여러 개의 Comment 생성이 가능

    - User.hasMany

     db.User.hasMany(db.Comment, { foreignKey : 'commenter', sourceKey : 'id' });

    - Comment.belongsTo

     db.Comment.belongsTo(db.User, { foreignKey : 'commenter', targetKey : 'id' });

     

    - 📌 1 : 1 = hasOne : User 1명은 하나의 Info를 갖는다 

    db.User.hasOne(db.Info, { foreignKey : 'UserId', sourceKey : 'id' });
    db.Info.belongsTo(db.User, { foreignKey : 'UserId', targetKey : 'id' });

     

    - 📌 N : M = belongsToMany : Post는 여러개의 Hashtag 가질 수 있고, Hashtag는 여러개의 Post에 쓰일 수 있음

    db.Post.belongsToMany(db.Hashtag, { through : 'PostHashtag' });
    db.Hashtag.belongsToMany(db.Post, { through : 'PostHashtag' });

    ✅ 시퀄라이즈 CRUD 쿼리 

    - 📌 CREATE

    INSERT INTO nodestudy.users (name, age, married, comment) values ('zero', 24, 0, '자기소개1');
    const { User } = require('../models');
    User.create({
    	name : 'zero',
        age : 24,
        married : false,
        comment : '자기소개1',
    });

     

    - 📌 READ

    SELECT * FROM nodestudy.users;
    SELECT * FROM nodestudy.users LIMIT 1;
    User.findAll({});
    User.findOne({});
    SELECT name, age FROM nodestudy.users WHERE married = 1 AND age > 30;
    const { Op } = require('sequelize');
    const { User } = require('../models');
    User.findAll({
    	attributes : ['name', 'age'],
        where : {
        	married : true,
            age : { [Op.gt] : 30 }, // Op.gt : Op객체의 gt 함수, 초과를 의미
        },
    });

     

    - 📌 UPDATE 

    UPDATE nodestudy.users SET comment = '바꿀 내용' WHERE id = 2;
    User.update({
    	comment : '바꿀 내용',
    }, {
    	where : { id : 2 },
    });

     

    - 📌 DELETE

    DELETE FROM users WHERE id = 2;
    User.destory({
    	where : { id : 2 },
    });

    ✅ 시퀄라이즈 관계 쿼리

    const user = await User.findOne({
    	include : [{
        	model : Comment,
        }] // 특정 사용자를 가져오면서 그 사용자의 댓글까지 가져오고 싶은 경우 사용하는 옵션
    });
    
    // 혹은 아래처럼 관계 설정시 자동 생성되는 동사+모델이름 형태의 메소드 사용가능
    const user = await User.findOne({});
    const comments = await user.getComments(); // 조회

    - ❗ 그 밖에 set(수정), add(생성), remove(삭제)등의 메서드도 지원

    const user = await User.findOne({
    	include : [{
        	model : Comment,
            where : {
            	id : 1,
            },
        }]
    });
    
    const comments = await user.getComments({
    	where : {
        	id : 1,
        },
        attributes : ['id']
    });

    - 쿼리에도  where, attributes등의 옵션 사용도 가능

     

    const [ result, metadata ] = await sequelize.query('SELECT * from comments');

    - 직접 SQL 쿼리 사용하는 방법도 있음


    ✅ 실습 코드 (프론트 코드 생략)

    // 사용자 이름을 누르면 댓글 로딩
    document.querySelectorAll('#user-list tr').forEach((el) => {
        el.addEventListener('click', function() {
            const id = el.querySelector('td').textContent;
            getComment(id);
        });
    });
    
    // 사용자 가져오기
    async function getUser() {
        try {
            const res = await axios.get('/users');
            const users = res.data;
            console.log(users);
            const tbody = document.querySelector('#user-list tbody');
            tbody.innerHTML = '';
            users.map(function (user) {
                const row = document.createElement('tr');
                row.addEventListener('click', () => {
                    getCommnet(user.id);
                });
    
                let td = document.createElement('td');
                td.textContent = user.id;
                row.appendChild(td);
    
                td = document.createElement('td');
                td.textContent = user.name;
                row.appendChild(td);
    
                td = document.createElement('td');
                td.textContent = user.age;
                row.appendChild(td);
    
                td = document.createElement('td');
                td.textContent = user.married ? '기혼' : '미혼';
                row.appendChild(td);
                tbody.appendChild(row);
            })
        } catch (err) {
            console.error(err);
        }
    }
    
    // 댓글 가져오기
    async function getComment(id) {
        try {
            const res = await axios.get(`/users/${id}/comments`);
            const comments = res.data;
            const tbody = document.querySelector('#comment-list tbody');
            tbody.innerHTML = '';
            comments.map(function (comment) {
                const row = document.createElement('tr');
                let td = document.createElement('td');
                td.textContent = comment.id;
                row.appendChild(td);
                td = document.createElement('td');
                td.textContent = comment.User.name;
                row.appendChild(td);
                td = document.createElement('td');
                td.textContent = comment.comment;
                row.appendChild(td);
                
                const edit = document.createElement('button');
                edit.textContent = '수정';
                edit.addEventListener('click' , async () => {
                    const newComment = prompt('바꿀 내용을 입력하세요');
                    if(!newComment) {
                        return alert('반드시 내용을 입력해야 합니다. ');
                    }
                    try {
                        await axios.patch(`/comments/${comment.id}`, { comment : newComment });
                        getComment(id);
                    } catch (err) {
                        console.error(err);
                    }
                });
                const remove = document.createElement('button');
                remove.textContent = '삭제';
                remove.addEventListener('click', async () => {
                    try {
                        await axios.delete(`/comments/${comment.id}`);
                        getComment(id);
                    } catch (err) {
                        console.error(err);
                    }
                });
                td = document.createElement('td');
                td.appendChild(edit);
                row.appendChild(td);
                td = document.createElement('td');
                td.appendChild(remove);
                row.appendChild(td);
                tbody.appendChild(row);
            });
        } catch (err) {
            console.error(err);
        }
    }
    
    //사용자 등록 
    document.getElementById('user-form').addEventListener('submit', async (e) => {
        e.preventDefault();
        const name = e.target.username.value;
        const age = e.target.age.value;
        const married = e.target.married.checked;
        if(!name) {
            return alert('이름을 입력하세요');
        }
        if(!age) {
            return alert('나이를 입력하세요');
        }
        try {
            await axios.post('/users', {name, age, married});
            getUser();
        } catch (err) {
            console.error(err);
        }
        e.target.username.value = '';
        e.target.age.value = '';
        e.target.married.checked = false;
    });
    
    // 댓글 등록
    document.getElementById('comment-form').addEventListener('submit', async (e) => {
        e.preventDefault();
        const id = e.target.userid.value;
        const comment = e.target.comment.value;
        if(!id) {
            return alert('아이디를 입력하세요');
        }
        if(!comment) {
            return alert('댓글을 입력하세요');
        }
        
        try {
            await axios.post('/comments', { id, comment });
            getComment(id);
        } catch (err) {
            console.error(err);
        }
        e.target.userid.value = '';
        e.target.comment.value = '';
    });

     

    - app.js 연결

    const indexRouter = require('./routes');
    const usersRouter = require('./routes/users');
    const commentsRouter = require('./routes/comments');
    .
    .
    .
    app.use('/', indexRouter);
    app.use('/users', usersRouter);
    app.use('/comments', commentsRouter);

     

    - routes 폴더 생성 및 index.js 파일 작성

    const express = require('express');
    const User = require('../models/user');
    
    const router = express.Router();
    
    router.get('/', async(req, res, next) => { // '/'로 Get 요청 들어왔을 경우
        try {
            const users = await User.findAll(); // 모든 유저 찾기
            res.render('sequelize', { users }); // users를 렌더링
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     

    - routes/user.js

    const express = require('express');
    const User = require('../models/user');
    const Comment = require('../models/comment');
    const router = express.Router();
    
    router.route('/') 
        .get(async (req, res, next) => { // 사용자 조회 요청(get)
            try {
                const users = await User.findAll();
                res.json(users); // json형식의 데이터 반환
            }
            catch (err) {
                console.error(err);
                next(err);
            }
        })
        .post(async (req, res, next) => { // 사용자 등록 요청(post)
            try {
                const user = await User.create({
                    name : req.body.name,
                    age : req.body.age,
                    married : req.body.married,
                });
                console.log(user);
                res.status(201).json(user);
            } catch(err) {
                console.error(err);
                next(err);
            }
        });
    
        router.get('/:id/comments' , async(req, res, next) => {
            try {
                const comments = await Comment.findAll ({
                    include : {
                        model : User,
                        where : { id : req.params.id },
                    },
                });
                console.log(comments);
                res.json(comments); // 특정 사용자의 댓글들 불러옴
            } catch (err) {
                console.error(err);
                next(err);
            }
        });
    
    module.exports = router;

     

    - routes/comment.js

    const express = require('express');
    const { User, Comment } = require('../models');
    
    const router = express.Router();
    
    router.post('/', async(req, res, next) => { // 댓글 작성
        try {
            const comment = await Comment.create({
                commenter: req.body.id,
                comment : req.body.comment,
            });
            console.log(comment);
            res.status(201).json(comment);
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    router.route('/:id') 
        .patch(async (req, res, next) => { // 댓글 수정
            try {
                const result = await Comment.update({
                    comment: req.body.comment,
                }, {
                    where : {id : req.params.id},
                });
                res.json(result);
            } catch (err) {
                console.error(err);
                next(err);
            }
        })
        .delete(async(req, res, next) => { // 댓글 삭제
            try {
                const result = await Comment.destroy({ where : {id : req.params.id } });
                res.json(result);
            } catch (err) {
                console.error(err);
                next(err);
            }
        });
    
    module.exports = router;

     

    - 실습 화면

     

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

    Node.js 프로젝트 리팩토링(1)  (0) 2022.02.27
    Node.js 스터디 5주차  (0) 2022.02.15
    Node.js 스터디 4주차  (0) 2022.02.07
    Node.js 스터디 3주차  (0) 2022.02.05
    Node.js 스터디 2주차  (0) 2022.01.25
Designed by Tistory.