ORM

김민석·2021년 4월 13일
1

Immersive

목록 보기
29/30

ORM이란

객체 관계 매핑(Object-relational mapping; ORM)은 데이터베이스와 객체 지향 프로그래밍 언어 간의 호환되지 않는 데이터를 변환하는 프로그래밍 기법이다. 객체 지향 언어에서 사용할 수 있는 "가상" 객체 데이터베이스를 구축하는 방법이다. [출처: 위키 백과]

즉, 객체와 관계형 데이터베이스의 데이터를 자동으로 연결해주는 것을 말한다.
둘 간의 형식이 다르므로, SQL 문을 자동으로 생성하여 데이터베이스를 객체처럼 다룰 수 있게 만드는 것이라고 이해하였다.

사용하면서 느꼈던 장점은 mysql모듈을 그대로 사용하는 것보다 전체적인 코드의 연결성이 돋보인다고 해야할까.
둘이 이질적이지 않다는 생각이 든다. 마치 한 언어를 사용하는 것과 같이

그 이외로 와닿는 장점은 없었다.

당장 느껴지는 단점은 효율적으로 사용하려면 커스터마이징이 필요한 것으로 보인다. (익숙하지 않아서 그럴지도..)


이 글은 오늘 했던 스프린트를 다시 처음부터 다뤄보며 작성하려고 한다.
사용한 ORM은 Sequalize다.

Sequelize 설치 Migration을 위한 sequelize-cli 설치

공식문서 찾아보면 바로 나오므로 생략.

Sequelize만을 이용해서 바닥부터 설계할 수 있지만, sequelize-cli를 이용하여 프로젝트를 시작할 수 있다.
이렇게 하면 기본적인 것들이 만들어져 있어서 편리하다.
(bootstrapping)

Sequelize-cli를 이용하여 프로젝트 부트스트래핑 하는 법

우선 모델이라는 개념을 알고 가야할 듯하다.

모델읜 Sequelize의 정수다. 이 모델을 기반으로 sequelize가 database와 객체를 잇는 다고 보면 될 것 같다.

Sequelize에서 Model class의 하위 class를 models라고 지칭하며, 하나의 모델은 하나의 entity(entity는 database에서 하나의 정보 단위라고 생각하면 된다/table이 바로 그것)와 연결되어 있다.

단순 연결만 되어있을 뿐 아니라, 온갖 정보를 갖고있다. (또한 그것들을 다루는 메소드도 갖고 있다.)

참고
테이블 이름을 마음대로 설정 가능하지만, 아무것도 행하지 않는다면
model은 단수형의 이름을, database안의 table은 그것의 복수형의 이름을 갖고 있다.


아래의 코드는 routing에 사용될 함수들을 객체에 담은 것이다.
참고로 routing 과정에서

get /links
get /links/:id

는 한번에 다루었다. 참고 : regex를 사용하여 routing 하기

참고
sequelize-cli가 자동으로 생성한 것들 중 models 폴더에 index.js를 보면, 해당 디렉토리에 있는 모든 것을 객체에 담아 index.js에서 export 하는 것을 볼 수 있다. 따라서 models 디렉토리를 export하면 모든 models가 export 된다.

const {url} = require('../../models')
const { isValidUrl, getUrlTitle } = require('../../modules/utils')
module.exports = {
    get : async (req,res)=>{
        if(req.params.id){
            const data = await url.findOne({
                where: {
                    id : req.params.id
                }
            })
            data.visits ++
            await data.save()
            res.status(302).redirect(data.dataValues.url)
        } else {
            let data = await url.findAll()
            res.status(200).send(data)
        }
    },
    post : async (req, res)=>{
        if(isValidUrl(req.body.url)){
            getUrlTitle(req.body.url, async (err, title)=>{
                if(err){
                    return err
                } 
                const [ data, created ] = await url.findOrCreate({
                    where : { 
                        title : title 
                    },
                    defaults : {
                        url : req.body.url,
                        title: title
                    }
                })
                res.status(201).send(data)
            })
        }
    }
}

사용한 쿼리들

  • findAll
  • findOne
  • findorCreate
  • save (findOne으로 찾음과 동시에 내용을 update(visits ++) 하기 위해 사용하였다.)

Migration

