7. MySQL
포스트
취소

7. MySQL

기존 블로그에 작성했던 포스트를 이전한 글입니다.

해당 포스트는 NODEJS를 학습하며 정리한 내용에 대한 포스트입니다.




🌈 MySQL

💻 데이터베이스

🍳 데이터베이스란?

지금까지는 데이터를 서버 메모리에 저장했음

  • 서버를 재시작하면 데이터도 사라져버림 -> 영구적으로 저장할 공간 필요

  • MySQL 관계형 데이터베이스 사용

  • 데이터베이스: 관련성을 가지며 중복이 없는 데이터들의 집합

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

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

  • 서버의 하드 디스크나 SSD 등의 저장 매체에 데이터를 저장

  • 서버 종료 여부와 상관 없이 데이터를 계속 사용할 수 있음

  • 여러 사람이 동시에 접근할 수 있고, 권한을 따로 줄 수 있음




💻 MySQL 설치하기

🍳 MySQL 접속해보기

MySQL의 설치에 대한 자세한 내용은 따로 포스팅할 예정입니다.

콘솔(CMD)에서 MySQL이 설치된 경로로 이동

  • 기본 경로는 C:\Program Files\MySQL\MySQL Server 8.0\bin

  • -h는 호스트, -u는 사용자 –p는 비밀번호 의미

