ORM(Sequelize
)을 사용해서 M:N 관계를 구현하는 방법을 정리합니다
테이블 모델을 생성하고 다음과 같은 메서드를 통해 테이블간 관계를 형성합니다
hasMany
: One to Many 관계를 맺기 위해 사용합니다 (부모 모델)belongsTo
: One to Many 관계를 맺기 위해 사용합니다 (자식 모델)belongsToMany
: Many To Many 관계를 맺기 위해 사용합니다.
두 테이블간의 관계를 나타내는 조인 테이블이 자동으로 생성됩니다
[User]
module.exports = (sequelize, Sequelize) => {
class User extends Sequelize.Model {
static initialize() {
return this.init(
{
userid: {
type: Sequelize.STRING(60),
primaryKey: true,
},
userpw: {
type: Sequelize.STRING(64),
allowNull: false,
},
username: {
type: Sequelize.STRING(30),
allowNull: false,
},
provider: {
type: Sequelize.ENUM("local", "kakao"),
allowNull: false,
defaultValue: "local",
},
snsId: {
type: Sequelize.STRING(30),
allowNull: true,
},
},
{
sequelize,
}
);
}
// PK > FK
static associate(models) {
this.hasMany(models.Board, {
foreignKey: "userid",
});
this.hasMany(models.Comment, {
foreignKey: "userid",
});
// M:N 관계 설정 메서드 ~ 조인 테이블이 자동으로 생성됩니다!
this.belongsToMany(models.Board, {
through: "Liked", // 조인 테이블 명칭
foreignKey: 'userid',
});
}
}
User.initialize();
};
[Board]
module.exports = (sequelize, Sequelize) => {
class Board extends Sequelize.Model {
static initialize() {
this.init(
{
subject: {
type: Sequelize.STRING(100),
allowNull: false,
},
content: {
type: Sequelize.TEXT,
allowNull: true,
},
createdAt: {
type: Sequelize.DATE,
defaultValue: sequelize.fn("now"),
},
hit: {
type: Sequelize.INTEGER,
defaultValue: 0,
},
},
{
sequelize,
}
);
}
// FK > PK
static associate(models) {
this.belongsTo(models.User, {
foreignKey: "userid",
// 코드를 읽는 순간 Board에도 userid 필드가 생성됩니다
});
this.hasMany(models.Comment, {
foreignKey: "boardid",
// 자식 테이블에 생길 필드명입니다
});
// User 테이블과 M:N 관계를 맺으며 조인 테이블을 생성합니다
this.belongsToMany(models.User, {
through: "Liked",
foreignKey: "boardid",
});
// Hash 테이블과 M:N 관계를 맺으며 조인 테이블을 생성합니다
this.belongsToMany(models.Hash, {
through: "Hashtag",
foreignKey: 'boardid',
});
}
}
Board.initialize();
};
[Hash]
module.exports = (sequelize, Sequelize) => {
class Hash extends Sequelize.Model {
static createTable() {
return this.init(
{
tagname: {
type: Sequelize.STRING(30),
allowNull: false,
primaryKey: true,
},
},
{
sequelize
}
);
}
static associate(models) {
this.belongsToMany(models.Board, {
through: "Hashtag",
foreignKey: 'tagname'
})
}
}
Hash.createTable();
};
[index]
1) 인스턴스(sequelize) 생성
2) associate 함수(정적 메서드) 실행
const fs = require("fs");
const path = require("path");
const Sequelize = require("sequelize");
const config = require("../config");
const db = config.db[config.env];
const sequelize = new Sequelize(db.database, db.username, db.password, db);
// 모델을 생성하기 위해 익명함수를 실행하는 코드입니다
fs.readdirSync(__dirname) // __dirname : 현재위치에 대한 절대경로를 불러오는 내장객체
.filter(v => v.indexOf("index"))
.forEach((file) => {
require(path.join(__dirname, file))(sequelize, Sequelize);
// path.join : 경로와 파일을 인자로 받습니다
});
// 생성한 모델 객체로부터 associate 함수를 실행하기 위한 코드입니다
const { models } = sequelize
for (const v in models) {
if (typeof models[v].associate === "function")
sequelize.models[v].associate(models);
}
// console.log(models)
module.exports = {
sequelize,
Sequelize,
};
코드 실행 결과 belongsToMany
메서드로 인해 다음과 같은 조인 테이블이 생성됩니다
mysql> desc Liked;
+---------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+-------------+------+-----+---------+-------+
| userid | varchar(30) | NO | PRI | NULL | |
| BoardId | int | NO | PRI | NULL | |
+---------+-------------+------+-----+---------+-------+
mysql> desc Hashtag;
+---------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+-------------+------+-----+---------+-------+
| boardid | int | NO | PRI | NULL | |
| tagname | varchar(30) | NO | PRI | NULL | |
+---------+-------------+------+-----+---------+-------+
create
, destroy
메서드를 사용해서 좋아요를 추가하고 삭제합니다userid
와 boardid
가 모두 같을 때는 에러를 출력해야 합니다 (중복 방지)[Repository]
async createLike(likeData) {
console.log("repo :", likeData);
try {
const create = await this.Liked.create({
BoardId: likeData.boardid,
userid: likeData.userid,
});
return create;
} catch (e) {
throw new Error(e);
}
}
async destroyLike({ boardid, userid }) {
console.log("repo :", { boardid, userid });
try {
const destroy = await this.Liked.destroy({
where: {
BoardId: boardid,
userid: userid,
},
});
return destroy;
} catch (e) {
throw new Error(e);
}
}
findOrCreate
메서드를 이용하면 중복값이 없을 때만 INSERT문을 실행할 수 있습니다 `SELECT * FROM Hash WHERE tagname='#javascript'`
`INSERT INTO Hash(tagname) VALUES ('#javascript')`
글을 등록할 때 한 번에 여러 태그를 추가할 수 있도록 배열을 인자로 받고,
findOrCreate
메서드를 실행할 때 반복문을 사용해야 합니다(map()
사용)
반복문 + 비동기 함수의 결과 처리는 promise.all
메서드를 이용합니다
Promise.all()
은 여러 개의 Promise 객체를 받아 하나의 새로운 Promise 객체를 반환하고,
새로운 Promise 객체는 전달된 모든 Promise 객체의 처리가 완료되면 결과를 배열로 반환합니다
Hash 테이블의 필드에 채워진 내용을 Hashtag 테이블에도 연동시켜야 합니다
1) console.log(instance.__proto__)
로 인스턴스가 상속받은 메서드를 확인할 수 있습니다
그 중에서 belongsToMany
로 엮인 테이블명을 찾습니다(addHashes
)
2) board.addHashes(tags.map((v)=>v[0]))
실행시
Hashtag 테이블에 boardid
와 tagname
필드의 값이 담깁니다
update를 구현할 때 해당 글의 해시태그도 같이 수정하려면 Hashtag 테이블의 row를 따로 삭제 & 생성해야 합니다
어우 복잡해라....
[Repository]
async createBoard(writeData) {
// writeData: { hashtag : [] }
try {
const createBoard = await this.Board.create(writeData);
const addHash = writeData.hashtag.map((tagname) => this.Hash.findOrCreate({ where: { tagname } }))
const tagResult = await Promise.all(addHash)
// console.log(tagResult)
// console.log(createBoard.__proto__)
await createBoard.addHashes(tagResult.map(v=>v[0]))
return createBoard;
} catch (e) {
throw new Error(e);
}
}
async updateBoard({ id, subject, content, hashtag }) {
console.log("update :", id, subject, content, hashtag);
try {
const updateBoard = await this.Board.update(
{
subject: subject,
content: content,
},
{ where: { id: id } }
);
const addHash = hashtag.map((tagname) => this.Hash.findOrCreate({ where: { tagname } }))
await this.Hashtag.destroy({ where: { boardid: id } });
const addHashTag = hashtag.map((tagname) => this.Hashtag.create({ boardid: id, tagname }));
await Promise.all(addHash, addHashTag)
return updateBoard;
} catch (e) {
throw new Error(e);
}
}
마지막으로 좋아요와 해시태그가 잘 담겼는지 findOne()
을 사용해서 확인해보겠습니다
async findOne(id) {
try {
const view = await this.Board.findOne({
where: { id: id },
});
// raw: true 옵션을 사용하면 SQL 쿼리문 실행결과만 반환합니다
const comments = await view.getComments({ raw: true });
const liked = await this.Liked.findAll({
attributes: ['userid'],
where: { boardid: id },
});
const hashtag = await this.Hashtag.findAll({
attributes: ['tagname'],
where: { boardid: id },
});
return { view: view, comments: comments, liked: liked, hashtag: hashtag };
} catch (e) {
throw new Error(e);
}
}
// get 요청으로 확인하기
{
"view": {
"id": 29,
"subject": "수정 테스트",
"content": "수정",
"hit": 0,
"createdAt": "2023-01-17T06:53:38.000Z",
"updatedAt": "2023-01-17T06:53:58.000Z",
"userid": "web7722"
},
"comments": [
{
"id": 7,
"content": "댓글 입력 테스트",
"createdAt": "2023-01-17T08:30:27.000Z",
"updatedAt": "2023-01-17T08:30:27.000Z",
"boardid": 29,
"userid": "web7722"
},
{
"id": 8,
"content": "댓글 입력 테스트2",
"createdAt": "2023-01-17T08:30:32.000Z",
"updatedAt": "2023-01-17T08:30:32.000Z",
"boardid": 29,
"userid": "web7722"
}
],
"liked": [
{
"userid": "web7722"
},
{
"userid": "web8833"
}
],
"hashtag": [
{
"tagname": "#modify"
},
{
"tagname": "#test"
}
]
}