Migration은 데이터베이스 스키마에 변경이 생겼을 때, 변경을 줄 부분을 git의 commitmessage 처럼 기록한 후에 그것을 토대로 데이터베이스를 변경하는 것이다.

Migration 순서는 다음과 같다.
1) Migration-skeleton 생성
2) up 함수와 down 함수 정의
3) cli를 통해 db:migrate 실행

사용하다보니, 데이터베이스 스키마에 변경이 생길 때, 무적권 마이그레이션에서 설정 해야하는 것으로 보인다.

이상한 것이 하나 있는데 models 파일들에서 init은 재실행 되지 않는 것처럼 보인다는 것이다.(반면 association을 정의하는 부분은 제대로 실행되는 것으로 보인다.)

하나 생각이 드는게 있다면,
원래 sequelize는 Model.sync()를 이용하여 DB내의 table과 각 model을 일치시키는데, migration을 사용한다는 것은 git의 commit처럼 확인 절차를 가지게 되므로, 자동으로 sync하지 않게 하는 것으로 보인다.

만약 sync를 사용했다면 model을 변경하는 것으로 db의 data도 변경 되지 않았을까~ 싶다.


Associations

데이터 베이스간의 관계를 association 이라고 한다.
Sequelize에는 4가지의 관계가 있다.

  • HasOne
  • BelongsTo
  • HasMany
  • BelongToMany

이중 내가 시도해 본 것은
1:N을 표현하기 위한
HasMany
BelongsTo다.

Team.hasMany(player);
player.belongsTo(team);

라는 관계가 있다면
Team:player는 1:N의 관계를 가지고 있다.
따라서 player에 teamId가 생길 것이다.

다른 관계들에 대한 설명과 상세한 코드는 여기 자세하게 나와있다.


내가 이번에 가장 오랜시간 동안 해맸던 것은,
둘의 관계를 설정하는 방식이다.

코드는 위의 링크에서 알 수 있었는데, 어디에서 관계 정의가 필요한 것인지 도통 찾기가 힘들었다.
이 부분이 찾기 힘들다는 것은 다른 사람들도 마찬가지 인가보다.
나는 이 링크를 참조하여 힌트를 얻었다.

관계 자체는 모델에서 설정하며 sequelize-cli가 자동으로 생성한 models 디렉토리에 각 model들에서 association을 설정하라고 친절하게 설명까지 코드에 붙어있는 것을 발견하였다.

'use strict';
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class url extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      // define association here
      url.belongsTo(models.user)  // <-- 이 부분이다.
    }
  };
  url.init({
    url: DataTypes.STRING,
    title: DataTypes.STRING,
    visits: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    }
  }, {
    sequelize,
    modelName: 'url',
  });
  return url;
};

반대로 user.js에서는 user.hasMany(models.url) 코드를 통해 관계 설정을 하였다.

그 다음은 테이블에가서 (나의 경우는 urls) 외래키를 (userId) 만들어줘야 한다.
나는 migration 디렉토리에서에서 설정해주었다. (관계 설정시 option으로 생성하는 것도 가능한 것 같다.)

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

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

참고!
Op이라는 것이 있다. 공식 문서
const {Op} = require("sequelize")로 불러와서 사용할 수 있는데, 이런 식으로 사용할 수 있다.

const { Op } = require("sequelize");
Post.findAll({
  where: {
    [Op.and]: [{ a: 5 }, { b: 6 }],            // (a = 5) AND (b = 6)
    [Op.or]: [{ a: 5 }, { b: 6 }],             // (a = 5) OR (b = 6)
    someAttribute: {
      // Basics
      [Op.eq]: 3,                              // = 3
      [Op.ne]: 20,                             // != 20
      [Op.is]: null,                           // IS NULL
      [Op.not]: true,                          // IS NOT TRUE
      [Op.or]: [5, 6],                         // (someAttribute = 5) OR (someAttribute = 6)
      ... 생략

관계 설정이 끝이 났다면

let data = await url.findOne({
                where : {
                    userId : {
                        [Op.ne] : null
                    }
                    
                },
                include: user
            })
            
            res.status(200).send(data)

이런 식으로 include option을 사용해서, 연관된 rows를 병합하여 불러올 수 있다.

0개의 댓글