백엔드 개발자(node.js)가 되는 과정

Sequelize로 모델 생성 및 테이블 연동하기

soopy 2023. 6. 21. 14:35
728x90

모델생성 및 테이블 연동하기

마이그레이션을 통해 테이블을 생성하면 아래와 같이 models 폴더 내 members.js파일이 함께 생성된다.
코드를 보면 마이그레이션과 비슷한 형태로 생성된 것을 확인할 수 있다.

const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class Member extends Model {}
  Member.init(
    {
      name: DataTypes.STRING,
      team: DataTypes.STRING,
      position: DataTypes.STRING,
      emailAddress: DataTypes.STRING,
      phoneNumber: DataTypes.STRING,
      admissionDate: DataTypes.DATE,
      birthday: DataTypes.DATE,
      profileImage: DataTypes.STRING,
    },
    {
      sequelize,
      modelName: 'Member',
    }
  );
  return Member;
};

가이드를 위한 주석과 static associate(models){} 부분을 우선 제외하면 위 코드가 남게 된다.
테이블의 정의를 보면 primary key와 createdAt, updatedAt에 대한 정의가 담겨있지 않은데, 보통 자동 생성되는 영역이므로 이 곳에 정의되지 않는게 일반적이다. 특별한 이유가 없다면 굳이 없어도 상관 없다. 하지만 Member 데이터 생성(create) 시 id를 직접 입력한다거나 부가적인 기능 추가가 있을 경우 아래처럼 추가해줄 필요가 있다.

Member.init(
    {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: DataTypes.INTEGER,
      },
      name: DataTypes.STRING,
      team: DataTypes.STRING,
      position: DataTypes.STRING,
      emailAddress: DataTypes.STRING,
      phoneNumber: DataTypes.STRING,
      admissionDate: DataTypes.DATE,
      birthday: DataTypes.DATE,
      profileImage: DataTypes.STRING,
    },
    ...
)

위 id 정보는 마이그레이션에서 그대로 가져왔으며 type의 Sequelize.INTEGER 부분을 DataTypes.INTEGER로 바꿔주면 된다.

이제 index.js파일을 통해 app.js와 모델을 연동하기 위한 작업을 진행한다.
빠른 이해를 돕기 위해 index.js에 이미 작성된 기존 코드값을 지우고 아래 코드를 작성한다.

const Sequelize = require('sequelize');
const config = require('../config/config.json');

const { username, password, database, host, dialect } = config.development;
const sequelize = new Sequelize(database, username, password, {
  host,
  dialect,
});

const Member = require('./member')(sequelize, Sequelize.DataTypes);


const db = {};
db.Member = Member;

module.exports = db;

sequelize와 config.json을 불러온 뒤
먼저 config.development에 사전에 적어둔 데이터베이스 관련 정보를 불러옵니다.
그리고 나서 Sequelize 객체를 생성하는데 활용하고,
Member 객체를 불러옵니다. 이 때 member.js에서 공개된 함수의 실행을 통해 초기 설정을 마무리합니다.
이제 db라는 오브젝트로 담아 외부에 공개합니다.

최종적으로 app.py에 모델을 불러옵니다.
이때 신기한 점은 require에서 models 디렉토리만 선택했지만 자동으로 하위 파일인 index.js을 찾아서 db 오브젝트를 불러 올 수 있다는 점입니다.

💡 Tip
index.js파일을 자동으로 찾는 것은 node의 특징입니다.

// app.py

const db = require('./models');
const { Member } = db;'

이제 모델과 데이터베이스를 연동하였고, 모델을 통해 데이터베이스를 수정할 수 있게 되었습니다.

 

ORM 방식으로 MySQL 데이터베이스에 GET 요청하기

아래 코드는 전체 데이터와 필터링에 대한 GET 방식을 구현한다.

const express = require('express');
const app = express();
const db = require('./models');
const { Member } = db;

app.use(express.json());

app.get('/api/members', async (req, res) => {
  const { team } = req.query;
  if (team) {
    // const teamMembers = await Member.findAll({ where: { team: team } });
    const teamMembers = await Member.findAll({ where: { team } }); // 위와 동일
    res.send(teamMembers);
  } else {
    const members = await Member.findAll();
    res.send(members);
  }
});

app.listen(3000, () => {
  console.log('Server is listening...');
});

위 코드에서 데이터베이스를 가져오는 방법과 데이터를 찾는 방법에 주목할 필요가 있다.
먼저 위에서도 언급했지만 require를 통해 models 디렉토리를 가져오면 하위파일 중 index.js를 자동으로 가져오게 되고(node.js의 특징), index.js는 db라는 변수를 export하고 있어 결론적으로 db 변수에 담는다.
이후 db 안에 담긴 Member라는 객체(테이블)를 가져온 뒤 findAll 메서드로 데이터를 가져온다.

💡 Tip
findAll 메소드의 where에서 {team: team}을 Shorthand Property Names 기법을 적용한 것을 볼 수 있다.
이를 통해 찾고자 하는 key의 값과 확인할 데이터의 변수명이 동일할 경우 하나로 쓸 수 있다.

sequelize ORM find의 또다른 기능들

// 하나만 찾기
const member = await Member.findOne({ where: { id } });
res.send(member);

// 기본 정렬
const members = await Member.findAll({ order: [['admissionDate', 'DESC'], ['team', 'ASC']] });
res.send(members);

