단일 테이블 상속

요약: 단일 테이블 상속 (STI)을 사용하여 새 테이블을 설계하지 마십시오. STI 패턴을 사용하는 기존 테이블의 경우 새 유형을 추가하지 않고 별도의 테이블로 분리하는 것이 좋습니다.

STI는 단일 테이블에 다른 유형의 레코드를 저장하는 데이터베이스 설계 패턴입니다. 이러한 레코드에는 공유 열의 하위 집합과 응용 프로그램에게 해당 레코드를 나타내어야 하는 객체를 지시하는 다른 열이 있습니다. 이를 사용하면 예를 들어 동일한 테이블에 두 가지 다른 유형의 SSH 키를 저장할 수 있습니다. ActiveRecord는 이를 사용하고 STI 사용을 더 편리하게 만드는 몇 가지 기능을 제공합니다.

우리는 더 이상 새로운 STI 테이블을 허용하지 않습니다. 왜냐하면 이러한 이유 때문입니다:

  • 테이블이 작아야 하는데 많은 수의 행을 포함하게 됨으로써 발생하는 문제
  • 가벼운 락 사용량을 증가시키는 추가 인덱스가 필요하여 포화가 문제가 될 수 있음
  • 모든 데이터를 값으로 필터링해야 하므로 추가 오버헤드가 발생하여 읽기 시 더 많은 페이지 접근이 발생함
  • class_name을 사용하여 개체에 대한 올바른 클래스를 로드하지만 클래스 이름을 저장하는 것은 소모적이고 불필요함

STI 대신 다음 대안을 고려하세요:

  • 각 유형마다 다른 테이블을 사용합니다.
  • *_type 열을 추가하지 않습니다. 이는 미래에 새 유형이 추가될 수 있다는 코드 스멜일 수 있으며, 나중에 리팩토링이 훨씬 어려워질 수 있습니다.
  • 이미 _type 열에서 실질적으로 STI인 테이블이 있는 경우:
    • 기존 데이터를 여러 테이블로 분할합니다.
    • 새 유형을 새 테이블로 추가하면서 기존 유형을 유지할 수 있도록 리팩토링합니다 (예: 기본 클래스의 로직을 concern으로 이동).

위의 모든 단점과 대안을 고려한 후에도 STI가 해당 문제에 대한 유일한 해결책이라면, 적어도 레코드에 클래스 이름을 저장하는 문제를 EnumInheritance concern을 사용하여 열거형 타입으로 대신함으로써 피할 수 있습니다.

class Animal < ActiveRecord::Base
  include EnumInheritance
  
  enum species: {
    dog: 1,
    cat: 2
  }
  
  def self.inheritance_column_to_class_map = {
    dog: 'Dog',
    cat: 'Cat'
  }
  
  def self.inheritance_column = 'species'
end

class Dog < Animal
  self.allow_legacy_sti_class = true
end

class Cat < Animal
  self.allow_legacy_sti_class = true
end

이미 테이블에 *_type이 있는 경우, 필요에 따라 다른 유형의 새 클래스를 추가할 수 있습니다.

마이그레이션에서

모델이 마이그레이션에서 사용될 때마다, 단일 테이블 상속(STI)은 비활성화되어야 합니다. Rails가 연관을 로드하는 방식으로 인해(STI조차도 마찬가지) STI를 비활성화하지 않으면 예상치 못한 코드나 연관이 로드될 수 있으며, 이는 의도하지 않은 부작용이나 업그레이드 중 실패를 일으킬 수 있습니다.

class SomeMigration < Gitlab::Database::Migration[2.1]
  class Services < MigrationRecord
    self.table_name = 'services'
    self.inheritance_column = :_type_disabled
  end
  
  def up
  ...

모델에 EachBatch 이외에 비활성화할 STI가 추가로 필요한 경우, 클래스를 정의하는 대신 도우미 define_batchable_model을 사용하십시오. 이렇게 하면 마이그레이션이 마이그레이션에 대한 열을 격리하여 로드하고 도우미는 기본적으로 STI를 비활성화합니다.

class EnqueueSomeBackgroundMigration < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!
  
  def up
    define_batchable_model('services').select(:id).in_batches do |relation|
      jobs = relation.pluck(:id).map do |id|
        ['ExtractServicesUrl', [id]]
      end
      
      BackgroundMigrationWorker.bulk_perform_async(jobs)
    end
  ...