58일차 [데이터베이스] ORM

2021. 10. 20. 13:32
반응형

2021. 10. 18 월요일

1. Today's Key Points!🔑

  • ORM
  • Sequelize ORM
  • Migrations

2. 정리해보자!🧹

ORM

Object-Relational Mapping. 관계형 데이터베이스에 있는 데이터에 접근할 때 마치 자바스크립트에 있는 객체, 클래스 처럼 취급할 수 있다. 자바스크립트 코드상에 있는 객체와 관계형데이터베이스 사이에서 중계자 역할을 하는 것이 ORM. 

ORM

왜 사용하는 걸까? 가장 큰 이유는 SQL을 몰라도 데이터베이스에 접근해서 원하는 데이터를 얻어낼 수 있다는 것이 아닐까 한다. 접근방법 자체를 프로그래밍 언어의 관점에서 맞출 수 있도록 도와주기 때문에 프로그래밍 언어만을 가지고도 데이터베이스와 소통이 가능하다.

3. Sprint 과제 복기!🧐

Sequelize ORM(공식문서 링크) 설정과 모델 생성

앞으로 사용할 ORM은 Sequelize이다. 기본적으로 Promise를 기반한다. 

터미널에 아래 명령어를 입력해서 설치해주자. 그리고 우리가 사용할 데이터 베이스를 하나 만들어주자.

npm install --save sequelize
mysql -u root -p
mysql> CREATE DATABASE shortly

 

마이그레션이라는 툴이 있는데, CLI에서 모델을 생성해주거나 스키마를 적용할 수 있도록 도와준다. 우선 Sequelize CLI를 설치하자.

npm install --save-dev sequelize-cli

빈 프로젝트를 만들려면 init명령을 실행해야 한다.

npx sequelize-cli init

 

그럼 아래의 4개의 폴더가 만들어진다.

  • config : 데이터베이스에 연결하는 방법을 CLI에 알려주는 구성 파일이 포함되어 있다.
  • models : 프로젝트의 모든 모델을 포함한다.
  • migrations : 모든 마이그레이션 파일을 포함한다.
  • seeders : 모든 시드 파일을 포함한다.

만들어진 models폴더에 index파일을 한번 살펴보자.

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

코드를 한번 쭉 보면, 데이터베이스에 연결해주는 코드를 알아서 만들어 준다. CLI init을 하지않았다면 직접 저 코드를 써야했을 것이다. 연결하는 방법은 여기 나와있다. 그 다음 만들어진 config파일을 살펴보자.

{
  "development": {
    "username": "root",
    "password": null,
    "database": "shortly",
    "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"
  }
}

패스워드 부분에 null이 되어있는데, mysql에 접근할 때 필요한 패스워드를 적어주고 데이터베이스를 아까 만든 shortly를 적어주면 연결이 될 것이다.

모델을 만들어주자. 우리가 만들어야 하는 모델은 url이고 속성으로는 url필드는 문자열, title필드는 문자열, visits필드는 정수형이다.

npx sequelize-cli model:generate --name url --attributes url:string,title:string,visits:integer

그럼 models 폴더에 url파일과 migrations 폴더에 하나의 파일이 만들어 진다. 한번 살펴보자. visits는 데이터 타입이 정수형이고, default값이 0이어야 한다.

// migrations 파일

"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,
        defaultValue: Sequelize.literal("NOW()"),
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.literal("NOW()"),
      },
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable("urls");
  },
};

위와 같이 입력해주면 visits의 속성을 정수형, default값을 0으로 줄 수 있다.

id, createdAt, updatedAt필드는 알아서 자동 생성을 해준다. 그 안에 내가 임의로 defaultValue를 저렇게 넣어 주었다.

(사실 나는 여기서 모델을 수정한 뒤에 migration을 실행해주면, 그 속성들이 적용된 테이블이 데이터베이스에 생성되는줄 알았는데, 그게 아니었다. 마이그레이션 폴더에 생성된 파일에서 속성을 수정해주고 마이그레이션을 진행해주어야 제대로 속성들이 적용된 테이블이 생성되었다.)

근데 여기까지 해주었다고 해서 데이터베이스에 삽입된 것이 아니다. 마이그레이션을 실행해 주어야 한다.

npx sequelize-cli db:migrate

이제 url 테이블이 만들어 졌을 것이다. 확인해보자

마이그레이션 실행 결과

그런데 table을 보면 urls가 만들어진 것을 볼 수 있다. 왜냐하면 테이블 이름을 정의한적이 없기 때문이다. 하지만 모델명은 url로 정의해 주었다. 기본적으로 테이블 이름을 지정하지 않으면 Sequelize는 자동으로 모델 이름을 복수화해서 테이블 이름으로 사용한다.

이것을 방지해줄 수 있는 방법은 아래처럼 해주면 된다. 공식문서를 참고했다.

const sequelize = new Sequelize('sqlite::memory:', {
  define: {
    freezeTableName: true
  }
});

freezeTableName: true를 해주면 모델명과 테이블명을 같게해줄 수 있다.

Controller 작성 및 구현, Router 연결

controllers파일안에 links파일안에 index.js를 만들어 컨트롤러를 구현해주어야 한다.

우선 index파일안에 그냥 응답만 끝내는 get, post메소드를 넣어주자.

// controllers/links/index.js

module.exports = {
  get: (req, res) => {
    res.end();
  },
  post: (req, res) => {
    res.end();
  },
};

그리고 그 컨트롤러와 route연결을 해주어야 한다.

