시퀄라이즈 공식 문서
이 공식 문서를 읽어보니 constraints와 references가 뭔지 정확히 이해하게 된 것 같다.
원래 constraints라는 설정값에 대해서 알아보게된 계기는
Error : sequelize cannot add or update a child row a foreign key constraint fails
이 에러를 해결하기 위함이었다.
저 에러에 constraint라는 키워드가 들어가 있었는데 이는 제한사항이라는 뜻이었고, 검색하다보니 constraints 라는 설정값을 알게되어서 이것을 false로 한다면 두 테이블 관계의 관계로 인한 제한사항을 풀어버려서 저 에러가 안나게 할 수 있지 않을까라는 생각이 있어서 시도해보았었다.
그러나 저 에러는 constraints 속성으로 해결되지 않고 결과적으로 다른 방식으로 해결되었다.
그런데 이번에 다시 시퀄라이즈에 대해서 정리해보다가 저 것을 다시 찾아봤는데 공식 문서를 통해 저것의 정체가 무엇인지 그리고 추가로 reference가 무엇인지 알게되었다. reference 자체는 무엇인지 이해는 갔으나 테이블 간의 관계성 작성으로도 충분히 외래키를 지정할 수 있는데 왜 이게 또 따로 있는지 그것은 이해가 가지 않았었다.
Adding constraints between tables means that tables must be created in the database in a certain order, when using sequelize.sync. If Task has a reference to User, the User table must be created before the Task table can be created. This can sometimes lead to circular references, where Sequelize cannot find an order in which to sync.
테이블 간에 참조 관계가 있을 때 이 두 테이블은 특정 순서를 갖고 생성되어야 함을 설명하고 있다. 하지만 이 것이 어떤 경우에는 순서를 찾을 수 없는 순환 참조로 이어진다고 한다.
Imagine a scenario of documents and versions. A document can have multiple versions, and for convenience, a document has a reference to its current version.
const { Sequelize, Model, DataTypes } = require("sequelize");
class Document extends Model {}
Document.init({
author: DataTypes.STRING
}, { sequelize, modelName: 'document' });
class Version extends Model {}
Version.init({
timestamp: DataTypes.DATE
}, { sequelize, modelName: 'version' });
Document.hasMany(Version); // This adds documentId attribute to version
Document.belongsTo(Version, {
as: 'Current',
foreignKey: 'currentVersionId'
}); // This adds currentVersionId attribute to document
However, unfortunately the code above will result in the following error:
Cyclic dependency found. documents is dependent of itself. Dependency chain: documents -> versions => documents
순환 의존성 에러를 가지는 예시를 설명하고 있다.
한 문서는 여러개의 버전을 가지고 있을 수 있으므로 1대다 관계로서 버전은 문서를 참조하고 있고,
동시에 한 문서는 하나의 현재 버전을 갖고 있으므로 문서도 1대1로써 버전을 참조한다.
이런 경우 코드에도 나와 있 듯이, 두 개의 관계를 작성하게 되는데 이럴 때 순환 의존성이 발생한다고 한다.
In order to alleviate that, we can pass constraints: false to one of the associations:
Document.hasMany(Version);
Document.belongsTo(Version, {
as: 'Current',
foreignKey: 'currentVersionId',
constraints: false
});
Which will allow us to sync the tables correctly:
CREATE TABLE IF NOT EXISTS "documents" (
"id" SERIAL,
"author" VARCHAR(255),
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"currentVersionId" INTEGER,
PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "versions" (
"id" SERIAL,
"timestamp" TIMESTAMP WITH TIME ZONE,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"documentId" INTEGER REFERENCES "documents" ("id") ON DELETE
SET
NULL ON UPDATE CASCADE,
PRIMARY KEY ("id")
);
순환 의존성을 해결하기 위해 두 관계 중 하나의 제한 사항을 무시하게 하는 것이 constraints인 것이었다. 위 경우에는 Document.belongsTo의 제한 사항을 무시하였으므로, documents 테이블이 먼저 생성되도록 순서가 정해진다.
Enforcing a foreign key reference without constraints
Sometimes you may want to reference another table, without adding any constraints, or associations. In that case you can manually add the reference attributes to your schema definition, and mark the relations between them.
그런데 이러한 constraints를 쓰지 않는 방법도 소개하고 있다. 즉, 두 테이블 간의 관계성에 의한 제한 사항 등은 추가시키지 않은 채로 그저 참조만을 하게 하고 싶을 때이다. 이럴 때 스키마 정의에서 reference를 추가하면 된다고 한다.
class Trainer extends Model {}
Trainer.init({
firstName: Sequelize.STRING,
lastName: Sequelize.STRING
}, { sequelize, modelName: 'trainer' });
// Series will have a trainerId = Trainer.id foreign reference key
// after we call Trainer.hasMany(series)
class Series extends Model {}
Series.init({
title: Sequelize.STRING,
subTitle: Sequelize.STRING,
description: Sequelize.TEXT,
// Set FK relationship (hasMany) with `Trainer`
trainerId: {
type: DataTypes.INTEGER,
references: {
model: Trainer,
key: 'id'
}
}
}, { sequelize, modelName: 'series' });
// Video will have seriesId = Series.id foreign reference key
// after we call Series.hasOne(Video)
class Video extends Model {}
Video.init({
title: Sequelize.STRING,
sequence: Sequelize.INTEGER,
description: Sequelize.TEXT,
// set relationship (hasOne) with `Series`
seriesId: {
type: DataTypes.INTEGER,
references: {
model: Series, // Can be both a string representing the table name or a Sequelize model
key: 'id'
}
}
}, { sequelize, modelName: 'video' });
Series.hasOne(Video);
Trainer.hasMany(Series);
이 예시는 자세히 읽어보진 않았지만 references를 활용하는 모습을 보여준다. 정확히 기억은 안나지만 아마도 내가 references만으로 외래키를 생성하려고 했었을 때 cascade같은 것이 적용 안됐던 이유가 이 references는 관계에 의한 제한 사항을 무시하고 참조만을 하는 설정값이었기 때문일지도 모르겠다.
다대다 관계로 테이블 생성했을 때
code: 'ER_FK_COLUMN_NOT_NULL',
errno: 1830,
sqlState: 'HY000',
sqlMessage: "Column 'PostId' cannot be NOT NULL: needed in a foreign key constraint 'like_ibfk_1' SET NULL",
sql: 'CREATE TABLE IF NOT EXISTS `like` (`like` TINYINT(1) NOT NULL, `PostId` INTEGER , `UserId` INTEGER , PRIMARY KEY (`PostId`, `UserId`), FOREIGN KEY (`PostId`) REFERENCES `post` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, FOREIGN KEY (`UserId`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_general_ci;', parameters: undefined },
이와 같은 에러가 났을 때도 constraints로 하나 끊어줌으로써 해결 가능
https://jaehoney.tistory.com/90?category=869692 일괄등록, 일괄수정