엘리스 AI트랙 4기/프로젝트

[Sequelize] Nested Eager Loading

남쪽마을밤송이 2022. 6. 22. 21:59

 [01 Eager Loading이란? ] 

 (1) 개념 

  • 서비스가 커질수록, 참조하는 객체가 많아지고, 객체가 가지는 데이터의 양이 많아진다.
  • 이렇게 객체가 커지면 DB로부터 "참조하는 객체들의 데이터"까지 한꺼번에 가져오는 행동은 부담이 커진다.
  • 따라서 JPA는 참조하는 객체들의 데이터를 가져오는 시점을 정할 수 있는데, 이것을 Fetch Type이라고 한다.
  • 그리고 이 Fetch Type이 Eager와 Lazy 두 가지로 나뉜다.
  • Eager Loading은 한 마디로 하나의 객체를 DB로부터 읽어올 때 참조 객체들의 데이터까지 전부 열심히 읽어오는 방식을 뜻한다. 반대로 Lazy Loading은 게을러서 참조 객체들의 데이터들은 무시하고 해당 엔티티의 데이터만을 가져온다.
    • 지금까지 쿼리를 짤 때는 따로 join을 신경 쓰지 않았기 때문에 대부분 Lazy Loading 방식을 디폴트로 사용했는데 이번에 처음 Eager Loading을 사용해 보고 이러한 개념을 알게 되었다.

참고 : https://velog.io/@bread_dd/JPA%EB%8A%94-%EC%99%9C-%EC%A7%80%EC%97%B0-%EB%A1%9C%EB%94%A9%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C

 (2) Sequelize의 Eager Loading 

  • Eager Loading을 하려면 참조할 객체들이 존재한다는 것이기 때문에 어쨌든 외래키가 있거나 테이블 간의 join을 하는 과정이 필요하다.
  • 그런데 Sequelize는 다른 ORM과 다르게 특이한 점이 Associate 관계에 있는 테이블 관계만 join을 가능하게 해두었다.
    • 원래 MySQL 쿼리로나 다른 ORM은 Associate 관계가 아닌 테이블들끼리도 잘 join 가능하다.
    • 나는 연관 관계가 없는 두 테이블의 겹치는 컬럼을 기준으로 join을 하고 싶었는데, Sequelize로는 불가능하다는 것을 깨닫고 외래키를 이용한 Eager Loading을 사용했다.
  • 어떤 방식이든 Sequelize에서 다른 객체를 참조하려면(Eager Loading 사용하려면) finder query에서 include 옵션을 사용한다.

참고 : https://sequelize.org/docs/v6/advanced-association-concepts/eager-loading/

 

 [02 참고 사항 ] 

 (1) 참조된 모든 테이블 불러오기 

  • 먼저 참조된(associated) 모든 테이블의 정보를 가져오고 싶다면  all = true, nested = true 옵션을 사용하면 된다.
    • 이 옵션을 사용해 실행시켜 보고 모델간의 n:m 관계 설정이 제대로 되지 않아 있다는 사실을 깨달았다.
    • n:m은 1:n 관계가 양쪽으로 설정되어 있는 것과 같은데, 확인해보니 한 쪽만 다른 테이블과 연결되어 있는 상태였다. 원래 그런 용도는 아니겠지만 관계 설정이 잘 되었는지 헷갈릴 때 확인해보기 좋은 것 같다.
const supplementSchedules = await Schedules.findAll({
      include: {
        all: true,
        nested: true,
      },
    });

 (2) Inner Join vs Outer Join 

  • join의 종류에 대해 공부한지가 너무 오래돼서 이번 기회에 복습했다.
  • 간단하게 inner join은 join 조건에 부합하는 행만 join이 발생하는 것이라면, outer join은 조건에 부합하지 않는 행까지도 포함시켜 결합하는 것을 의미한다.
  • 따라서 null값이 발생하더라도 양 쪽 테이블의 모든 컬럼이 살아 있길 바란다면 outer join, 값이 존재하는(의미있는) 데이터만 남기고 합치고 싶다면 inner join을 사용하면 된다.
  • Sequelize에서 include 옵션을 사용하면 default로 required = false 옵션이 들어가있어 left outer join이 실행된다.
  • inner join을 사용하고 싶다면 반드시 존재해야 한다는 의미로  required = true 옵션을 넣어주자.

참조 : http://egloos.zum.com/sweeper/v/3002220 | http://egloos.zum.com/sweeper/v/3002133 

 

 [03 코드 적용 ] 

  • 완성된 코드는 다음과 같다.
  • 중요한 포인트는 nested 된 모델을 where문에서 선택하고 싶다면 양 옆에  $ 기호를 붙여주고 모델 참조 순서대로  . 을 붙여 안쪽으로 들어간다. 그리고 자기 자신의 컬럼을 선택하려면 Sequelize의  col  메소드를 import하여 아래와 같이 사용하면 된다.
const supplementSchedules = await Schedules.findAll({
      attributes: ["to_do"],
      where: { type: "S", start: time, "$User.DailySupplements.type$": { [Op.eq]: col("Schedules.to_do") } },
      include: {
        required: true, // inner join
        model: Users,
        attributes: ["user_name"],
        include: [
          {
            required: true,
            model: Subscribes,
            attributes: ["device_token"],
          },
          {
            required: true,
            model: DailySupplements,
            attributes: ["fk_supplement_id"],
            include: [
              {
                required: true,
                model: Supplements,
                attributes: ["name"],
              },
            ],
          },
        ],
      },
    });
  • 모델 참조 순서는 다음과 같다.
Schedules -> Users -> { Subscribes, DailySupplements -> { Supplements } }
  • 결론적으로 총 다섯개의 테이블을 참조하는건데, Schedules와 DailySupplements가 직접적으로 외래키 설정이 되어 있거나 join이 가능하지 않아 MongoDB 때처럼 각각 조회하려 했지만 그렇게 되면 Database에 쿼리를 몇 배로 요청하기 때문에 이 방법이 훨씬 효율적이다.

 


+) 2022. 06. 29에 추가

영양제 일정만 있는 회원에게도 영양제 정보 빼고 알림을 보내주기 위해 DailySupplements만 outer join하는 것으로 변경했다. 또 이렇게 변경시 DailySupplements.type과 Schedules.to_do가 다른 경우에도 모두 불러와버리기 때문에 where문의 위치를 DailySuppelemnts 안으로 넣었다.

const supplementSchedules = await Schedules.findAll({
      attributes: ["to_do"],
      where: { type: "S", start: time },
      include: {
        required: true, // inner join
        model: Users,
        attributes: ["pk_user_id", "user_name", "email"],
        include: [
          {
            required: true, // inner join
            model: Subscribes,
            attributes: ["device_token"],
          },
          {
            required: false, // outer join, 영양제 일정만 있는 회원 정보도 불러오기
            model: DailySupplements,
            attributes: ["fk_supplement_id"],
            where: { "$User.DailySupplements.type$": { [Op.eq]: col("Schedules.to_do") } },
            include: [
              {
                required: true, // inner join
                model: Supplements,
                attributes: ["name"],
              },
            ],
          },
        ],
      },
    });

 DailySupplements 정보 있는 경우 

 DailySupplements 정보 없는 경우