7장 MySQL
예전에 노드로 랭킹 서버를 만들었는데 서버를 재실행할 때마다 랭킹이 초기화됐었다. 그래서 데이터베이스를 구축하고 싶었는데 어떻게 하는지 몰라서 그냥 재실행 할 때마다 깃허브에 있는 JSON 파일을 갱신해줬던 기억이 있다. 2년이나 지났으니 이제는 할 수 있기를 바란다.
7-1. 데이터베이스란?
- 데이터베이스: 관련성을 가지며 중복이 없는 데이터들의 집합
- DMBS( DataBase Management System ): 데이터베이스를 관리하는 시스템
- RDMBS (Relational DMBS): 관계형 DMBS. Oracle, MySQL, MSSQL 등이 있음
7-2. MySQL 설치, 7-3. 워크벤치 설치하기
Server File Permissions 부분은 교재에 없길래 그냥 첫 번째 옵션을 선택했다.
7-4. 데이터베이스 및 테이블 생성하기
워크벤치는 UI가 있어서 한 번 훑어보기만 해도 크게 어렵지 않을 것 같아서 실습 및 블로깅은 프롬프트로만 진행하였다.
데이터 베이스 생성하기
COLLATE는 해당 CHARCTER SET을 어떻게 정렬할 지 정하는 것으로 기본값을 사용 시 한글에서 오류가 난다고 한다.
CHARCTER SET과 COLLATE는 utf8mb4, ufg8mb4_general_ci 로 설정하면 된다.
테이블 생성하기
괄호 안의 id부터 created_at 은 각 열(컬럼, column)의 이름을 뜻한다. 각 열의 이름 옆에는 INT, VARCHAR 등이 적혀있는데 이는 자료형을 뜻한다. 각 자료형은 다음과 같다.
- INT: 정수. 소수점까지 사용하고 싶다면 FLOAT나 DOUBLE을 사용하면 된다.
- VARCHAR, CHAR: 둘 다 문자열이지만 전자는 가변 길이이고, 후자는 고정 길이이다. CHAR의 경우 주어진 길이보다 짧은 문자열이 들어오면 나머지를 공백으로 채운다.
- TEXT: 마찬가지로 문자열이지만 수백 자 이내라면 VARCHAR, 이상이라면 TEXT로 처리한다
- TINYINT: -128 ~ 127 까지의 정수를 저장한다. 1과 0만 저장하면 불 값으로 사용할 수도 있다.
- DATETIME: 날짜와 시간에 대한 정보이다. 날짜만 담는 DATE, 시간만 담는 TIME도 존재한다.
자료형 뒤에 있는 NULL, UNSIGNED 등은 옵션이다.
- NULL, NOT NULL: null의 허용 여부를 결정한다. NOT NULL이라면 행(로우, row)을 생성할 때 반드시 데이터를 입력해야 한다.
- AUTO_INCREMENT: 숫자를 자동으로 올려준다. 즉 행의 번호를 매길 수 있다.
- UNSIGNED: 숫자 자료형에서 양수만을 지원한다. FLOAT와 DOUBLE은 사용할 수 없다. 나이같이 음수가 불가능할 때 적용하면 좋다.
- ZEROFILL: 숫자의 자릿수가 고정되어 있을 때 비어있는 자리를 0으로 채운다. INT(4)에 숫자 1을 넣으면 0001이 되는 방식
- DEFAULT now(): 데이터 저장 시 해당 값이 없으면 MySQL이 대신 넣어준다. now()는 현재 시각으로 CURRENT_STAMP도 같은 역할을 한다.
- PRIMARY KEY: 각 행을 구분할 고유한 값을 설정한다. 여기서는 id로 설정한다. 리액트에서 배열을 만들 때 key라는 props를 설정해야하는 것과 비슷한 느낌인 것 같다.
- UNIQUE INDEX: 해당 값이 고유해야 하는지에 대한 옵션이다. name 컬럼을 오롬차순(ASC)으로 기억하겠다는 뜻이다. 내림차순은 DESC이다.
- INDEX: 검색 속도를 향상시키기 위해 특정 열이나 열의 조합에 대해 별도의 자료 구조를 만드는 것이다.
- PRIMARY KEY나 UNIQUE INDEX는 데이터베이스가 별도로 컬럼을 관리하므로 조회 속도가 빨라진다.
- PRIMARY KEY는 자동으로 UNIQUE INDEX를 포함하므로 따로 적지 않아도 된다.
괄호 밖에는 테이블 자체에 대한 설정을 할 수 있다.
- COMMENT: 테이블의 보충 설명을 의미한다. 테이블의 역할을 적어두면 된다.
- ENGINE: 엔진을 설정한다. MyISAM과 InnoDB가 보편적이다.
"DESC 테이블명 으로 테이블을 확인할 수 있다.
DROP TABLE 테이블명 으로 테이블을 삭제할 수도 있다.
이번엔 사용자의 댓글을 저장하는 테이블을 만든다.
commenter는 댓글을 쓴 사용자의 ID를 저장하는 곳이다. 이렇게 다른 테이블의 기본 키를 저장하는 것을 외래 키(foreign key)라고 한다.
CONSTRAINT [제약조건명] FOREIGN KEY [컬럼명] REFERENCES [참고하는 컬럼명] 으로 외래 키를 지정할 수 있다.
[제약조건명]이 뭔지 헷갈려서 조금 더 알아봤는데 외래 키에 대한 별칭을 지정하는 거라고 한다. 나중에 외래 키를 수정하거나 삭제할 때 유용하다.
ON UPDATE와 ON DELETE는 모두 CASCADE로 설정되었는데 사용자 정보가 수정되거나 삭제되면 그것과 연결된 댓글 정보도 같이 수정하거나 삭제한다는 뜻이다. 이를 설정해야 데이터가 불일치하는 현상이 나타나지 않는다.
SHOW TABLES 명령어로 테이블이 제대로 생성되었는지 확인할 수 있다.
7-5. CRUD 작업하기
7-6. 시퀄라이즈 사용하기
시퀄라이즈: MySQL 작업을 쉽게 할 수 있도록 도와주는 라이브러리. ORM(Object-relational Mapping, 자바스크립트 객체와 데이터베이스의 릴레이션을 매핑해주는 도구)으로 분류된다. 꼭 MySQL이 아니더라도 다른 데이터베이스와도 사용 가능하다.
자바스크립트 구문을 자동으로 SQL로 바꿔주기 때문에 편리하다.
프로젝트 생성
MySQL 연결하기
교재에 나와있는 익스프레스와 시퀄라이즈를 연결하는 코드는 대부분이 저번에 사용했던 것이라 시퀄라이즈를 사용하는 부분만 확인했다.
서버가 실행됐을 때 MySQL과 연동하도록 한 것이다. force는 서버를 실행할 때마다 테이블을 재생성할지의 여부를 결정한다.
MySQL과 연결할 때에는 config 폴더 안의 config.json의 정보가 사용되므로 작성해준다.
devlepopment 말고도 production과 test도 있었는데 .env.NODE_ENV의 값에 따라 적용되는 설정이 다르다.
이후 서버를 실행해보면 정상적으로 작동하는 것을 확인할 수 있었다.
서버 실행 시 주로 발생하는 오류는 다음과 같다.
- Error: connect ECONNREFUSED 127.0.0.1:3306: MySQL 데이터베이스를 실행하지 않았을 때
- Error: Access denied for user 'root'@'localhost' (using password: YES): 비밀번호가 틀렸을 때
- Error: Unknown database: 존재하지 않는 데이터베이스를 적었을 때
모델 정의하기
시퀄라이저의 모델은 MySQL에서 테이블과 대응된다. 시퀄라이즈는 이 둘을 연결시켜주는 역할이다. 전에 만들었던 users와 comments 테이블을 User와 Comment 모델을 만들어 연결해보자.
//models/user.js
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 모델에 접근하여 사용할 수 있는듯 하다.
User 모델은 Sequelize.Model을 확장한 클래스로 선언한다.
js에서 클래스를 배우지는 않았는데 객지프에서 JAVA로 배웠던 것과 큰 차이는 없는 것 같다. [참고]
모델은 크게 static initiate 메서드와 static associate 메서드로 나뉜다. 전자에는 테이블에 대한 설정을 하고, 후자에는 다른 모델과의 관계를 적는다.
모델.init 메서드의 첫 번째 인수가 테이블 컬럼에 대한 설정이고, 두 번째 인수가 테이블 자체에 대한 설정이다.
User.init({컬럼 설정}, {테이블 설정})꼴인 식. 이 때 시퀄라이즈는 알아서 id를 기본 키로 연결하므로 id 컬럼을 적을 필요는 없다. 또한 MySQL 테이블과 컬럼 내용이 일치해야한다.
시퀄라이즈의 자료형은 MySQL과 조금 다른데 그 내용은 아래 표와 같다.
모델.init 메서드의 두 번째 인수는 테이블 옵션이다. 각 옵션은 다음과 같다.
- sequelize: static initiate 메서드의 매개변수와 연결되는 옵션으로 db.sequelize 객체를 넣어야 한다다. 나중에 model/index.js에서 연결한다.
- timestamps: 이 속성이 true이면 시퀄라이즈는 createdAt과 updatedAt 컬럼을 추가한다. 각각 로우가 생성될 때와 수정될 때의 시간이 자동으로 입력된다. users 테이블에는 직접 created_at 컬럼을 만들었으므로 timestamps 속성이 필요하지 않기 때문에 false로 설정한다.
- underscored: 시퀄라이즈는 기본적으로 테이블명과 컬럼명을 캐멀 케이스로 만듭니다. 이를 스네이크 케이스로 바꾸는 옵션이다.
- modelName: 모델 이름을 설정할 수 있다. 노드 프로젝트에서 사용한다.
- tableName: 실제 데이터베이스의 테이블 이름이 된다. 기본적으로는 모델 이름을 소문자 및 복수형으로 만든다. 모델 이름이 User라면 테이블 이름은 users가 되는 식.
- paranoid: true로 설정하면 deletedAt이라는 컬럼이 생긴다. 로우를 삭제할 때 완전히 지워지지 않고 deletedAt에 지운 시각이 기록된다. 로우를 조회할 때 deletedAt의 값이 null인 로우(삭제되지 않음)를 조회한다. 로우를 복원해야할 때 설정하면 된다.
- charset과 collate: 각각 utf8과 utf8_general_ci로 설정해야 한글이 입력된다. 이모티콘까지 입력할 수 있게 하고 싶다면 utf8mb4와 utf8mb4_general_ci를 입력한다.
//models/comment.js
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;
comment 모델도 생성하였는데 외래 키를 설정한 commenter 컬럼이 없다. 이 부분을 모델에서 정의할 수도 있지만 시퀄라이즈 자체에서 따로 관계를 정의할 수도 있는데 후에 알아본다고 한다.
db 객체에 User와 Comment 모델을 담아주었다. 앞으로 db를 require해서 각 모델에 접근할 수 있다.
모델.initiate와 모델.associate는 모델의 static initiate와 static associate를 호출하는 것으로 모델.init이 실행되어야 테이블이 모델로 연결된다. 또한 다른 테이블과의 관계를 연결하는 메서드도 미리 실행해둔다.
관계 정의하기
테이블 간의 관계
- 일대다(1:N): 사용자 한 명은 댓글을 여러 개 작성할 수 있지만 댓글 하나에 사용자(작성자)가 여러 명일 수는 없다. 이러한 관계를 일대다 관계라고 한다.
- 일대일(1:1): 사용자 한 명은 자신의 정보를 담고 있는 테이블과만 관계가 있다. 정보 테이블도 한 사람만을 가리킨다. 이러한 관계를 일대일 관계라고 합니다.
- 다대다(N:M): 한 게시글에는 해시태그가 여러 개 달릴 수 있고, 한 해시태그도 여러 게시글에 달릴 수 있다. 이러한 관계를 다대다 관계라고 합니다.
MySQL에서는 JOIN이라는 기능으로 여러 테이블 간의 관계를 파악해 결과를 도출한다. 시퀄라이즈는 JOIN 기능도 알아서 구현하지만 테이블 간에 어떠한 관계가 있는지 시퀄라이즈에 알려야 한다.
- 1:N
각 메서드로 서로의 테이블에 접근할 수 있다.
두 번째는 웹스톰에서 자동으로 작성해주었다.
const Comment = require(./comment')로 불러와 db를 사용하지 않고 사용하면 안된다. comment.js에서 user.js를 require하는데 user.js에서도 comment.js를 require하면 문제가 발생하기 때문이다. 이러한 방식을 순환 참조라고 하는데 지양해야하는 방식이다.
hasMany와 belongsTo를 각각 어디에 사용해야하나면 다른 모델의 정보가 들어가는 테이블에 belongsTo를 작성한다.
시퀄라이즈는 위와 같은 모델 간 관계를 파악하여 컬럼을 추가한다.
연결을 끝내고 서버를 실행했는데
음.. 원래 생성된 파일에서 sequelize를 지정하는 부분을 빼고 지워줬더니 해결됐다. 사실 뭔지는 잘 모르겠는데 일단 돌아가니 계속 해보자.
- 1:1
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 컬럼이 추가되기 때문이다.
- N:M
db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' });
db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' });
양쪽 모델에 모두 belongsToMany 메서드를 사용하며, N:M 관계 특성상 새로운 모델이 생성된다. through 속성에 그 이름을 적으면 된다.
N:M에서는 데이터를 조회할 때 여러 단계를 거쳐야 한다. #노드 해시태그를 사용한 게시물을 조회하는 경우라면, 먼저 #노드 해시태그를 Hashtag 모델에서 조회하고, 가져온 태그의 아이디(1)를 바탕으로 PostHashtag 모델에서 hashtagId가 1인 postId들을 찾아 Post 모델에서 정보를 가져온다.
쿼리 알아보기
const { User } = require('../models');
User.create({
name: 'zero',
age: 24,
married: false,
comment: '자기소개1',
});
로우를 생성하는 쿼리이다.
이 때 주의할 점은 MySQL의 자료형이 아니라 시퀄라이즈 모델에 정의한 자료형대로 넣어야 한다는 것이다.
원래라면 married를 0으로 설정했어야 하지만 여기서는 false로 설정하는 이유이다.
User.findAll({});
User.findOne({});
User.findAll({
attributes: ['name', 'married'],
});
const { Op } = require('sequelize');
const { User } = require('../models');
User.findAll({
attributes: ['name', 'age'],
where: {
married: true,
age: { [Op.gt]: 30 },
},
});
User.findAll({
attributes: ['id', 'name'],
order: ['age', 'DESC'],
limit: 1,
offset: 1,
});
로우를 조회하는 쿼리들이다.
- findAll: 테이블의 모든 데이터를 조회한다. 여러 개의 데이터를 가져올 때도 사용한다.
- attributes 옵션으로 원하는 컬럼만 가져올 수 있다.
- where 옵션에 조건들을 나열할 수 있다. MySQL은 undefined를 지원하지 않으므로 null을 사용해야 한다.
- order 옵션으로 정렬 방식을 지정할 수 있다. 컬럼 두 개 이상으로 정렬할 수도 있기 때문에 대괄호가 두 번 씌워져있다.
- limit: 조회할 로우 개수 설정
- offset: OFFSET. 데이터의 특정 위치부터 조회
- findOne: 테이블의 데이터 하나만을 조회한다.
시퀄라이즈는 자바스크립트 객체를 사용해서 쿼리를 생성해야 하므로 Op.gt 같은 특수한 연산자들이 사용된다.
Op.gt(초과), Op.gte(이상), Op.lt(미만), Op.lte(이하), Op.ne(같지 않음), Op.or(또는), Op.in(배열 요소 중 하나), Op.notIn(배열 요소와 모두 다름) 등이 있다.
User.update({
comment: '바꿀 내용',
}, {
where: { id: 2 },
});
로우를 수정하는 쿼리이다.
첫 번째 인수에 수정할 내용을, 두 번째 인수에 수정할 로우를 작성한다.
User.destory({
where: { id: 2 },
});
로우를 삭제하는 쿼리이다.
where옵션에 조건들을 적는다.
- 관계 쿼리
const user = await User.findOne({});
console.log(user.nick);
findOne이나 findAll 메서드를 호출하면 프로미스의 결과로 모델을 반환한다.
User 모델에 바로 접근할 수도 있지만, 관계 쿼리도 지원한다.(MySQL의 JOIN 기능)
hasMany-belongsTo 관계가 맺어져있는 User와 Comment 모델이 있으니, 특정 사용자를 가져오면서 그 사용자가 남긴 댓글까지 모두 가져올 수도 있다.
const user = await User.findOne({
include: [{
model: Comment,
}]
});
console.log(user.Comments);
include에 어떤 모델과 관계가 있는지 작성하면 된다.
const user = await User.findOne({});
const comments = await user.getComments();
console.log(comments);
이러한 방법도 가능하다.
관계가 설정되어있다면 getComments(조회) 외에도 setComments(수정), addComment(하나 생성), addComments(여러 개 생성), removeComments(삭제) 메서드를 지원한다.
관계를 설정할 때 모델의 이름을 바꿀 수도 있다.
db.User.hasMany(db.Comment, { foreignKey: 'commenter', sourceKey: 'id', as: 'Answers' });
- SQL 쿼리하기
const [result, metadata] = await sequelize.query('SELECT * from comments');
시퀄라이즈의 쿼리를 사용하지 않을 때 직접 SQL문을 통해 쿼리할 수도 있다.
쿼리 수행하기
7-7. 함께 보면 좋은 자료
- 데이터베이스 설명: https://ko.wikipedia.org/wiki/데이터베이스
- MySQL 매뉴얼: https://dev.mysql.com/doc/refman/8.0/en
- 워크벤치 매뉴얼: https://dev.mysql.com/doc/workbench/en
- 시퀄라이즈 문서: http://docs.sequelizejs.com
'WINK-(Web & App) > Express.js (Node.js) 스터디' 카테고리의 다른 글
[2024-2 Node.js 스터디] 김민재 #5주차 (0) | 2024.11.18 |
---|---|
[2024-2 Node.js 스터디] 류상우 #5주차 (2) | 2024.11.18 |
[2024-2 Node.js 스터디] 김민재 #4주차 (0) | 2024.11.11 |
[2024-2 Node.js 스터디] 류상우 #3주차 (0) | 2024.11.04 |
[2024-2 Node.js 스터디] 김민재 #3주차 - 익스프레스 웹 서버 만들기 (1) | 2024.11.04 |