[BE] Sequelize 외래 키 설정 - innovationacademy-kr/slabs-munetic GitHub Wiki

sequelize에서 테이블 간 관계를 설정할 때 다음 네가지 함수를 사용합니다.

이 함수들은 모델(테이블)의 메소드로 호출하며 인수로 다른 모델(테이블)을 집어 넣는 식으로 사용합니다.

테이블 생성 시에 외래 키 관계를 설정해 주는 것 외에도 모델 객체에 관련 메소드 (getter/setter)를 설정하는 역할도 수행합니다.

sequelize에선 호출할 때 사용하는 모델을 source라 부르며 인자로 들어가는 모델을 target라고 부릅니다.

source.func(target);
  • hasOne : 외래키를 target에 삽입하며 source와 1:1 관계를 설정합니다.
    • A.hasOne(B) 를 직역하면 “A는 B를 하나 가진다” 입니다.
    • 만약 A.hasOne(B) 와 같이 함수를 호출하면 외래 키는 B에 삽입됩니다.
  • BelongsTo : 외래키를 source에 삽입하며 source와 1:1 관계를 설정합니다.
    • A.BelongsTo(B) 를 직역하면 “A는 B에 속한다” 입니다.
    • 만약 A.BelongsTo(B) 와 같이 함수를 호출하면 외래 키는 A에 삽입됩니다.
  • hasMany : 외래키를 target에 삽입하며 source와 1:n 관계를 설정합니다.
    • A.hasMany(B) 를 직역하면 “A는 B를 여러개 가진다” 입니다.
    • 만약 A.hasMany(B) 와 같이 함수를 호출하면 외래 키는 B에 삽입됩니다.
  • belongsToMany : 연결 테이블을 사이에 두고 M:N 관계를 설정합니다.
    • A.belongsToMany(B, { through: 'C' }) 를 직역하면 “A는 C를 통하여 B를 여러개 가진다”

sequelize에선 보통 BelongsTo와 hasOne/hasMany 를 쌍으로 사용합니다. 둘 중 하나의 메소드만 옳게 사용해도 외래 키는 설정하지만 각 객체에 메소드 (getter/setter)를 사용하기 위해 쌍으로 사용하는 것이 좋습니다.

상황 1. 테이블 A와 B가 1:1 관계를 가질 때

이런 경우 테이블 A 또는 B 둘 중 하나에 외래 키를 집어 넣어서 관계를 형성하게 한다. 이번 상황은 테이블 B가 외래 키 (aId → 테이블 A의 ID)를 가진다고 가정합니다.

구현

다음과 같이 설정합니다.

A.hasOne(B);
B.belongsTo(A);

이렇게 설정한다면 외래 키는 B에 삽입되는 것을 알 수 있다. 위와 같이 관계를 설정한다면 MySQL 기준으로 외래 키는 다음과 같이 설정됩니다.

CREATE TABLE IF NOT EXISTS "B" (
    /* ... 생략 ... */
    `aId` INTEGER NOT NULL,
    /* ... 생략 ... */
    FOREIGN KEY (`aId`) REFERENCES `A` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
);

외래 키 설정 뿐만 아니라 sequelize가 자동으로 외래 키 컬럼까지 추가해 주는 것에 주목해야 합니다. 기존에 외래 키를 설계하는 것을 염두에 두고 외래 키까지 만드는 것으로 설정할 경우 sequelize는 카멜 케이스 규칙에 의거하여 외래 키를 별도로 만듭니다.

그래서 키 명이 카멜 케이스를 따르지 않고, 기존 테이블에 존재하는 컬럼을 외래 키로 사용하고자 하는 경우 별도로 설정을 해야 합니다.

만약 a_id 라는 컬럼이 B에 존재하고 이를 외래 키로 사용하고자 하는 경우는 다음과 같이 설정해야 합니다.

// Option 1
A.hasOne(B, {
  foreignKey: 'a_id'
});
B.belongsTo(A, {
  foreignKey: 'a_id'
);

// Option 2
A.hasOne(B, {
  foreignKey: {
    name: 'a_id'
  }
});
B.belongsTo(A, {
  foreignKey: {
    name: 'a_id'
  }
});

둘 중 한곳에만 커스텀 된 이름을 사용한다면 키가 두개가 생깁니다. (공식 메뉴얼에는 한 곳에만 작성했지만 둘 다 혹은 한 곳에만 사용해야 한다는 언급은 없으며 쿼리문을 직접 분석해보면 한곳에만 설정하면 키가 두개가 생기는 현상을 겪었습니다. 관련 이슈 : https://github.com/sequelize/sequelize/issues/13232)

상황 2. 테이블 A와 B가 1:N 관계를 가질 때

A와 B가 1:N 관계라면 B가 외래 키를 가지는 것이 옳습니다. 예를 들면 Team과 Player 관계가 있습니다.

구현

다음과 같이 설정합니다.

Team.hasMany(Player);
Player.belongsTo(Team);

이렇게 설정한다면 외래 키는 Player에 삽입되는 것을 알 수 있습니다. 위와 같이 관계를 설정한다면 MySQL 기준으로 외래 키는 다음과 같이 설정됩니다.

CREATE TABLE IF NOT EXISTS "Player" (
    /* ... 생략 ... */
    `TeamId` INTEGER NOT NULL,
    /* ... 생략 ... */
    FOREIGN KEY (`TeamId`) REFERENCES `Team` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
);

커스텀 된 이름을 사용하는 사례는 위와 동일합니다.

상황 3. 테이블 A와 B가 M:N 관계를 가질 때

WIP

연관 데이터 접근

sequelize에선 테이블 간 연관 관계를 정하고 이 관계에 기반하여 데이터를 넣거나 가져올 수 있습니다. sequelize에선 다른 ORM이 그렇듯이 지연 로딩 (Lazy Loading)과 즉시 로딩(Eager Loading)을 지원합니다.

지연 로딩

A와 B가 1:1 관계일 때를 가정하여 지연 로딩의 예시를 들어 봅니다.

// A, B 모델링 생략...
A.hasOne(B);
B.belongsTo(A);
// 기타 코드 생략...
const a_data = await A.findOne({
  where: {
    // 특정 조건 생략...
  }
});
// a_data : 조건에 따른 a의 데이터 객체가 추출됨
// getB : B에서 A의 키를 이용해 데이터를 가져옴
const b_about_a = await a_data.getB();

A.hasOne(B); 으로 인해 A 클래스에 getB 라는 메소드가 자동으로 생성되었습니다. 이 메소드 명은 테이블 이름을 따르게 됩니다.

이 예시는 먼저 테이블 A에 접근을 하고 이후에 테이블 B에 접근을 하게 됩니다.

TypeSctipt에서 지연 로딩 사용 시 주의할 점

타입스크립트에선 컴파일 타임에 모델 관계를 정의할 수 없으므로 해당 메소드를 호출하는 클래스에 정의해야 합니다.

class A extends Model<...> implements ... {
  // 생략

  // HasOneGetAssociationMixin은 sequelize에 정의되어 있다.
  declare getB: HasOneGetAssociationMixin<B>;
  // 생략
}

즉시 로딩

즉시 로딩은 include 옵션을 이용해 한번에 A와 B 테이블 모두를 조회합니다.

// A, B 모델링 생략...
A.hasOne(B);
B.belongsTo(A);
// 기타 코드 생략...
const a_data = await A.findOne({
  where: {
    // 특정 조건 생략...
  },
  include: B
});

참조 링크

Sequelize manual 1

Sequelize manual 2

⚠️ **GitHub.com Fallback** ⚠️