[Rails] 엔음의 단점을 생각하세요.

33408 단어 Railsenumtech

개시하다


나는 Rails로 응용 프로그램을 구축할 때 자주 enum을 사용한다고 생각한다.
그러나 DB 속 엔움형은'SQL 역모드'라는 책 속'31Flavors'의 역모드로 널리 알려져 있다.
이번에는 엔움의 장점과 단점에 대해 생각하고 정리해 봤다.

전제 조건

  • 본고에서 처리한 enum은 DB의 enum형이 아니라 Rails 기능으로 제공된 enum을 가리킨다.
  • Ruby: 2.7.1
  • Rails: 6.0.3.4
  • MySQL: 5.7.29
  • 이 보도의 결론

  • enum을 사용하는 것은 나쁜 일이 아니라 장점과 단점을 파악한 토대에서 사용한다.
  • enum의 단점을 보완하기 위한 해결 방안이 존재한다는 것을 인식해야 한다.
  • 는 적당한 시기에 enum을 사용하지 않는 옵션을 고려해야 한다(다른 표를 잘라내는 방법으로).
  • 이른바 enum


    열거형입니다.예를 들어'남=0','여=1'로 DB에 저장하려고 할 때 사용한다.
    예를 들어, User 클래스가 있다고 가정합니다.속성은 "name(이름)", "gender(성별)", "prefecture(도도부현)"입니다.
    gender와 prefecture를 enum으로 정의합니다.
    app/models/user.rb
    class User < ApplicationRecord
      enum gender: { man: 0, woman: 1 }, _prefix: true
      enum prefecture: { tokyo: 0, osaka: 1, nagoya: 2 }, _prefix: true
    end
    
    enum에서 정의한 값을 DB에 수치로 저장합니다.
    SELECT * FROM users;
    
    | id | name  | gender | prefecture | created_at                 | updated_at                 |
    |----|-------|--------|------------|----------------------------|----------------------------|
    |  1 | user1 |      0 |          0 | 2021-09-23 15:37:30.781431 | 2021-09-23 15:37:30.781431 |
    |  2 | user2 |      0 |          1 | 2021-09-23 15:37:30.790889 | 2021-09-23 15:37:30.790889 |
    |  3 | user3 |      1 |          0 | 2021-09-23 15:37:30.801230 | 2021-09-23 15:37:30.801230 |
    |  4 | user4 |      1 |          2 | 2021-09-23 15:37:30.809486 | 2021-09-23 15:37:30.809486 |
    
    rails console에서 수치를 얻으면enum에서 정의한 manwoman 등 문자열로 수치를 검색할 수 있습니다.
    Rails 측부터 사용할 때
    User.first.gender
    # => "man"
    
    User.first.prefecture
    # => "tokyo"
    
    # 男性のUser一覧を取得したいとき
    User.gender_man
    
    # 東京都のUser一覧を取得したいとき
    User.prefecture_tokyo
    
    # 東京都以外のUser一覧を取得したいとき
    User.not_prefecture_tokyo
    
    # 定義されている性別一覧を取得したいとき
    User.genders
    # => {"man"=>0, "woman"=>1}
    
    # 定義されている都道府県一覧を取得したいとき
    User.prefectures
    # => {"tokyo"=>0, "osaka"=>1, "nagoya"=>2}
    

    해결책


    enum 이외의 설치 방법으로'enum이 정의한 부분을 다른 표로 잘라내는 방법'이 있다.
    이번 예에서 우리는genders표와prefectures표를 만들었다.

    사용자 테이블
    | id | name   | gender_id | prefecture_id | created_at                 | updated_at                 |
    |----|--------|-----------|---------------|----------------------------|----------------------------|
    |  1 | user1 |         1 |             1 | 2021-09-23 15:37:30.930411 | 2021-09-23 15:37:30.930411 |
    |  2 | user2 |         1 |             2 | 2021-09-23 15:37:30.939223 | 2021-09-23 15:37:30.939223 |
    |  3 | user3 |         2 |             1 | 2021-09-23 15:37:30.950641 | 2021-09-23 15:37:30.950641 |
    |  4 | user4 |         2 |             3 | 2021-09-23 15:37:30.959353 | 2021-09-23 15:37:30.959353 |
    
    genders표
    | id | name  | created_at                 | updated_at                 |
    |----|-------|----------------------------|----------------------------|
    |  1 | man   | 2021-09-23 15:37:30.863884 | 2021-09-23 15:37:30.863884 |
    |  2 | woman | 2021-09-23 15:37:30.871785 | 2021-09-23 15:37:30.871785 |
    
    prefectures 표
    | id | name   | created_at                 | updated_at                 |
    |----|--------|----------------------------|----------------------------|
    |  1 | tokyo  | 2021-09-23 15:37:30.887358 | 2021-09-23 15:37:30.887358 |
    |  2 | osaka  | 2021-09-23 15:37:30.896177 | 2021-09-23 15:37:30.896177 |
    |  3 | nagoya | 2021-09-23 15:37:30.905950 | 2021-09-23 15:37:30.905950 |
    
    app/models/user.rb
    class User < ApplicationRecord
      belongs_to :gender
      belongs_to :prefecture
    end
    
    Rails 측부터 사용 시
    User.first.gender.name
    # => "man"
    
    User.first.prefecture.name
    # => "tokyo"
    
    # 男性のUser一覧を取得したいとき
    User.joins(:gender).merge(Gender.where(name: 'man'))
    
    # 東京都のUser一覧を取得したいとき
    User.joins(:prefecture).merge(Prefecture.where(name: 'tokyo'))
    
    # 東京都以外のUser一覧を取得したいとき
    User.joins(:prefecture).merge(Prefecture.where.not(name: 'tokyo'))
    
    # 定義されている性別一覧を取得したいとき
    Gender.all
    # => [#<Gender:0x00007f94e6d10ee8
      id: 1,
      name: "man",
      created_at: Thu, 23 Sep 2021 15:37:30 UTC +00:00,
      updated_at: Thu, 23 Sep 2021 15:37:30 UTC +00:00>,
     #<Gender:0x00007f94e6d10e20
      id: 2,
      name: "woman",
      created_at: Thu, 23 Sep 2021 15:37:30 UTC +00:00,
      updated_at: Thu, 23 Sep 2021 15:37:30 UTC +00:00>]
    
    # 定義されている都道府県一覧を取得したいとき
    Prefecture.all
    # => [#<Prefecture:0x00007f94e76eea00
      id: 1,
      name: "tokyo",
      created_at: Thu, 23 Sep 2021 15:37:30 UTC +00:00,
      updated_at: Thu, 23 Sep 2021 15:37:30 UTC +00:00>,
     #<Prefecture:0x00007f94e76ee8e8
      id: 2,
      name: "osaka",
      created_at: Thu, 23 Sep 2021 15:37:30 UTC +00:00,
      updated_at: Thu, 23 Sep 2021 15:37:30 UTC +00:00>,
     #<Prefecture:0x00007f94e76ee7f8
      id: 3,
      name: "nagoya",
      created_at: Thu, 23 Sep 2021 15:37:30 UTC +00:00,
      updated_at: Thu, 23 Sep 2021 15:37:30 UTC +00:00>]
    

    num의 장점과 단점


    그렇다면 상술한 enum을 다른 표의 방법으로 비교할 때의 enum의 장점과 단점을 고려해 보자.

    num의 장점


    장점책상의join과select 횟수를 줄이다


    사용자의 성별을 문자열로 비교합니다.
    enum에서 정의된 상황
    User.first.gender
      User Load (6.4ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
    => "man"
    
    다른 테이블로 잘라낸 경우
    User.first.gender.name
      User Load (2.8ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
      Gender Load (3.0ms)  SELECT `genders`.* FROM `genders` WHERE `genders`.`id` = 1 LIMIT 1
    => "man"
    
    다른 표로 자르면 Gender 표에서도 SELECT 문장이 실행되기 때문에 얻을 때의 성능이 떨어진다.

    장점Rails 측에서 편리한 방법 제공


    enum만 정의하면 User.gender_manUser.not_gender_man 같은 편리한 방법으로 설치 원가를 줄일 수 있다.

    장점소스 코드만 수정하면 새 값을 추가할 수 있습니다


    DB에 대한 INSERT 없이 소스 코드를 수정하면 새로운 enum 값을 추가할 수 있어 상황에 따라 값을 늘리는 운용 비용이 줄어들 수 있다.
    만약 다른 표로 잘라낼 때 수치를 추가해야 한다
  • 관리 화면에서 새 값을 추가할 수 있는 UI 생성 여부
  • 를 고려해야 합니다.
  • 회사에 따라 안전성 때문에 DB에 직접 문의하는 것은 여러 번의 검사가 필요하다
  • 이런 결점이 발생할 수 있다.

    num의 단점


    결점DB에서만 enum의 정의를 확인할 수 없음

  • enum의 정의(gender의 0은 남성 1은 여성)를 확인하려면 DB가 아닌 원본 코드를 확인해야 한다.
  • 결점DB 등급에 제약을 가하기 어렵다

  • Rails에 enum을 설치할 때 DB의 유형으로 인덱스로 이루어지지만 이 경우 enum으로 정의되지 않은 값을 저장할 수 있다.
  • gender는'0:남성, 1:여성'만 정의했지만 DB에 UPDATE 글을 직접 쓰면'2'로 업데이트할 수 있다.
  • 도 DB의 열 자체를 ENUM형으로 설정할 수 있지만, 이 경우 새 추가값 시 소스 코드뿐만 아니라 User 표에 Alter TABLE를 추가해야 해 운영 비용이 증가한다.
  • 결점다른 표와 관계를 맺을 수 없기 때문에 귀일화할 수 없다


    개인적으로 엔움을 사용하는 것이 결점이 될 가능성이 가장 높은 부분이라고 생각합니다.반대로 다른 표에서 관계를 맺어 규범화할 필요가 없다면 엔움으로 정의하는 것은 문제없다.
    예를 들어 업무상 다음과 같은 요건이 발생했다.
  • 사용자에게 알리는 화면을 표시하려고 합니다.
  • 사용자의 성별과 도도부현에 따라 모든 사용자에게 표시를 변경하고자 하는 통지.여러 조건을 선택할 수 있습니다.
  • 남성에게만 표시되는 알림
  • 도쿄도나 오사카부에 사는 사용자에게만 표시되는 알림
  • "도쿄도나 오사카부에 살고 있다"며 "여성"사용자에게만 표시되는 통지
  • 여러 개를 선택할 수 있도록 요구하기 때문에 노티픽션에'gender'와'prefecture'표시줄만 추가하는 것은 불가능하다.
    이번에는 json형을 사용해서 실현하기로 결정했다.(설명을 생략하지만 제이슨형 혈액형은 고통스러운 일이 많기 때문에 기본적으로 사용하지 말아야 한다)
    Notification 테이블
  • title: 알림 제목
  • body: 알림 본문
  • display_condition: 표시 조건
  • | id | title         | body       | display_condition                       | created_at                 | updated_at                 |
    |----|---------------|------------|-----------------------------------------|----------------------------|----------------------------|
    |  1 | notifiaction1 | 大事なお知らせです1 | {"genders": [], "prefectures": []}      | 2021-09-23 15:37:30.825805 | 2021-09-23 15:37:30.825805 |
    |  2 | notifiaction2 | 大事なお知らせです2 | {"genders": [0], "prefectures": []}     | 2021-09-23 15:37:30.833728 | 2021-09-23 15:37:30.833728 |
    |  3 | notifiaction3 | 大事なお知らせです3 | {"genders": [], "prefectures": [0, 1]}  | 2021-09-23 15:37:30.841235 | 2021-09-23 15:37:30.841235 |
    |  4 | notifiaction4 | 大事なお知らせです4 | {"genders": [1], "prefectures": [0, 1]} | 2021-09-23 15:37:30.849499 | 2021-09-23 15:37:30.849499 |
    
    app/models/notification.rb의 한 예
    class Notification < ApplicationRecord
      class << self
        def displayable_to(user)
          notification_ids = Notification.all.select { |notification| notification.displayable_to?(user) }.map(&:id)
          Notification.where(id: notification_ids)
        end
      end
    
      def displayable_to?(user)
        (display_condition['genders'].blank? || display_condition['genders'].include?(user.gender_before_type_cast)) &&
          (display_condition['prefectures'].blank? || display_condition['prefectures'].include?(user.prefecture_before_type_cast))
      end
    end
    
    (※ 표시조건을 선택하지 않으면 빈 진열에 진입)
    이렇게 실현하는 것도 가능하지만 몇 가지 문제가 있다.
  • 존재하지 않는 값을 추가할 수 있습니다(참고 일치성을 유지할 수 없습니다).
  • "genders": [3]라는 값을 입력할 수 있습니다.
  • 제1정규 형식을 위반한다.
  • 이는 엔움의 단점이라기보다는 json형의 단점이지만,'1열 중 1개의 값'이라는 제1정규형 정의를 위반하고 RDB의 근본적인 생각을 위반한 것이다.
  • 위의 구현을 테이블 또는 prefecture로 정의하면 테이블 간에 관계를 설정할 수 있습니다.

    notification 테이블
    | id | title         | body       | created_at                 | updated_at                 |
    |----|---------------|------------|----------------------------|----------------------------|
    |  1 | notifiaction1 | 大事なお知らせです1 | 2021-09-23 15:37:30.825805 | 2021-09-23 15:37:30.825805 |
    |  2 | notifiaction2 | 大事なお知らせです2 | 2021-09-23 15:37:30.833728 | 2021-09-23 15:37:30.833728 |
    |  3 | notifiaction3 | 大事なお知らせです3 | 2021-09-23 15:37:30.841235 | 2021-09-23 15:37:30.841235 |
    |  4 | notifiaction4 | 大事なお知らせです4 | 2021-09-23 15:37:30.849499 | 2021-09-23 15:37:30.849499 |
    
    notification_genders 테이블
    | id | notification_id | gender_id | created_at                 | updated_at                 |
    |----|-----------------|-----------|----------------------------|----------------------------|
    |  1 |               2 |         1 | 2021-09-23 15:37:31.028341 | 2021-09-23 15:37:31.028341 |
    |  2 |               4 |         2 | 2021-09-23 15:37:31.037408 | 2021-09-23 15:37:31.037408 |
    
    notification_prefectures 테이블
    | id | notification_id | prefecture_id | created_at                 | updated_at                 |
    |----|-----------------|---------------|----------------------------|----------------------------|
    |  1 |               3 |             1 | 2021-09-23 15:37:31.055711 | 2021-09-23 15:37:31.055711 |
    |  2 |               3 |             2 | 2021-09-23 15:37:31.064761 | 2021-09-23 15:37:31.064761 |
    |  3 |               4 |             1 | 2021-09-23 15:37:31.075442 | 2021-09-23 15:37:31.075442 |
    |  4 |               4 |             2 | 2021-09-23 15:37:31.083863 | 2021-09-23 15:37:31.083863 |
    
    app/models/notification.rb의 한 예
    class Notification < ApplicationRecord
      has_many :notification_genders
      has_many :notification_prefectures
      has_many :genders, through: :notification_genders
      has_many :prefectures, through: :notification_prefectures
    
      class << self
        def displayable_to(user)
          Notification.left_joins(:notification_genders, :notification_prefectures)
                      .merge(NotificationGender.where(id: nil).or(NotificationGender.where(gender_id: user.gender_id)))
                      .merge(NotificationPrefecture.where(id: nil).or(NotificationPrefecture.where(prefecture_id: user.prefecture_id)))
                      .order(:id)
        end
      end
    end
    
    (디스플레이 조건을 선택하지 않으면 notification genders나 notification prefectures 테이블의 기록을 만들지 않습니다)
    위에서 설명한 대로 구현하여 DB의 정규화를 유지합니다.id에 외부 키 제한을 붙이면gender가 존재하지 않습니다id 저장도 방지할 수 있습니다.
    기본적으로 RDB는 정규화되어야 한다. 이것은 선인의 경험 법칙이기 때문에 이번 예에서 다른 표로 제시한 설치 방법을 선택해야 한다.

    언제 엔um을 사용하고 언제 책상으로 자를까요


    라일스의 엔음은 준비하는 방법이 많아 편리하기 때문에 기본적으로 엔음으로 한다는 방침에는 문제가 없다.
    단, 아래의 상황은 다른 탁자로 잘라야 한다.
  • 다른 표와 관계를 맺고 귀일화하기를 희망
  • 참고 일치를 유지하지 않으면 치명적인 악취충
  • 일 수 있습니다

    최후


    이번에는 엔움의 장점과 단점에 대해 생각했다.다음은 이 보도의 결론의 재발표이다.
  • enum을 사용하는 것은 나쁜 일이 아니라 장점과 단점을 파악한 토대에서 사용한다.
  • enum의 단점을 보완하기 위한 해결 방안이 존재한다는 것을 인식해야 한다.
  • 는 적당한 시기에 enum을 사용하지 않는 옵션을 고려해야 한다(다른 표를 잘라내는 방법으로).
  • 조금만 참고해주시면 감사하겠습니다.
    그리고 더 좋은 해결 방안과 의견이 있다면 저에게 말씀해 주시면 기쁩니다.

    좋은 웹페이지 즐겨찾기