sequelize ORM find 결과의 실체

위 예시와 같이 find메소드와 추가 옵션을 덧붙여 받은 결과를 res.send(members)로 리스폰하고 있다.
여기서 만약 members를 console.log()로 출력해서 확인할 경우 단순 오브젝트 형태의 정보가 아니라는 점을 확인할 수 있다.

// members의 실제 결과

Member {
  dataValues: {
    id: 1,
    name: 'Alex',
    team: 'engineering',
    position: 'Server Developer',
    emailAddress: 'alex@google.com',
    phoneNumber: '010-xxxx-xxxx',
    admissionDate: 2018-12-10T00:00:00.000Z,
    birthday: 1994-11-08T00:00:00.000Z,
    profileImage: 'profile1.png',
    createdAt: 2023-06-02T02:42:07.000Z,
    updatedAt: 2023-06-02T02:42:07.000Z
  },
  _previousDataValues: {
    id: 1,
    name: 'Alex',
    team: 'engineering',
    position: 'Server Developer',
    emailAddress: 'alex@google.com',
    phoneNumber: '010-xxxx-xxxx',
    admissionDate: 2018-12-10T00:00:00.000Z,
    birthday: 1994-11-08T00:00:00.000Z,
    profileImage: 'profile1.png',
    createdAt: 2023-06-02T02:42:07.000Z,
    updatedAt: 2023-06-02T02:42:07.000Z
  },
  uniqno: 1,
  _changed: Set(0) {},
  _options: {
    isNewRecord: false,
    _schema: null,
    _schemaDelimiter: '',
    raw: true,
    attributes: [
      'id',            'name',
      'team',          'position',
      'emailAddress',  'phoneNumber',
      'admissionDate', 'birthday',
      'profileImage',  'createdAt',
      'updatedAt'
    ]
  },
  isNewRecord: false
}

원래 members 변수는 위 결과처럼 하나의 객체 형태를 response한다. 여기서 실제 우리가 필요한 데이터는 dataValues 프로퍼티이다.
그럼에도 우리가 dataValues 프로퍼티에 직접적으로 접근하지 않고서도 원하는 결과를 얻을 수 있었던 이유는 바로 send 메서드가 이를 자동으로 처리해줬기 때문이다.
자동 처리된 과정은 바로 .toJSON() 메서드를 적용하는 것으로, 이는 Member 클래스에서 dataValues 객체를 가져오는 역할을 하는데 만약 send 메서드로 모델 클래스를 보내준다면 이 과정의 생략이 가능하다.

데이터 추가를 위한 POST 요청

이전 예시들을 통해 ORM 메서드에 대한 설명이 충분했으므로 코드만 보고 파악해보자

app.post('/api/members', async (req, res) => {
  const newMember = req.body;
  const member = Member.build(newMember);
  await member.save()
  res.send(member)
})

빌드 후 저장 방식으로 새 데이터를 추가하는 것을 확인할 수 있다.

app.post('/api/members', async (req, res) => {
  const newMember = req.body;
  const member = await Member.create(newMember);
  res.send(member);
});

build와 save 메서드를 합친 create 메서드를 써도 동일한 결과를 얻을 수 있다.
하지만 만약에 빌드 후 수정이 필요한 상황이라면 build, save로 구분하서 쓰면 된다.

데이터 수정을 위한 PUT 요청

sequealize의 update 결과로 [수정된 row의 개수, 수정된 row의 data]가 반환된다. 공식 문서에 따르면 결과 배열의 두번째 요소는 postgreSQL에서만 지원한다고 한다.
어쨌든 result[0]은 수정 성공 개수를 의미하므로 아래와 같이 활용한다.

app.put('/api/members/:id', async (req, res) => {
  const id = req.params.id;
  const newInfo = req.body;
  const result = await Member.update(newInfo, { where: { id } });
  if (result[0]) {
    res.send({ message: `${result[0]} row(s) is affected` });
  } else {
    res.status(404).send({ message: 'There is no member with id' });
  }
});

참고로 변경할 사항 즉 req.body에 보낼 json 데이터에는 모든 컬럼이 아닌 변경할 컬럼과 값만 지정해줘도 된다.

// 또 다른 수정 방법
app.put('/api/members/:id', async (req, res) => {
  const id = req.params.id;
  const newInfo = req.body;
  const member = await Member.findOne({ where: { id } });
  if (member) {
    Object.keys(newInfo).forEach((prop) => {
      member[prop] = newInfo[prop];
    });
    await member.save();
    res.send(member);
  } else {
    res.status(404).send({ message: 'There is no member with id' });
  }
});

또 다른 수정 방법으로는 위 코드와 같이 수정하고자 하는 데이터를 findOne메소드를 불러와 직접적으로 수정한 뒤 다시 저장하는 방법을 취할 수도 있다.

데이터 삭제를 위한 DELETE 요청

DELETE의 기능으로 destroy메서드를 사용하고 있으며, 데이터 삭제의 결과로 삭제된 row의 수를 출력하여 update메서드와 같이 성공 유무의 기준으로 사용하고 있다.

app.delete('/api/member/:id', async (req, res) => {
  const id = req.params.id;
  const deletedCount = Member.destroy({ where: { id } });
  if (deletedCount) {
    res.send({ message: `${deletedCount} row(s) deleted` });
  } else {
    res.status(404).send({ message: 'There is no member with id' });
  }
});
728x90
728x90