-h`는 호스트, `-u`는 사용자 `–p`는 비밀번호 의미

  • 프롬프트가 mysql>로 바뀐다면 성공

  • 프롬프트를 종료하려면 exit 입력

프롬프트를 종료하려면 exit 입력


🍳 MySQL 서버 주소 입력

Connection Name에 localhost 적고 비밀번호 입력

Connection Name에 localhost 적고 비밀번호 입력


🍳 localhost 서버 접속하기

아까 설정했던 비밀번호를 입력하여 접속

localhost 서버 접속하기

접속이 되었다면 성공




💻 데이터베이스 및 테이블 생성하기

🍳 데이터베이스 생성하기

콘솔에서 MySQL 프롬프트에 접속

CREATE SCHEMA nodejs;로 nodejs 데이터베이스 생성

use nodejs;로 생성한 데이터베이스 선택

1
2
3
4
mysql> CREATE SCHEMA `nodejs` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;
Query OK, 1 row affected (0.01sec)
mysql> use nodejs;
Database changed
  • DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci: 한글과 이모티콘을 사용

  • utf8mb4: 데이터베이스에서 한글과 이모티콘을 사용

COLLATE : CHARACTER SET을 어떤 형식으로 정렬할 것인지를 의미

MySQL 8 버전의 기본 COLLATE는 utf8mb4_0900_ai_ci이지만 한글 문제가 있으므로 여기서는 utf8mb4_general_ci를 사용한다.

SQL 구문을 입력할 때는 마지막에 세미콜론(;)을 붙여야 해당 구문이 실행

  • 세미콜론을 붙이지 않으면 프롬프트가 다음 줄로 넘어가서 다른 입력이 들어오기를 계속 대기 상태

CREATE SCHEMA와 같이 MySQL이 기본적으로 알고 있는 구문은 예약어

  • 예약어는 소문자로 써도 되지만, 가급적 대문자로 쓰는 것이 좋다.

  • nodejs와 같은 사용자가 직접 만든 이름과 구분하기 위해서입니다.


🍳 워크벤치 사용 시

커넥션에 접속한 뒤 데이터베이스를 생성하기 위해 상단 메뉴의 데이터베이스 모양 버튼(하단 이미지 4번째 칸)을 누릅니다.

데이터베이스를 생성하기

데이터베이스의 이름을 nodejs로 입력하고, Charset/Collation을 각각 utf8mb4utf8mb4_general_ci로 바꾼다. 그러고 나서 Apply 버튼을 누른다.

데이터베이스 생성 창

다음과 같이 워크벤치가 저절로 SQL문을 생성해주는 것을 볼 수 있다.

nodejs라는 이름의 데이터베이스를 생성하라는 명령어다.

워크벤치가 자동으로 생성해주는 SQL들은 알아두는 게 좋다.

다만, Collation에 관련된 명령어가 없는 경우에는 다음 그림과 같이 DEFAULT COLLATE utf8mb4_general_ci를 직접 적어넣는다. Apply 버튼을 눌러 명령어를 실행한다.

데이터베이스 생성 SQL

Apply 버튼을 누르면 SQL 적용 화면으로 넘어간다.

여기서 체크박스에 체크 표시를 하고 Finish 버튼을 누르면 된다.

SQL 적용 화면

실행 완료 후 왼쪽 하단에서 Schemas 탭을 누르면 다음 그림과 같이 nodejs라는 데이터베이스가 생성된 것을 볼 수 있다.

생성 완료 후 화면


🍳 테이블 생성하기

MySQL 프롬프트에서 테이블 생성

1
CREATE TABLE [데이터베이스명.테이블명] --으로 테이블 생성

사용자 정보를 저장하는 테이블

1
2
3
4
5
6
7
8
9
10
11
12
mysql> CREATE TABLE nodejs.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 = '사용자 정보'
    -> ENGINE = InnoDB;
-- Query OK, 0 row affected (0.09 sec)

컬럼과 로우

데이터베이스에서는 id, name, age, married로 시작되는 세로줄을 컬럼(column)이라고 한다.

  • age 컬럼에는 24, 32, 28처럼 모두 age에 관한 정보만 들어 있다.

반대로 1, zero, 24, false 같은 가로줄은 로우(row)라고 한다.

  • 컬럼명을 제외하면 현재 로우가 세 개 들어 있다.

컬럼과 로우가 교차하는 칸 하나는 필드라고 한다.

  • 컬럼은 세로 필드의 집합
  • 로우는 가로 필드의 집합

테이블에 데이터를 넣을 때는 미리 컬럼을 정의해두고, 컬럼에 맞춰 데이터를 넣으면 된다.


컬럼의 자료형

  • INT
    • 정수를 의미
    • 소수까지 저장하고 싶다면 FLOAT이나 DOUBLE 자료형을 사용
  • VARCHAR(자릿수), CHAR(자릿수)
    • CHAR는 고정 길이이고, VARCHAR는 가변 길이
    • 예를 들어 CHAR(10)이면 반드시 길이가 10인 문자열만 넣어야 하고, VARCHAR(10)일 경우에는 길이가 0~10인 문자열을 넣을 수 있다.
    • CHAR에 주어진 길이보다 짧은 문자열을 넣는다면 부족한 자릿수만큼 스페이스가 채워진다.
  • TEXT
    • 긴 글을 저장할 때 사용
    • VARCHAR와 헷갈릴 수 있다.
    • 수백 자 이내의 문자열은 보통 VARCHAR로 많이 처리하고, 그보다 길면 TEXT로 처리하곤 한다.
  • TINYINT
    • -128부터 127까지의 정수를 저장할 때 사용
    • 1 또는 0만 저장한다면 불 값(Boolean)과 같은 역할을 할 수 있다.
  • DATETIME
    • 날짜와 시간에 대한 정보
    • 날짜 정보만 담는 DATE
    • 시간 정보만 담는 TIME
    • 이외에도 많은 자료형이 있으나 이 정도가 자주 쓰인다.


컬럼의 옵션

  • NULL과 NOT NULL
    • 빈칸을 허용할지 여부를 묻는 옵션
    • comment 컬럼만 NULL이고, 나머지는 모두 NOT NULL
    • 자기소개를 제외한 나머지 컬럼은 반드시 로우를 생성할 때 데이터를 입력해야 한다.
  • AUTO_INCREMENT
    • 숫자를 저절로 올리겠다는 뜻
    • 예를 들어 처음에 Zero라는 사람의 데이터를 넣으면 MySQL은 알아서 id로 1번을 부여
    • 다음에 Nero라는 사람의 데이터를 넣으면 자동으로 id 2번을 부여
    • 이를 가능하게 하는 옵션이 AUTO_INCREMENT
  • UNSIGNED
    • 숫자 자료형에 적용되는 옵션
    • 숫자 자료형은 기본적으로 음수 범위를 지원
    • 예를 들어 INT는 -2147483648~2147483647까지의 숫자를 저장
    • 만약 UNSIGNED가 적용되어 있다면 음수는 무시되고 0~4294967295까지 저장
    • FLOAT과 DOUBLE에는 UNSIGNED 적용이 불가능
    • 나이처럼 음수가 나올 수 없는 컬럼은 체크해두는 것이 좋다.
  • ZEROFILL
    • 숫자의 자릿수가 고정되어 있을 때 사용
    • 가끔 자료형으로 INT 대신 INT(자릿수)처럼 표현하는 경우
    • 이때 ZEROFILL을 설정해둔다면 비어 있는 자리에 모두 0을 넣는다.
    • 예를 들어 INT(4)인데 숫자 1을 넣었다면 0001이 되는 식
    • 실습 예제에서는 age 컬럼에 UNSIGNED 옵션만 주었다.
  • DEFAULT now()
    • 데이터베이스 저장 시 해당 컬럼에 값이 없을 때 MySQL이 기본값을 대신 넣는다.
    • now()는 현재 시각을 넣으라는 뜻
    • now() 대신 CURRENT_TIMESTAMP를 적어도 같은 뜻
    • 사용자 정보를 넣으면 created_at 컬럼에는 넣는 순간의 시각이 자동으로 기록
  • PRIMARY KEY
    • 해당 컬럼이 기본 키인 경우에 PRIMARY KEY 옵션을 설정
    • 기본 키란 로우를 대표하는 고유한 값을 의미
    • 데이터베이스에 데이터를 넣을 때 로우 단위로 넣는다.
    • 이때 로우들을 구별할 고유한 식별자가 필요하다.
    • 이름, 나이, 결혼 여부 컬럼은 다른 사람과 내용이 겹칠 수 있다.
    • 따라서 id라는 새로운 컬럼을 하나 만들어 고유한 번호를 부여한 것
    • 주민등록번호나 학번과 비슷한 개념
    • MySQL에는 PRIMARY KEY(id)라는 옵션으로 id 컬럼이 기본 키임을 알렸다.
  • UNIQUE INDEX
    • 는 해당 값이 고유해야 하는지에 대한 옵션
    • name 컬럼이 해당
    • 인덱스의 이름은 name_UNIQUE로, name 컬럼을 오름차순(ASC)으로 기억하겠다는 것
    • 내림차순은 DESC
    • PRIMARY KEY나 UNIQUE INDEX의 경우에는 데이터베이스가 별도로 컬럼을 관리하므로 조회 시 속도가 빨라진다.
    • 기본 키인 id도 사실 고유해야 하지만, PRIMARY KEY는 자동으로 UNIQUE INDEX를 포함하므로 따로 적지 않아도 된다.


테이블 옵션

COMMENT

  • 테이블에 대한 보충 설명(필수 아님)

DEFAULT CHARSET

  • utf8로 설정해야 한글이 입력됨(utf8mb4 하면 이모티콘 가능)

ENGINE

  • MyISAM과 InnoDB가 제일 많이 사용

  • 엔진별로 기능 차이 존재


만들어진 테이블 확인

1
DESC [테이블명];


테이블 삭제

1
DROP TABLE [테이블명];


사용자의 댓글을 저장하는 테이블 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> CREATE TABLE nodejs.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 nodejs.users (id)
    -> ON DELETE CASCADE
    -> ON UPDATE CASCADE)
    -> COMMENT = '댓글'
    -> ENGINE=InnoDB;
Query OK, 0 row affected (0.09 sec)


외래키(foreign key)

댓글 테이블은 사용자 테이블과 관계가 있음(사용자가 댓글을 달기 때문)

  • 외래키를 두어 두 테이블이 관계가 있다는 것을 표시
1
2
FOREIGN KEY (컬럼명) REFERENCES 데이터베이스.테이블명 (컬럼)
FOREIGN KEY (commenter) REFERENCES nodejs.users (id)
  • 댓글 테이블에는 commenter 컬럼이 생기고 사용자 테이블의 id값이 저장됨

  • ON DELETE CASCADE, ON UPDATE CASCADE

    • 사용자 테이블의 로우가 지워지고 수정될 때 댓글 테이블의 연관된 로우들도 같이 지워지고 수정됨


테이블 목록 보기

1
SHOW TABLES;

테이블 목록 보기




💻 CRUD 작업하기

🍳 CRUD

CREATE, READ, UPDATE, DELETE의 두문자어

CRUD

🍳 CREATE

1
INSERT INTO [테이블명] ([컬럼1], [컬럼2], .. .) VALUES ([1], [ 2], ...)

CREATE


🍳 READ

1
SELECT 컬럼 FROM 테이블명

SELECT * 은 모든 컬럼을 선택한다는 의미

READ

컬럼만 따로 추리는 것도 가능

READ

WHERE로 조건을 주어 선택 가능

  • AND로 여러가지 조건을 동시에 만족하는 것을 찾음

AND로 여러가지 조건을 동시에 만족하는 것을 찾음

  • OR로 여러가지 조건 중 하나 이상을 만족하는 것을 찾음

OR로 여러가지 조건 중 하나 이상을 만족하는 것을 찾음

정렬해서 찾기

  • ORDER BY로 특정 컬럼 값 순서대로 정렬 가능

  • DESC는 내림차순, ASC 오름차순

정렬해서 찾기

LIMIT, OFFSET

LIMIT으로 조회할 개수 제한

LIMIT으로 조회할 개수 제한

OFFSET으로 앞의 로우들 스킵 가능(OFFSET 2면 세 번째 것부터 찾음)

OFFSET으로 앞의 로우들 스킵 가능(OFFSET 2면 세 번째 것부터 찾음)


🍳 UPDATE

데이터베이스에 있는 데이터를 수정하는 작업

1
UPDATE [테이블명] SET [컬럼명=바꿀 ] WHERE [조건]

UPDATE


🍳 DELETE

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

DELETE




💻 시퀄라이즈 사용하기

🍳 시퀄라이즈 ORM

SQL 작업을 쉽게 할 수 있도록 도와주는 라이브러리

  • ORM: Object Relational Mapping: 객체와 데이터를 매핑(1대1 짝지음)

  • MySQL 외에도 다른 RDB(Maria, Postgre, SQLite, MSSQL)와도 호환됨

  • 자바스크립트 문법으로 데이터베이스 조작 가능

소스코드 참고

프로젝트 세팅 후, 콘솔을 통해 경로로 이동한 후 package.json 작성

🔻 pakage.json

1
2
3
4
5
6
7
8
9
10
11
{
  "name": "learn-sequelize",
  "version": "0.0.1",
  "description": "시퀄라이즈를 배우자",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "ZeroCho",
  "license": "MIT"
}

시퀄라이즈에 필요한 sequelize와 sequelize-cli, mysql2 패키지를 설치

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

sequelize-cli는 시퀄라이즈 명령어를 실행하기 위한 패키지

mysql2는 MySQL과 시퀄라이즈를 이어주는 드라이버

mysql2 자체는 데이터베이스 프로그램이 아님

설치 완료 후 sequelize init 명령어를 호출하면 된다.

전역 설치 없이 명령어로 사용하려면 앞에 npx를 붙이면 된다.

1
npx sequelize init


🍳 MySQL 연결하기

시퀄라이즈를 통해 익스프레스 앱과 MySQL을 연결

app.js를 생성하고 익스프레스와 시퀄라이즈 연결 코드를 작성

🔻 app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const nunjucks = require('nunjucks');

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

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 })
  .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'), '번 포트에서 대기 중');
});

require('./models')require('./models/index.js')와 같다.

폴더 내의 index.js 파일은 require할 때 이름을 생략할 수 있다.

db.sequelize를 불러와서 sequelize.sync 메서드를 사용해 서버를 실행할 때 MySQL과 연동

내부에 force: false 옵션이 있는데, 이 옵션을 true로 설정하면 서버를 실행할 때마다 테이블을 재생성

테이블을 잘못 만든 경우에 true로 설정하면 된다.

MySQL과 연동할 때는 config 폴더 안의 config.json 정보가 사용된다.

다음과 같이 수정

만약 자동으로 생성한 config.json에 operatorAliases 속성이 들어 있다면 삭제

DB 연결 정보를 넣기

🔻 config/config.json

1
2
3
4
5
6
7
8
9
10
{
  "development": {
    "username": "root",
    "password": "[root 비밀번호]",
    "database": "nodejs",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
...
}


연결 테스트하기

npm start로 실행해서 SELECT 1+1 AS RESULT가 나오면 연결 성공

1
2
3
4
5
6
7
8
9
10
11
12
$ npm start
> learn-sequelize@0.0.1 start 
> nodemon app

[nodemon] 2.0.16
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
3001 번 포트에서 대기 중
Executing (default): SELECT 1+1 AS result
데이터베이스 연결 성공


🍳 모델 정의하기

MySQL에서 정의한 테이블을 시퀄라이즈에서도 정의해야 한다.

MySQL의 테이블은 시퀄라이즈의 모델과 대응된다.

시퀄라이즈는 모델과 MySQL의 테이블을 연결해주는 역할

시퀄라이즈는 기본적으로 모델 이름은 단수형으로, 테이블 이름은 복수형으로 사용

🔻 models/user.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const Sequelize = require('sequelize');

class User extends Sequelize.Model {
  static initiate(sequelize) {
    User.init({
      name: {
        type: Sequelize.STRING(20),
        allowNull: false,
        unique: true,
      },
      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,
      timestamps: false,
      underscored: false,
      modelName: 'User',
      tableName: 'users',
      paranoid: false,
      charset: 'utf8',
      collate: 'utf8_general_ci',
    });
  }

  static associate(db) {}
};

module.exports = User;


모델 옵션들

User 모델을 만들고 모듈로 exports

User 모델은 Sequelize.Model을 확장한 클래스로 선언

클래스 문법을 사용하지만 클래스에 대한 지식이 없어도 사용할 수 있다. 패턴만 숙지

모델은 크게 static initiate 메서드와 static associate 메서드로 나뉘어진다.

static initiate 메서드에는 테이블에 대한 설정

static associate 메서드에는 다른 모델과의 관계

static initiate 메서드부터 살펴보면, 모델.init 메서드의 첫 번째 인수가 테이블 컬럼에 대한 설정이고, 두 번째 인수가 테이블 자체에 대한 설정

시퀄라이즈는 알아서 id를 기본 키로 연결하므로 id 컬럼은 적어줄 필요가 없다.

MySQL 테이블과 컬럼 내용이 일치해야 정확하게 대응

시퀄라이즈의 자료형은 MySQL의 자료형과는 조금 다르다.

  • sequelize
    • static initiate 메서드의 매개변수와 연결되는 옵션
    • db.sequelize 객체를 넣어야 한다.
  • timestamps
    • true면 createdAt(생성 시간), updatedAt(수정 시간) 컬럼을 자동으로 만듦
    • 예제에서는 직접 created_at 컬럼을 만들었으므로 false로 함
  • paranoid
    • true면 deletedAt(삭제 시간) 컬럼을 만듦
    • 로우 복구를 위해 * 완전히 삭제하지 않고 deletedAt에 표시해둠
  • underscored
    • 카멜케이스로 생성되는 컬럼을 스네이크케이스로 생성
  • modelName
    • 모델 이름, 노드 프로젝트에서 사용, tableName 옵션은 테이블 이름을 설정
  • charsetcollate
    • 한글 설정을 위해 필요
    • utf8utf8_general_ci로 설정해야 한글이 입력
    • 이모티콘까지 입력할 수 있게 하고 싶다면 utf8mb4utf8mb4_general_ci를 입력

MySQL과 시퀄라이즈의 비교


댓글 모델 생성하기

🔻 models/comment.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const Sequelize = require('sequelize');

class Comment extends Sequelize.Model {
  static initiate(sequelize) {
    Comment.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) {
    db.Comment.belongsTo(db.User, { foreignKey: 'commenter', targetKey: 'id' });
  }
};

module.exports = Comment;

index.js에 모델 연결

  • init으로 sequelize와 연결

  • associate로 관계 설정

🔻 models/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Sequelize = require('sequelize');
const User = require('./user');
const Comment = require('./comment');
...
db.sequelize = sequelize;

db.User = User;
db.Comment = Comment;

User.initiate(sequelize);
Comment.initiate(sequelize);

User.associate(db);
Comment.associate(db);

module.exports = db;


🍳 관계 정의하기

사용자 한 명은 댓글을 여러 개 작성할 수 있다. 하지만 댓글 하나에 사용자(작성자)가 여러 명일 수는 없다.

이러한 관계를 일대다(一對多)(이하 1:N) 관계라고 한다.

1:N 관계에서는 사용자가 1이고, 댓글이 N이다.

다른 관계로 일대일, 다대다 관계가 있다.

일대일 관계로는 사용자와 사용자에 대한 정보 테이블을 예로 들 수 있다.

사용자 한 명은 자신의 정보를 담고 있는 테이블과만 관계가 있다.

정보 테이블도 한 사람만을 가리킵니다. 이러한 관계를 일대일(이후 1:1) 관계라고 한다.

MySQL에서는 JOIN이라는 기능으로 여러 테이블 간의 관계를 파악해 결과를 도출한다.

시퀄라이즈는 JOIN 기능도 알아서 구현한다. 대신 테이블 간에 어떠한 관계가 있는지 시퀄라이즈에 알려야 한다.


1:N

1:N 관계 (사용자 한 명이 댓글 여러 개 작성)

시퀄라이즈에서는 1:N 관계를 hasMany로 표현(사용자.hasMany(댓글))

반대의 입장에서는 belongsTo(댓글.belongsTo(사용자))

belongsTo가 있는 테이블에 컬럼이 생김(댓글 테이블에 commenter 컬럼)

1:N

시퀄라이즈 용어 설명


1:1

hasOne 메서드를 사용

사용자 정보를 담고 있는 가상의 Info 모델이 있다고 하면 다음과 같이 표현할 수 있다.

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

1:1 관계라고 해도 belongsTo와 hasOne이 반대이면 안된다.

belongsTo를 사용하는 Info 모델에 UserId 컬럼이 추가되기 때문

1:1 관계


N대M 관계

다대다 관계

예) 게시글과 해시태그 테이블

하나의 게시글이 여러 개의 해시태그를 가질 수 있고 하나의 해시태그가 여러 개의 게시글을 가질 수 있음

DB 특성상 다대다 관계는 중간 테이블이 생김

N대M 관계

N대M 관계 테이블


🍳 쿼리 알아보기

시퀄라이즈로 CRUD 작업을 하려면 먼저 시퀄라이즈 쿼리를 알아야 한다.

SQL문을 자바스크립트로 생성하는 것이라 시퀄라이즈만의 방식이 있다.

쿼리는 프로미스를 반환하므로 then을 붙여 결괏값을 받을 수 있다.

async/await 문법과 같이 사용할 수도 있다.

로우를 생성하는 쿼리부터 알아보자.

🔻 SQL

1
INSERT INTO nodejs.users (name, age, married, comment) VALUES ('zero', 24, 0, '자기소개1');

🔻 sequelize

1
2
3
4
5
6
7
const { User } = require('../models');
User.create({
  name: 'zero',
  age: 24,
  married: false,
  comment: '자기소개1',
});

models 모듈에서 User 모델을 불러와 create 메서드를 사용

앞으로 나오는 모든 메서드는 User 모델을 불러왔다는 전제하에 소개

한 가지 주의할 점은 데이터를 넣을 때 MySQL의 자료형이 아니라 시퀄라이즈 모델에 정의한 자료형대로 넣어야 한다.

이것이 married가 0이 아니라 false인 이유

시퀄라이즈가 알아서 MySQL 자료형으로 바꿔준다.

자료형이나 옵션에 부합하지 않는 데이터를 넣었을 때는 시퀄라이즈 에러 발생

이번에는 로우를 조회하는 쿼리들

users 테이블의 모든 데이터를 조회하는 SQL문. findAll 메서드를 사용

🔻 SQL

1
SELECT * FROM nodejs.users LIMIT 1;

🔻 Sequelize

1
User.findOne({});

attributes 옵션을 사용해서 원하는 컬럼만 가져올 수도 있다.

🔻 SQL

1
SELECT name, married FROM nodejs.users;

🔻 Sequelize

1
2
3
User.findAll({
  attributes: ['name', 'married'],
});

where 옵션이 조건들을 나열하는 옵션

🔻 SQL

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

🔻 Sequelize

1
2
3
4
5
6
7
8
9
const { Op } = require('sequelize');
const { User } = require('../models');
User.findAll({
  attributes: ['name', 'age'],
  where: {
    married: true,
    age: { [Op.gt]: 30 },
  },
});

MySQL에서는 undefined라는 자료형을 지원하지 않으므로 where 옵션에는 undefined가 들어가면 안 된다.

빈 값을 넣고자 하면 null을 대신 사용

자주 쓰이는 연산자로는 Op.gt(초과), Op.gte(이상), Op.lt(미만), Op.lte(이하), Op.ne(같지 않음), Op.or(또는), Op.in(배열 요소 중 하나), Op.notIn(배열 요소와 모두 다름) 등이 있다.

Op.or 예

🔻 SQL

1
SELECT id, name FROM users WHERE married = 0 OR age > 30;

🔻 Sequelize

1
2
3
4
5
6
7
8
const { Op } = require('sequelize');
const { User } = require('../models');
User.findAll({
  attributes: ['id', 'name'],
  where: {
    [Op.or]: [{ married: false0 }, { age: { [Op.gt]: 30 } }],
  },
});

Op.or 속성에 OR 연산을 적용할 쿼리들을 배열로 나열

🔻 SQL

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

🔻 Sequelize

1
2
3
4
User.findAll({
  attributes: ['id', 'name'],
  order: [['age', 'DESC']],
});

시퀄라이즈의 정렬 방식. order 옵션으로 가능

배열 안에 배열이 있다는 점에 주의

정렬은 꼭 컬럼 하나로만 하는 게 아니라 컬럼 두 개 이상으로 할 수도 있기 때문


조회할 로우 개수를 설정하는 방법

🔻 SQL

1
SELECT id, name FROM users ORDER BY age DESC LIMIT1;

🔻 Sequelize

1
2
3
4
5
User.findAll({
  attributes: ['id', 'name'],
  order: [['age', 'DESC']],
  limit: 1,
});

limit 옵션으로 가능

OFFSET도 역시 offset 속성으로 구현할 수 있다.

🔻 SQL

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

🔻 Sequelize

1
2
3
4
5
6
User.findAll({
  attributes: ['id', 'name'],
  order: ['age', 'DESC'],
  limit: 1,
  offset: 1,
});


로우를 수정하는 쿼리

🔻 SQL

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

🔻 Sequelize

1
2
3
4
5
User.update({
  comment: '바꿀 내용',
}, {
  where: { id: 2 },
});

update 메서드로 수정 첫 번째 인수는 수정할 내용이고, 두 번째 인수는 어떤 로우를 수정할지에 대한 조건 where 옵션에 조건들을 적는다.


로우를 삭제하는 쿼리는 다음과 같다.

🔻 SQL

1
DELETE FROM nodejs.users WHERE id = 2;

🔻 Sequelize

1
2
3
User.destory({
  where: { id: 2 },
});

destroy 메서드로 삭제

where 옵션에 조건들을 적는다.

관계 쿼리

findOne이나 findAll 메서드를 호출할 때 프로미스의 결과로 모델을 반환(findAll은 모두 찾는 것이므로 모델의 배열을 반환).

1
2
const user = await User.findOne({});
console.log(user.nick); // 사용자 닉네임

특정 사용자를 가져오면서 그 사람의 댓글까지 모두 가져오고 싶다면 include 속성을 사용

1
2
3
4
5
6
const user = await User.findOne({
  include: [{
    model: Comment,
  }]
});
console.log(user.Comments); // 사용자 댓글

어떤 모델과 관계가 있는지를 include 배열에 넣어주면 된다.

배열인 이유는 다양한 모델과 관계가 있을 수 있기 때문

댓글은 여러 개일 수 있으므로(hasMany) user.Comments로 접근 가능 또는 다음과 같이 댓글에 접근할 수도 있다.

1
2
3
const user = await User.findOne({});
const comments = await user.getComments();
console.log(comments); // 사용자 댓글

관계를 설정했다면 getComments(조회) 외에도 setComments(수정), addComment(하나 생성), addComments(여러 개 생성), removeComments(삭제) 메서드를 지원

동사 뒤에 모델의 이름이 붙는 형식

동사 뒤의 모델 이름을 바꾸고 싶다면 관계를 설정할 때 as 옵션을 사용할 수 있다.

1
2
3
4
5
6
// 관계 설정할 때 as로 등록
db.User.hasMany(db.Comment, { foreignKey: 'commenter', sourceKey: 'id', as: 'Answers' });
// 쿼리할 때는
const user = await User.findOne({});
const comments = await user.getAnswers();
console.log(comments); // 사용자 댓글

as를 설정하면 include할 때 추가되는 댓글 객체도 user.Answers로 바뀐다.

include나 관계 쿼리 메서드에도 where이나 attributes 같은 옵션을 사용 가능

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const user = await User.findOne({
  include: [{
    model: Comment,
    where: {
      id: 1,
    },
    attributes: ['id'],
  }]
});
// 또는
const comments = await user.getComments({
  where: {
    id: 1,
  },
  attributes: ['id'],
});

관계 쿼리 시 조회는 위와 같이 하지만 수정, 생성, 삭제 때는 조금 다른 점이 있다.

여러 개를 추가할 때는 배열로 추가 가능

수정은 set+모델명, 삭제는 remove+모델명

1
2
3
4
5
const user = await User.findOne({});
const comment = await Comment.create(); 
await user.addComment(comment);
// 또는
await user.addComment(comment.id);
1
2
3
4
const user = await User.findOne({});
const comment1 = await Comment.create();
const comment2 = await Comment.create();
await user.addComment([comment1, comment2]);

SQL 쿼리하기

만약 시퀄라이즈의 쿼리를 사용하기 싫거나 어떻게 해야 할지 모르겠다면 직접 SQL문을 통해 쿼리할 수도 있다.

1
2
3
const [result, metadata] = await sequelize.query('SELECT * from comments');
console.log(result);

시퀄라이즈 쿼리로 할 수 없는 경우에는 위와 같이 할 수 있다.


🍳 쿼리 수행하기

소스코드 참고

🔻 public/sequelize.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// 사용자 이름을 눌렀을 때 댓글 로딩
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', () => {
        getComment(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에 연결

🔻 app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
const { sequelize } = require('./models');
const indexRouter = require('./routes');
const usersRouter = require('./routes/users');
const commentsRouter = require('./routes/comments');

const app = express();
...
app.use(express.urlencoded({ extended: false }));

app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/comments', commentsRouter);

app.use((req, res, next) => {
  const error =  new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
...

🔻 routes/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express');
const User = require('../models/user');

const router = express.Router();

router.get('/', async (req, res, next) => {
  try {
    const users = await User.findAll();
    res.render('sequelize', { users });
  } catch (err) {
    console.error(err);
    next(err);
  }
});

module.exports = router;

GET /로 접속했을 때의 라우터

User.findAll 메서드로 모든 사용자를 찾은 후, sequelize.html을 렌더링할 때 결괏값인 users를 넣는다.

시퀄라이즈는 프로미스를 기본적으로 지원하므로 async/await과 try/catch문을 사용해서 각각 조회 성공 시와 실패 시의 정보를 얻을 수 있다.

이렇게 미리 데이터베이스에서 데이터를 조회한 후 템플릿 렌더링에 사용할 수 있다.

다음은 users.js입니다. router.route 메서드로 같은 라우트 경로는 하나로 묶었다.

🔻 routes/users.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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) => {
    try {
      const users = await User.findAll();
      res.json(users);
    } catch (err) {
      console.error(err);
      next(err);
    }
  })
  .post(async (req, res, next) => {
    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;

GET /users와 POST /users 주소로 요청이 들어올 때의 라우터

각각 사용자를 조회하는 요청과 사용자를 등록하는 요청을 처리

GET /에서도 사용자 데이터를 조회했지만, GET /users에서는 데이터를 JSON 형식으로 반환한다는 것에 차이가 있다.

GET /users/:id/comments 라우터에는 findAll 메서드에 옵션이 추가되어 있다.

include 옵션에서 model 속성에는 User 모델을, where 속성에는 :id로 받은 아이디 값을 넣었다.

req.params.id로 값을 가져올 수 있다.

GET /users/1/comments라면 사용자 id가 1인 댓글을 불러온다.

조회된 댓글 객체에는 include로 넣어준 사용자 정보도 들어 있으므로 작성자의 이름이나 나이 등을 조회할 수 있다.

🔻 routes/comments.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const express = require('express');
const { 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;

댓글에 관련된 CRUD 작업을 하는 라우터

POST /comments, PATCH /comments/:id, DELETE /comments/:id를 등록했다.

POST /comments 라우터는 댓글을 생성하는 라우터

commenter 속성에 사용자 아이디를 넣어 사용자와 댓글을 연결

PATCH /comments/:id와 DELETE /comments/:id 라우터는 각각 댓글을 수정, 삭제하는 라우터

수정과 삭제에는 각각 update와 destroy 메서드를 사용

이제 npm start로 서버를 실행하고 http://localhost:3001로 접속

콘솔에는 시퀄라이즈가 수행하는 SQL문이 나오므로 어떤 동작을 하는지 확인할 수 있다.

시퀄라이즈가 수행하는 SQL문

  • Executing으로 시작하는 SQL 구문을 보고 싶지 않다면 config/config.json의 dialect 속성 밑에 “logging”: false를 추가

실행화면




💻 함께 보면 좋은 자료

데이터베이스 설명

MySQL 매뉴얼

워크벤치 매뉴얼

시퀄라이즈 문서




📚 레퍼런스

조현영. Node.js 교과서 = Node.js Textbook / 조현영 지음 (2022). Print.

[리뉴얼] Node.js 교과서 - 기본부터 프로젝트 실습까지

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.