app.js를 보면

// 생략

const linksRouter = require('./routes/links');
app.use('/links', linksRouter);

//생략

'/links'경로에 linksRouter를 연결해주고 있다. 그래서 routes폴더에 links파일에서 컨트롤러와 라우트 연결을 해주면 된다.

// routes/links.js

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

router.get("/", controller.get);
router.post("/", controller.post);

module.exports = router;

이제 라우트 연결은 끝났다. 컨트롤러를 구현하러 가자. 여기서 진정한 ORM을 사용할 것이다.

우선 post부터 만들어 보자. 입력받은 url과 title을 urls 테이블에 삽입해야한다. 이것을 구현해주기 위해 필요한 것들을 다 가지고 와야한다. url모델을 가져와서 사용해야하고, utils안에 있는 함수를 사용할 것이다. getUrlTitle은 title을 얻게해주는 함수이고, isValidUrl은 url을 구조에 잘 맞춰서 썼는지를 확인해주는 함수이다.

const { url } = require("../../models");
const { getUrlTitle, isValidUrl } = require("../../modules/utils");

module.exports = {
  post: (req, res) => {
    const inputUrl = req.body.url;
    if (!isValidUrl(inputUrl)) {
      return res.status(400).send("Not url address structure");
    } else {
      getUrlTitle(inputUrl, async (err, title) => {
        if (err) return res.status(500).send("Server Error Code 500");
        else {
          try {
            const [data, created] = await url.findOrCreate({
              where: { url: inputUrl, title: title },
            });
            res.status(201).json(data);
          } catch (err) {
            res.status(500).send("Server Err Code 500");
          }
        }
      });
    }
  },
};

async / await을 쓸때, 작동이 잘 이루어졌을 때는 try에서 끝이나고, 에러가 나면 catch가 에러를 잡아서 에러가 났을 때를 처리 해준다. findOrCreate는 데이터를 INSERT하는데, 중복 데이터가 있으면 넣지않는 메소드이다.

get을 구현해보자. urls 테이블의 목록을 보여주어야 한다.

const { url } = require("../../models");
const { getUrlTitle, isValidUrl } = require("../../modules/utils");

module.exports = {
  get: async (req, res) => {
    try {
      const data = await url.findAll();
      res.status(200).json(data);
    } catch (err) {
      res.status(500).send("Server Error Code 500");
    }
  },
};

이제 리디렉션을 구현해주어야 한다. /links/:id을 요청하면 url 필드값으로 리디렉션을 해야한다. 그러기 위해서는 하나의 컨트롤러를 더 작성해야하고, 라우트 연결도 해주어야 한다.

// routes/links.js

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

router.get("/", controller.get);
router.post("/", controller.post);
router.get("/:id", controller.redirect);

module.exports = router;

이제 redirect를 구현하자. id값을 입력받으면 그것을 바탕으로 해당하는 데이터 하나를 찾아주고, url필드 값으로 리다이렉팅을 해주어야 한다.

const { url } = require("../../models");
const { getUrlTitle, isValidUrl } = require("../../modules/utils");

module.exports = {
  redirect: async (req, res) => {
    const inputId = req.params.id;
    try {
      const data = await url.findOne({ id: inputId });
      if (!data) {
        return res
          .status(404)
          .send("Invalid id entered or does not exist data");
      } else {
        data.update({ visits: data.visits + 1 });
        return res.status(301).redirect(data.url);
      }
    } catch (err) {
      return res.status(500).send("Server Error Code 500");
    }
  },
};

res.redirect(data.url)을 해주면 data.url로 주소창이 변경된다. 상태코드는 301이 기본값이다.

Optional(환경변수 사용하기)

설정 파일은 .gitignore에 등록되어 있다. 설정 파일을 git의 관리를 받게 하는 대신, 환경 변수를 사용하게 만들 수 있는가?

환경변수를 사용하기위해 dotenv를 설치하자.

npm install dotenv

.env파일을 만들어서 환경변수를 만들어 놓자. 그리고 config파일에 config.js파일을 만들어주자

.env 파일내용

PASSWORD=본인 패스워드
// config/config.js

const dotenv = require("dotenv");
dotenv.config();

const config = {
  development: {
    username: "root",
    password: process.env.PASSWORD,
    database: "shortly",
    host: "127.0.0.1",
    dialect: "mysql",
  },
  test: {
    username: "root",
    password: process.env.PASSWORD,
    database: "database_test",
    host: "127.0.0.1",
    dialect: "mysql",
  },
  production: {
    username: "root",
    password: process.env.PASSWORD,
    database: "database_production",
    host: "127.0.0.1",
    dialect: "mysql",
  },
};

module.exports = config;

models폴더에 index 파일을 가서 손을좀 봐주자

"use strict";

const fs = require("fs");
const path = require("path");
const Sequelize = require("sequelize");
const basename = path.basename(__filename);
// const config = require(__dirname + '/../config/config.json')[env];
const configJS = require("../config/config.js");
const env = process.env.NODE_ENV || "development";
const config = configJS[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;

process.env가 .env파일을 읽을 수 있게 하려면 config() 해주는 것이 필요하다. 그래서 config.js파일을 가져온 뒤에 env를 저렇게 선언해주면 process.env가 .env파일을 읽을 수 있게 되고(config.js파일에서 config()를 해주고 있다.), .env파일에서 NODE_ENV가 할당이 되어있으면 env는 그 값을 가지게 된다.

 

반응형
LIST

BELATED ARTICLES

more