- Published on
Sequelize와 ORM | node.js
Intro
기존 포스팅(MVC패턴 | node.js) 을 보면, mysql에서 테이블을 sql구문으로 만들어준 뒤, controller에서 sql을 직접 작성하여 DB상태를 변경하였다. 이 부분에서 객체 모델과 관계형 모델 간에 불일치가 존재하며 Sequelize를 이용한 ORM으로 이것을 해결하는 것을 배웠다.
이 포스팅은 ORM이란 무엇인지와 sequelize에 대해서 설명하고 이 개념을 이용해 만든 CRUD 기능이 있는 회원가입, 로그인, 프로필페이지가 있는 간단한 예제의 구현코드를 설명한다.
ORM?
Object Relational Mapping, 객체-관계 매핑
한마디로 관계형 데이터베이스와 객체 지향 프로그래밍 언어 간의 호환되지 않는 데이터를 자동으로 매핑(연결)해주는 프로그래밍 기법이다. 좀 더 적나라하게(?) 말하자면 node.js의 controller에서 sql구문을 이용해 RDBMS의 데이터를 연결해주던 것을 Sequelize 라이브러리를 통해 쉽게 연결할수 있다는 것이다. 데이터 매핑구조를 객체지향형으로 통일시키기 때문에 한층 더 oop적 구조에 가까워진다.
Sequelize를 이용한 ORM 프로그래밍
프로젝트 시작순서는 기존포스팅과 동일하나, Sequelize 라이브러리와 sequelize-cli를 설치하는 것이 추가된다.
npm i sequelize sequelize-cli
그리고 npx sequelize init로 시퀄라이즈를 환경변수에 추가해준다. (npx는 설치가 아닌 일회성으로 사용하는 명령어이다.)
MVC패턴+시퀄라이즈의 개념을 활용한 로그인과 회원가입 기능이 있는 간단한 웹페이지를 만들었다.
디렉토리 구조는 아래와 같다.
C:.
│ .gitignore
│ app.js
│ package-lock.json
│ package.json
│
├─config
│ config.json
│
├─controller //시퀄라이즈 사용
│ Cuser.js
│
├─models //DB정의
│ index.js
│ User.js
│
├─routes
│ user.js
│
├─static
│ └─css
│ index.css
│ profile.css
│ signin.css
│ signup.css
└─views
404.ejs //404에러 처리
index.ejs //메인index 페이지
profile.ejs //로그인 후 넘어가는 프로필페이지
signin.ejs //로그인 기능
signup.ejs //회원가입 기능
자잘한 코드는 생략하고 DB를 연결하는 config.json, DB를 정의하는 models와 시퀄라이즈를 사용한 controller부분을 첨부하고자 한다.
모델을 정의할 때 데이터베이스의 테이블이 실제로 존재하지 않거나 존재하지만 칼럼갯수의 차이를 방지하기 위해서 model.sync(options)로 비동기 함수(promise)를 호출하여 모델 동기화를 시작한다. 이 호출로 Sequelize는 데이터베이스에 대한 SQL 쿼리를 자동으로 수행한다. User.sync() 일때는 default가 force: false 이다. 따라서 테이블이 없으면 생성하고 있으면 아무작업도 수행하지 않는다. User.sync({ force: true }) 이면 무조건 테이블을 새로 생성한다. 그리고 이건 시퀄라이즈 공식문서에서 본것인데 User.sync({ alter: true }) 데이터베이스에 있는 테이블의 현재 상태(어떤 열이 있는지, 데이터 유형은 무엇인지 등)를 확인한 다음 모델과 일치하도록 테이블에서 필요한 변경을 수행한다고 한다.
모델 동기화 구문이 작성된 app.js는 아래와 같다.
const express = require("express");
const app = express();
const PORT = 8000;
const db = require("./models"); //모델을 불러옴
...생략...
//server start
db.sequelize.sync({ force: false }).then(() => { //모델 동기화
app.listen(PORT, () => {
console.log(`http://localhost:${PORT}`);
});
});
config.json에 db(mysql)를 연결한다.
{
"development": {
"username": "user",
"password": "1234",
"database": "kdt8",
"host": "127.0.0.1",
"dialect": "mysql"
},
다음은 models의 index.js 부분이다.
모델은 Sequelize에게 데이터베이스의 테이블 이름, 테이블에 포함된 열(및 해당 데이터 유형)과 같이 모델이 나타내는 엔터티에 대해 여러 가지 정보를 알려준다. 여기서 Sequelize는 시퀄라이즈 패키지이자 생성자이다. config/config.json에서 데이터베이스 설정을 require로 불러온 후 new Sequelize를 통해 MySQL 연결 객체를 생성한다.
"use strict";
const Sequelize = require("sequelize");
const config = require(__dirname + "/../config/config.json")["development"];
const db = {};
const sequelize = new Sequelize(
config.database,
config.username,
config.password,
config
);
//model
db.User = require("./User")(sequelize, Sequelize);
db.sequelize = sequelize;
//Sequelize 인스턴스(sequelize)를 db 객체의 sequelize 속성에 할당
-> db.sequelize를 사용하여 애플리케이션 내에서 Sequelize 인스턴스에 접근
db.Sequelize = Sequelize;
//Sequelize 모듈 자체를 db 객체의 Sequelize 속성에 할당
module.exports = db;
모델은 Sequelize에서 두 가지 방법으로 정의할 수 있다.
sequelize.define(modelName, attributes, options)init(attributes, options)
실습에서는 define을 사용하여 정의하였다.
정의한 후에는 sequelize.models.'모델명' 으로 액세스 할 수 있다. User.sync({ force: true })으로 테이블을 자동생성 했다면, 기본적으로 테이블 이름을 지정하지 않으면 Sequelize는 자동으로 모델 이름을 복수화하여 테이블 이름으로 사용한다. 이것을 방지하고자 한다면 freezeTableName: true로 설정한다. 그러면 테이블 이름을 단수로 생성할 수 있다. 그리고 기본적으로 Sequelize는 데이터 유형을 사용하여 모든 모델에 createdAt필드를 자동으로 추가하여 자동으로 관리한다. Sequelize를 사용하여 무언가를 생성하거나 업데이트할 때마다 해당 필드가 설정된다. 이는 아래 테이블 이미지(User테이블은 아니지만)에서 createdAt, updatedAt 가 자동으로 생성된것을 확인할 수 있다.
모델에서 정의하는 모든열에는 데이터 유형이 있어야 한다. 내장 데이터 유형에 액세스하려면 const { DataTypes } = require("sequelize");로 가져와야한다. sequelize에서는 다양한 데이터 유형들이 있다. (자세한 데이터 유형은 시퀄라이즈 공식문서 참고)
데이터 유형(type)을 지정했다면, Column Options를 정의해야한다. allowNull 이나 defaultValue, unique, primaryKey 등으로 지정할 수 있다. 이를 이용한 models의 User.js의 상세코드이다.
const { DataTypes } = require("sequelize");
//user 대한 테이블 정의
const User = function (sequelize) {
//위 변수의 Sequelize = models/index.js에 있는 sequelize
//DataTypes는 model/index.js에 있는 Sequalize
const model = sequelize.define(
"user",
{
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
},
userid: {
type: DataTypes.STRING(20),
allowNull: false,
},
name: {
type: DataTypes.STRING(10),
allowNull: false,
},
pw: {
type: DataTypes.STRING(20),
allowNull: false,
},
},
{
//db테이블에 대한 옵션정의
tableName: "user", //실제 db테이블 이름
freezeTableName: true, //복수 vs단수(true)
timestamps: false,
}
);
return model;
};
module.exports = User; //index에서 사용할수 있게 모듈로 export
모델은 ES6 클래스로 클래스의 인스턴스는 해당 모델의 하나의 개체를 나타내며 이런 모델 인스턴스는 DAO(데이터베이스의 data에 접근하기 위한 객체)라고 한다. 클래스지만 new연산자로 직접 인스턴스 생성은 불가능하며 sequelize가 정한 문법으로 인스턴스를 데이터베이스에 실제로 저장할 수 있다. 첨부한 코드에서는 다음과 같이 사용되었다.
models.User.create({userid: userid,..});
= sql의 insert()와 같다.models.User.findOne({where: { userid: userid },});
= sql의 select와 같으며 select는 기본적으로 배열을 반환하지만, findOne에서는 객체 하나만 반환한다.models.User.update({ name, pw }, { where: { id } })
= sql의 update()와 같다.models.User.destroy({ where: { id } })
= sql의 delete 와 같다.
공통적으로 where옵션은 쿼리를 필터링하는데 사용된다.
아래 controller의 Cuser.js에서 자세하게 확인할 수 있다.
const models = require("../models");
const { Op } = require("sequelize");
//회원가입
exports.getSignupList = (req, res) => {
const userid = req.body.userid;
const name = req.body.name;
const password = req.body.password;
models.User.create({
userid: userid,
name: name,
pw: password,
}).then(() => {
res.send({ result: true });
});
};
//로그인
exports.getSignin = (req, res) => {
const userid = req.body.userid;
const password = req.body.password;
models.User.findOne({
//where 안에는 객체이다.
where: { userid: userid, pw: password },
}).then(() => {
res.send({ success: true });
});
};
//프로필
exports.getProfile = (req, res) => {
const userid = req.body.profile;
console.log("profile실행", userid);
models.User.findOne({
where: { userid: userid },
}).then((result) => {
console.log(result);
res.render("profile", { profileInfo: result });
//배열이 아닌 객체형태로 보내기때문에 result[0]이 아니라 result로 보내도 가능
//실제로 가는 값은 dataValues만 전송, dataValues는 생략이 가능함
});
};
//프로필수정
exports.UpdateUserInfo = (req, res) => {
//구조분해 할당
const { name, pw, id } = req.body;
models.User.update({ name, pw }, { where: { id } }).then((result) => {
console.log(result);
res.send({ success: true, result });
});
};
//회원정보 삭제
exports.DeleteUserInfo = (req, res) => {
const id = req.body.id;
models.User.destroy({ where: { id } }).then(() => {
res.send({ success: true });
});
};
findOne이 있다면 findAll도 있다. 둘다 동일하게 select와 비슷한 기능을 하나 findAll은 배열로 반환된다는 차이점이 있다. findAll에 원하는 필드만 선택하여 조회할 수 있게끔 다양한 옵션을 걸수 있다. const { Op } = require("sequelize"); 을 통해 연산자를 사용할 수 있다. 연산자 종류는 아래와 같다.
- Op.gt(초과)
- Op.gte(이상)
- Op.lt(미만)
- Op.ne(같지 않은)
- Op.or(또는)
- Op.in(배열 요소 중 하나)
- Op.notIn(배열 요소와 모두 다름)
//findeall 연습, 아무기능도 없음
exports.findall = (req, res) => {
models.User.findAll({
//attributes : 원하는 컬럼 조회
attributes: ["name", "userid"],
where: { id: { [Op.gte]: 4 } },
order: [["id", "DESC"]], //order는 배열형태로 온다.
limit: 1,
offset: 1,
}).then((result) => {
console.log("result", result);
res.send(result);
});
};
또한 시퀄라이즈로 모델의 관계형성을 할수있으며 네가지 유형이 있다. 이를 통해 복잡한 테이블구조를 단순화하여 관리할 수 있다.
HasOne= 1:1관계BelongsTo= 1:1관계HasMany= 1:n 관계
=> 이 세가지는 모델에 외래키를 따로 설정하지 않다면 자동으로 생성해준다.
BelongsToMany= n:m 관계
=> 무척 복잡하기 때문에 잘 사용하지 않는다. 시퀄라이즈 자체를 사용하지 않는다기보다는 RDBMS상 다대다 관계를 잘 사용하지 않는다고 한다.
이 실습예제에서는 없지만 다른 실습예제(주제는 수강신청 웹페이지였다.)에서 사용한 관계 연결을 정의하는 것을 첨부한다. 뷰페이지를 만들지 않았기 때문에 참고할 수 있는 영상은 없다.
디렉토리 구조도이다.
...생략..
├
├─controller
│ Cstudent.js
│
├─models
│ index.js
│ Classes.js
│ Students.js
│ StudentsProfile.js
│
...생략..
우선, models의 index.js 이다.
...생략...
//모델
db.Student = require("./Students")(sequelize);
db.Classes = require("./Classes")(sequelize);
db.StudentProfile = require("./StudentsProfile")(sequelize);
//관계형성
//1:1 학생과 프로필
db.Student.hasOne(db.StudentProfile);
db.StudentProfile.belongsTo(db.Student);
//1:다 학생과 강의
db.Student.hasMany(db.Classes);
db.Classes.belongsTo(db.Student);
...생략...
Student 와 StudentProfile이 1:1 관계로 형성된다.
Student 와 Classes는 1:n 관계로 형성된다. 외래키를 지정하지 않았으므로 belongsTo를 통해 mysql 테이블에 자동으로 외래키가 생성된다. 외래키를 정의하는 방법은 3가지가 있다.
- 외래키 이름을 직접 정의한다.
db.StudentProfile.belongsTo( db.Student,{ foreignKey:'StudentId' }); - 별칭을 정의한다.
db.StudentProfile.belongsTo( db.Student,{ as :'Student' }); - 위 두가지 다 수행한다.
db.StudentProfile.belongsTo( db.Student,{ as :'Student', foreignKey:'StudentId' });
아래 mysql의 StudentProfile 테이블에 외래키(StudentId)가 생성됨을 확인할 수 있다.(필드이름은 카멜케이스로 생성된다.)
foreign keys탭으로 들어가면 더 자세하게 확인할 수 있다.
controller의 Cstudent.js 이다. { include: StudentProfile }를 통해 중첩연결(sql의 조인문과 비슷)으로 원하는 컬럼을 조회하여 데이터를 삽입하거나 조회할 수 있다. include는 다중으로 사용이 가능하다.
const { Student, StudentProfile, Classes } = require("../models");
exports.index = (res, req) => {
res.render("index");
};
//학생생성
exports.post_student = async (req, res) => {
try {
const { userid, password, email, name, major, enroll } = req.body;
console.log(userid);
const user = await Student.create(
{
userid,
password,
email,
StudentProfile: { //조인
name,
major,
enroll,
},
},
//include: 쿼리를 시행할 때 관련된 모델의 데이터도 함께 조회할 수 있도록 하는 옵션
{ include: StudentProfile }
);
console.log(user);
res.send(user);
} catch (error) {
console.log(error);
}
};
...생략...
exports.get_student = async (req, res) => {
try {
const student = await Student.findAll({
attributes: ["userid", "email"],
include: [{ model: StudentProfile, attributes: ["major", "enroll"] }],
});
res.send(student);
} catch (error) {
console.log(error);
}
};
...생략....
As a result...
사실 처음 보았을때는 환경변수 설정이나 sequelize 문법등이 생소하여 진입장벽이 높게 느껴졌으나 막상 사용해보니 model에서 콜백함수로 결과값을 반환해주기 위해 RDBMS 구문으로 한번 더 작성하는 대신 model에서는 DB만 정의하고 controller에서 한번에 처리하니 훨씬 더 효율적인 방법이라고 느꼈고 한층 더 객체지향프로그래밍에 가까워졌다는 생각이 들었다. 틈틈히 시퀄라이즈 공식문서를 읽으며 배경지식을 쌓도록 노력해야겠다.
상세코드
👉 https://github.com/fun1ty/KDT_8_web/tree/main/sequelize_DB 👉 https://github.com/fun1ty/KDT_8_web/tree/main/mvc_sequelize2
출처
https://hanamon.kr/orm%EC%9D%B4%EB%9E%80-nodejs-lib-sequelize-%EC%86%8C%EA%B0%9C/
http://www.incodom.kr/ORM
https://gmlwjd9405.github.io/2019/02/01/orm.html
https://munak.tistory.com/entry/DBORMObject-Relational-Mapping%EC%9D%B4%EB%9E%80
https://sequelize.org/docs/v6/core-concepts/model-basics/
https://velog.io/@kimkevin90/%EC%8B%9C%ED%80%84%EB%9D%BC%EC%9D%B4%EC%A6%88Sequelize-%EA%B8%B0%EB%B3%B8-%EC%82%AC%EC%9A%A9%EB%B2%95
+챗GPT
