2.MVC_ORM

const_yang·2022년 1월 9일
0

데이터베이스

목록 보기
2/2
post-thumbnail

ORM이란 무엇인가?

Object Relational Mapping의 약자이다. Model을 기술하는 도구이다.

개발 환경에서 데이터베이스에 접근할 수 있도록 하는 중간자 역할이라고 생각하자.

Sequelize ORM

비동기 기반의 Node.js ORM이다. 실제로 async, await 비동기가 기본으로 쓰인다.

나는 이번 Sprint를 통해서,

  • Sequelize 설치하기
  • 모델 생성 및 MVC 구조 설정하기
  • 모델 마이그레이션하기
  • CRUD 중 Create, Update 작업하기
  • Join 테이블 만들기 (관계 설정)

등을 해보았다.

Sequelize 설치하기

1) 공식문서에 나와있는 대로, 먼저 sequelize를 설치한다.

// Sequlize를 설치하자
npm install --save sequelize

2) Migration을 하려면 sequelize-cli도 설치해야 한다.

우리가 작성한 모델이 실제로 database에 존재하도록 하려면 migration 작업이 꼭 필요하다.

npm install --save-dev sequelize-cli

프로젝트 준비하기

1) 프로젝트를 시작하기 전, 아래 명령어를 통해 우리는 여러 준비 파일을 생성할 수 있다.

npx sequelize-cli init

2) 자동으로 생성되는 4개의 파일 중 config 폴더에는 작업 환경에 대한 정보가 담겨 있다. 기본적으로 development 환경이 설정이 되어있고, mysql 을 사용하는 것으로 되어 있다.

// 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"
  }
}

// models/index.js에 보면 아래처럼 development가 기본으로 쓰이고 있다는 걸 알 수 있다.
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];

3) 데이터 베이스 생성하기

// 아래 명령어를 입력하면 현재 기본으로 설정된 데이터베이스(database_development)가 자동으로 생성된다.
db:create

모델 생성 및 마이그레이션

1) url 모델을 먼저 생성하자. model:generate 를 통해 모델과 마이그레이션 파일을 만들 수 있다.

// name과 attribute를 어떻게 설정하는지 관찰하자
npx sequelize-cli model:generate --name url --attributes 
url:string,title:string,visits:integer
// 필드의 다른 속성을 추가할 수 있다. 변경 전, 
visits: DataTypes.INTEGER

// 변경 후,
visits: {
		type: DataTypes.INTEGER,
    defaultValue: 0
}

point: dataValues를 따로 설정해 주지 않으면 각 컬럼의 기본값은 NULL 이다.

2) 마이그레이션 파일을 살펴보자.

'use strict';
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('urls', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      url: {
        type: Sequelize.STRING
      },
      title: {
        type: Sequelize.STRING
      },
      visits: {
        type: Sequelize.INTEGER,
        defaultValue: 0
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('urls');
  }
};

테이블 이름이 우리가 모델에서 설정한 url과 달리 urls로 복수형이다. 아래 내용 참고하자.

A model in Sequelize has a name. This name does not have to be the same name of the table it represents in the database. Usually, models have singular names (such as User) while tables have pluralized names (such as Users), although this is fully configurable.

3) 마이그레이션 해보자.

마이그레이션을 진행하면 실제 mysql의 database-development 데이터베이스에 url이라는 테이블이 생성되는 것을 확인할 수 있다. (undo를 진행하면 테이블 사라진다)

// # 마이그레이션 진행하기
npx sequelize-cli db:migrate

// # 마이그레이션 파일 수정이 필요한 경우, 마이그레이션 해제하기
npx sequelize-cli db:migrate:undo

Router → Controller 연결하자

1) app.js 파일에 router 파일을 연결하자.

const express = require('express');
const logger = require('morgan');

const indexRouter = require('./routes/index');
const linksRouter = require('./routes/links');

const app = express();

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use('/', indexRouter);
app.use('/links', linksRouter);

module.exports = app;

2) routes/links.js 파일에 라우터 내용을 작성하자. 각 endpoint에 따라 분기할 메소드의를 연결해 주어야 한다.


const express = require('express');
const router = express.Router();
const controller = require('../controllers/links');

router.get('/', controller.get);          // get 메소드 연결
router.post('/', controller.post);        // post 메소드 연결
router.get('/:id', controller.getId)      // get 메소드 중 redirect를 위한 연결   

module.exports = router;

Controller를 작성해 보자

controller는 작성되어 있는 url model를 통해 데이터베이스의 정보를 조회 또는 작성할 예정이다.

그럼 우선 model를 가져오자.

const model = require("../../models").url;

point: 그런데 왜 const model = require("../../models/url") 이라고 작성하면 안 되는 걸까?

사실 url은 ‘models/index.js’ 즉, Model이라는 클래스의 하나의 instance이다.

post /links API 확인하자

  • request: payload에 사용자가 입력하는 url을 url 속성에 담아 요청한다.
{
  "url": "https://www.github.com"
}
  • response: 방금 생성한 모델의 JSON
{
  "id": 1,
  "url": "https://www.github.com",
  "title": "The world’s leading software development platform · GitHub",
  "visits": 1,
  "createdAt": "2020-07-25T20:07:15.000Z",
  "updatedAt": "2020-07-25T20:07:15.000Z"
}

post 컨트롤러 구현

여기에서 id, createdAt, updatedAt은 자동으로 생성된다. url, title, visits를 레코드로 urls 테이블에 전달해야 한다.

1) url은 req.body를 통해 획득할 수 있으니 pass

2) title은 다른 방법으로 만들어야 한다. sprint에 힌트가 나와있다.

modules/utils.js에 이미 구현되어 있는 두 메소드를 활용할 예정이다.

const {getUrlTitle, isValidUrl} = require('../../modules/utils')

isValidUrl 의 경우 url 검사를 한번 해주는 것 같고, getUrlTitle 은 title을 생성해 주는 것 같다.

const request = require('request');

const rValidUrl = /^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i;

exports.getUrlTitle = (url, cb) => {
  request(url, function (err, res, html) {
    if (err) {
      console.log('Error reading url heading: ', err);
      return cb(err);
    } else {
      const tag = /<title>(.*)<\/title>/;
      const match = html.match(tag);
      console.log(match.length)
      const title = match ? match[1] : url;
      return cb(err, title);
    }
  });
};

exports.isValidUrl = url => {
  return url.match(rValidUrl);
};
// controller에 작성한 post 요청 작업의 식은 아래와 같다.
// 1. 먼저 url 유효성 검사를 하고, 유효하지 않은 경우 400을 반환하자
// 2. 유효한 url의 경우 가져온 getUrlTitle 메소드를 활용하여 얻은 title을 반환하자

post: async (req, res) => {
        const url = req.body.url;
        if (!isValidUrl(url)) return res.stauts(400).send('Not Found')

				// getUrlTitle 두 번째 인자는 콜백함수였다. 콜백함수에 우리가 작업하고자 하는 식을 넣어 주자.	
        getUrlTitle(url, async (err, title) => {
            if (err) return res.sendStatus(400);
				
			// sequelize API 문서를 통해 "조회 또는 생성"하는 문법을 찾았다.
        	// title 값을 받아옴
            // title을 url 모델의 title 값으로 => 레코드 생성
            // 만약에 이미 해당 url이 데이터베이스에 존재한다면 => 레코드 조회
             model.findOrCreate({
                where: {
                    url 
                },
                defaults: {
                    title
                }
            })
            .then(([result, created]) => {
                if (created) {
                    return res.status(201).json(result); // created
                } else {
                    return res.status(201).json(result); // find
                }
            })
            .catch((err) => req.sendStatus(500))
        })
    }

model.findOrCreate는 특정 옵션에 해당하는 엔트리를 찾지 못한 경우, 옵션 내용 + defaults에 들어간 내용으로 레코드를 생성하게 된다. 그것이 result 객체로 생성이 된다. (다른 곳에서 생성한 getUrlTitle 함수를 가져와서 그 인자에 콜백함수 형식으로 쿼리문을 실행하는 것을 이해하기 굉장히 오래 걸렸다)

3) visits에는 기본값이 있으니 pass

get 컨트롤러 구현

post보다 어렵지 않았다. findAll() 은 SQL syntax인 select * from 과 동일하게 작동한다.

get: async (req, res) => {
        try {
            const result = await model.findAll();
            return res.status(200).json(result)
        } catch (err) {
            return res.sendStatus(500);
        }
    }

getId 컨트롤러 구현 (redirect 기능)

1) model.findByPk를 통해 params로 전달된 id값으로 레코드를 찾는다.

2) 방문 횟수 증가를 위해 model.increment를 사용한다. { {늘리고자 하는 필드: 늘리는 수의 크기} , {where : 조건}}

3) redirect를 구현하기 위해 Express 문법을 사용한다.

getId: async (req, res) => {
   		// id라는 파라미터
  		// 해당 파라미터를 이용해서 url 모델엘 조회를 해야함
        const id = req.params.id;
        const target = await model.findByPk(id);
        await model.update({visits: visits + 1};
        if (!target) {
          res.sendStatus(204);
        } else {
          const url = target.url;
          target.update({
          	visits: target.visits + 1
          })
          try {
            res.status(302).redirect(url)
          } catch (err) {
            res.sendStatus(500)
        }
    }

Association을 통한 JOIN 테이블 구현해보자

join할 추가 모델을 생성하자

npx sequelize-cli model:generate --name user --attributes 
name:string,email:string,age:integer

urls 테이블에 userId (user 테이블의 id를 참조) 필드를 생성하자

migration skeleton 파일을 하나 생성하여 기존 테이블을 업데이트할 수 있다

npx sequelize-cli migration:generate --name url-skeleton

생성한 skeleton 파일에서 필드를 하나 추가하고, FK 설정을 해주자

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.addColumn('urls','userId',Sequelize.INTEGER);
    
      {
        type: Sequelize.INTEGER,
        references: {
          model: 'users',
          key: 'id'
        },
        onUpdate: 'CASCADE',
        onDelete: 'CASCADE'
      }
    )
  },

  down: async (queryInterface, Sequelize) => {
    return queryInterface.removeColumn(
      'urls',
      'userId'
    )
  }
};

0개의 댓글