InfoGrab DocsInfoGrab Docs

Fixed Items 모델

요약

ActiveRecord::FixedItemsModel을 사용하면 데이터베이스 테이블 대신 코드에서 정적인 읽기 전용 데이터를 정의할 수 있습니다. 이 패턴은 다음과 같은 경우에 데이터베이스 기반 룩업 테이블을 대체합니다:

ActiveRecord::FixedItemsModel을 사용하면 데이터베이스 테이블 대신 코드에서 정적인 읽기 전용 데이터를 정의할 수 있습니다. 인스턴스는 ActiveRecord 객체처럼 동작하지만 결정론적이고 버전으로 관리되는 ID와 함께 메모리에 저장됩니다.

이 패턴은 다음과 같은 경우에 데이터베이스 기반 룩업 테이블을 대체합니다:

  • 데이터가 정적이며 코드 배포를 통해서만 변경됩니다.

  • Cells 전반에 걸쳐 전역적으로 일관된 ID가 필요합니다.

  • 룩업 시 데이터베이스 쿼리가 전혀 없어야 합니다.

사용 시기#

다음과 같은 경우에 FixedItemsModel을 사용합니다:

  • 데이터가 코드에 정의되어 있고 배포를 통해서만 변경됩니다.

  • ID가 모든 Cells와 환경에서 동일해야 합니다.

  • 데이터가 런타임에 변경되지 않습니다(사용자가 생성한 레코드 없음).

데이터셋이 작거나 정적으로 정의될 필요는 없습니다. .fixed_items 클래스 메서드를 사용하여 다른 소스로부터 동적으로 항목을 계산할 수 있습니다. 예를 들어, WidgetDefinition은 모든 work item 타입과 해당 위젯 설정을 순회하여 항목을 생성합니다. 항목에 영속성 레이어를 위한 안정적인 ID가 필요하지 않을 경우 auto_generate_ids!를 사용합니다.

다음과 같은 경우에는 FixedItemsModel을 사용하지 않습니다:

  • 사용자나 관리자가 런타임에 레코드를 생성, 수정, 삭제할 수 있습니다.

  • 레코드에 has_many 또는 has_many :through와 같은 데이터베이스 수준의 연관 관계가 필요합니다.

Cells 아키텍처 컨텍스트#

자동 증가 시퀀스가 있는 데이터베이스 테이블은 같은 논리적 엔티티에 대해 서로 다른 Cells에서 서로 다른 ID를 생성합니다. Cell A의 plan_id = 4premium을 의미하는 반면 Cell B의 plan_id = 4gold를 의미할 수 있기 때문에 크로스 셀 참조가 깨질 수 있습니다.

FixedItemsModel은 애플리케이션 코드에 ID를 하드코딩하여 이 문제를 해결합니다. 모든 Cell은 동일한 정의를 로드하고 동일한 ID를 생성합니다. 자세한 내용은 Cells 개발 가이드라인의 정적 데이터 섹션을 참조하세요.

기본 사용법#

ITEMS 상수를 사용한 모델 정의#

가장 간단한 패턴은 인라인으로 항목을 정의합니다:

module Security
  class StaticTrainingProvider
    include ActiveRecord::FixedItemsModel::Model

    ITEMS = [
      { id: 1, name: "Kontra", url: "https://application.security/api/webhook/gitlab/exercises/search" },
      { id: 2, name: "Secure Code Warrior", url: "https://integration-api.securecodewarrior.com/api/v1/trial" },
      { id: 3, name: "SecureFlag", url: "https://knowledge-base-api.secureflag.com/gitlab" }
    ].freeze

    attribute :name, :string
    attribute :url, :string
  end
end

각 항목에는 양의 정수 값을 가진 id 키가 있어야 합니다. ID가 아닌 각 속성은 attribute로 선언합니다. id 속성은 자동으로 선언됩니다.

ITEMS 상수 또는 .fixed_items 클래스 메서드 중 하나만 사용해야 하며, 둘 다 함께 사용할 수 없습니다.

.fixed_items를 사용한 모델 정의#

항목이 다른 소스에서 파생될 때 클래스 메서드를 사용합니다:

module WorkItems
  module TypesFramework
    module SystemDefined
      class Type
        include ActiveRecord::FixedItemsModel::Model

        attribute :name, :string
        attribute :base_type, :string
        attribute :icon_name, :string

        class << self
          def fixed_items
            [
              Definitions::Issue.configuration,
              Definitions::Incident.configuration,
              Definitions::Task.configuration,
              Definitions::Ticket.configuration
            ]
          end
        end
      end
    end
  end
end

각 정의 클래스는 id, name, base_type, icon_name 키가 있는 해시를 반환합니다. 이 패턴은 각 타입의 도메인 로직 가까이에 데이터 정의를 유지합니다.

ID 자동 생성#

항목에 안정적이고 외부에서 참조되는 ID가 필요하지 않을 경우 auto_generate_ids!를 사용합니다. ID는 배열 순서에 따라 1부터 순차적으로 할당됩니다. 이는 ActiveRecord와 유사한 쿼리 인터페이스(find_by, where, all)가 필요하지만 객체가 내부용이고 해당 ID가 절대 영속되거나 API를 통해 노출되지 않을 때 유용합니다.

WidgetDefinition이 좋은 예입니다. 모든 work item 타입과 해당 위젯 설정으로부터 동적으로 항목을 생성합니다. ID는 임시 핸들로, 중요한 것은 widget_typework_item_type_id의 조합입니다:

class WidgetDefinition
  include ActiveRecord::FixedItemsModel::Model
  include ActiveRecord::FixedItemsModel::HasOne

  auto_generate_ids!

  attribute :widget_type, :string
  attribute :work_item_type_id, :integer

  belongs_to_fixed_items :work_item_type,
    fixed_items_class: WorkItems::TypesFramework::SystemDefined::Type

  class << self
    def fixed_items
      Type.all.flat_map do |type|
        type.configuration_class.widgets.map do |widget_type|
          { widget_type: widget_type.to_s, work_item_type_id: type.id }
        end
      end
    end
  end
end

auto_generate_ids!를 사용하면 항목의 순서를 변경하면 ID도 변경됩니다. ID가 데이터베이스에 저장되거나 외부에서 참조될 경우에는 사용하지 마세요.

명시적 ID를 할당할 때 여러 팀이 독립적으로 항목을 추가할 수 있는 경우 ID 범위를 예약합니다. 예를 들어, work item 타입은 시스템 정의 타입에 ID 1-9를, 데이터베이스에 저장된 커스텀 타입에 1001+를 사용합니다.

항목 쿼리#

FixedItemsModel은 ActiveRecord와 유사한 쿼리 인터페이스를 제공합니다:

# ID로 찾기 (찾지 못하면 RecordNotFound 발생)
Security::StaticTrainingProvider.find(1)

# 속성으로 찾기 (찾지 못하면 nil 반환)
Security::StaticTrainingProvider.find_by(name: "Kontra")

# 속성으로 필터링 (배열 반환)
Security::StaticTrainingProvider.where(name: "Kontra")

# 모든 항목
Security::StaticTrainingProvider.all

# 순회
Security::StaticTrainingProvider.find_each { |provider| puts provider.name }

where 메서드는 여러 조건과 배열 값을 지원합니다:

WorkItems::TypesFramework::SystemDefined::Type.where(base_type: %w[issue incident])
WorkItems::TypesFramework::SystemDefined::Type.where(base_type: :issue, icon_name: 'issue-type-issue')

체이닝은 지원되지 않습니다. where 메서드는 관계(relation)가 아닌 Array를 반환합니다. 모든 조건을 단일 where 호출에 전달하거나, 정렬, 순서 지정 또는 기타 쿼리 로직을 위해 모델에 클래스 메서드를 추가합니다.

항목은 첫 번째 접근 시 인메모리 캐시에 로드되고 프로세스 수명 동안 재사용됩니다. 동일한 ID로 .find 또는 .find_by를 반복 호출하면 동일한 객체 인스턴스를 반환합니다(값 동등성뿐만 아니라 동일성):

a = WorkItems::TypesFramework::SystemDefined::Type.find(1)
b = WorkItems::TypesFramework::SystemDefined::Type.find(1)
a.equal?(b) # => true, 메모리상 같은 객체

ActiveRecord 모델과의 연관 관계#

ActiveRecord::FixedItemsModel::HasOne을 사용하여 ActiveRecord 모델에서 fixed items 모델로 belongs_to 스타일의 연관 관계를 만들 수 있습니다.

기본 연관 관계#

class CurrentStatus < ApplicationRecord
  include ActiveRecord::FixedItemsModel::HasOne

  belongs_to_fixed_items :system_defined_status,
    fixed_items_class: WorkItems::Statuses::SystemDefined::Status,
    foreign_key: 'system_defined_status_identifier'
end

연관 관계는 getter, setter, 쿼리 메서드를 제공합니다:

status = CurrentStatus.last
status.system_defined_status                   # fixed items 모델 인스턴스 반환
status.system_defined_status = Status.find(2)  # 객체를 통해 설정
status.system_defined_status_identifier = 2    # 칼럼을 통해 설정
status.system_defined_status?                  # 존재하면 true 반환

칼럼 이름: _id 대신 _identifier 사용#

데이터베이스 칼럼 이름을 <association>_id 대신 <association>_identifier로 지정합니다. PostgreSQL에서 _id 칼럼은 관례적으로 데이터베이스 수준의 무결성 제약이 있는 외래 키를 의미합니다. Fixed items 모델은 메모리에 존재하므로 데이터베이스는 이러한 칼럼에 대한 참조 무결성을 강제할 수 없습니다. _identifier를 사용하면 이 차이가 명확해집니다.

_identifier 칼럼 이름을 사용하려면 항상 belongs_to_fixed_itemsforeign_key:를 전달합니다:

class CustomType < ApplicationRecord
  include ActiveRecord::FixedItemsModel::HasOne

  belongs_to_fixed_items :converted_from_type,
    fixed_items_class: WorkItems::TypesFramework::SystemDefined::Type,
    foreign_key: 'converted_from_system_defined_type_identifier'
end

캐싱 동작#

연관 관계는 resolved된 객체를 캐시하며 외래 키가 변경될 때 자동으로 무효화됩니다. 캐시는 ActiveRecord 모델에서 reset을 호출할 때도 지워집니다.

GlobalID 지원#

GlobalID에 의존하는 GraphQL 또는 다른 시스템에서 fixed items 모델을 사용하려면 GlobalID::Identification을 포함합니다:

class Type
  include ActiveRecord::FixedItemsModel::Model
  include GlobalID::Identification

  # 선택 사항: GlobalID 모델 이름이 클래스 이름과 달라야 하는 경우 오버라이드
  def to_global_id(_options = {})
    ::Gitlab::GlobalId.build(self, model_name: 'WorkItems::Type', id: id)
  end
  alias_method :to_gid, :to_global_id
end

JSON 직렬화#

인스턴스는 :only, :except, :methods 옵션과 함께 as_jsonto_json을 지원합니다:

provider = Security::StaticTrainingProvider.find(1)

provider.as_json(only: [:id, :name])
# => {"id"=>1, "name"=>"Kontra"}

provider.as_json(except: [:url])
# => {"id"=>1, "name"=>"Kontra", "description"=>"..."}

provider.as_json(methods: [:some_computed_method])

객체 동작#

Fixed items 모델 인스턴스는 영속되고 읽기 전용인 레코드로 동작합니다:

메서드 반환 값
persisted? true
new_record? false
readonly? true
changed? false
destroyed? false

두 인스턴스는 같은 클래스이고 같은 id를 가지면 동등합니다.

오류 처리#

모듈은 두 가지 커스텀 에러 클래스를 정의합니다:

  • ActiveRecord::FixedItemsModel::RecordNotFound — 주어진 ID와 일치하는 항목이 없을 때 .find에 의해 발생합니다.

  • ActiveRecord::FixedItemsModel::UnknownAttribute — 쿼리가 선언되지 않은 속성을 참조할 때 .find_by 또는 .where에 의해 발생합니다.

컨트롤러나 서비스에서 ActiveRecord::RecordNotFound를 처리하는 것과 같은 방식으로 RecordNotFound를 처리합니다.

유효성 검사#

인스턴스는 ActiveModel::Validations를 지원합니다. ActiveModel 클래스와 동일한 방식으로 유효성 검사를 추가합니다:

class WidgetDefinition
  include ActiveRecord::FixedItemsModel::Model

  attribute :widget_type, :string
  attribute :work_item_type_id, :integer

  validates :widget_type, presence: true
  validates :work_item_type_id, presence: true
end

항목은 로드 시 유효성이 검사됩니다. 유효하지 않은 항목 정의는 .all을 처음 호출할 때 에러를 발생시킵니다.

테스트#

팩토리 패턴#

create가 아닌 build를 사용합니다. Fixed items 모델은 메모리에 존재하므로 create 시맨틱(데이터베이스에 영속)은 적용되지 않습니다. 팩토리는 분리된 인스턴스를 구성하는 대신 실제 인메모리 객체를 반환하기 위해 skip_createinitialize_with를 사용합니다:

FactoryBot.define do
  factory :work_item_system_defined_type, class: 'WorkItems::TypesFramework::SystemDefined::Type' do
    skip_create
    issue

    initialize_with do
      WorkItems::TypesFramework::SystemDefined::Type.find(attributes[:id] || 1)
    end

    trait :issue do
      id { 1 }
      base_type { 'issue' }
    end

    trait :incident do
      id { 2 }
      base_type { 'incident' }
    end
  end
end

build(:work_item_system_defined_type, :issue)Type.find(1)과 동일한 객체를 반환합니다. 이는 스펙이 분리된 복사본이 아닌 실제 인메모리 인스턴스에서 작동함을 의미합니다.

스펙 작성#

Fixed items 모델을 ActiveRecord 모델과 동일한 방식으로 테스트합니다: 유효성 검사, 클래스 메서드, 인스턴스 메서드, GlobalID 통합을 검증합니다. 차이점은 영속되지 않은 subject 대신 항상 특정 인메모리 객체와 함께 작업한다는 점입니다:

let(:type) { build(:work_item_system_defined_type) }

it 'has name attribute' do
  expect(type.name).to eq('Issue')
end

팩토리가 없는 모델의 경우 쿼리 메서드를 직접 호출합니다:

let(:provider) { Security::StaticTrainingProvider.find(1) }

기여#

FixedItemsModel 구현은 activerecord-gitlab gem의 일부입니다. 질문이나 변경 사항이 있으면 Slack의 #g_project-management 또는 #s_plan을 통해 Plan stage의 Project Management 그룹에 문의하세요.

주요 파일#

파일 목적
gems/activerecord-gitlab/lib/active_record/fixed_items_model/model.rb 핵심 모듈: 쿼리 인터페이스, 스토리지, 유효성 검사, 직렬화
gems/activerecord-gitlab/lib/active_record/fixed_items_model/has_one.rb 연관 관계 지원: belongs_to_fixed_items, 캐싱
gems/activerecord-gitlab/spec/active_record/fixed_items_model/model_spec.rb 핵심 모듈 스펙
gems/activerecord-gitlab/spec/active_record/fixed_items_model/has_one_spec.rb 연관 관계 지원 스펙

gem 스펙 실행#

gem에는 자체 의존성 집합이 있습니다. gem 디렉터리에서 의존성을 설치하고 스펙을 실행합니다:

cd gems/activerecord-gitlab
bundle install
bundle exec rspec spec/active_record/fixed_items_model/

프로덕션 예시#

모델 도메인 패턴 복잡도
Security::StaticTrainingProvider 보안 트레이닝 ITEMS 상수 단순
WorkItems::Statuses::SystemDefined::Status Work item 상태 ITEMS 상수, 연관 관계 중간
Ai::FoundationalChatAgent AI 에이전트 ITEMS 상수, GlobalID, 커스텀 쿼리 중간
WorkItems::TypesFramework::SystemDefined::Type Work item 타입 .fixed_items, GlobalID, 동적 조건자 복잡
WorkItems::TypesFramework::SystemDefined::WidgetDefinition 위젯 설정 auto_generate_ids!, .fixed_items, 연관 관계 복잡

관련 항목#

Fixed Items 모델

GitLab v19.1
원문 보기
요약

ActiveRecord::FixedItemsModel을 사용하면 데이터베이스 테이블 대신 코드에서 정적인 읽기 전용 데이터를 정의할 수 있습니다. 이 패턴은 다음과 같은 경우에 데이터베이스 기반 룩업 테이블을 대체합니다:

ActiveRecord::FixedItemsModel을 사용하면 데이터베이스 테이블 대신 코드에서 정적인 읽기 전용 데이터를 정의할 수 있습니다. 인스턴스는 ActiveRecord 객체처럼 동작하지만 결정론적이고 버전으로 관리되는 ID와 함께 메모리에 저장됩니다.

이 패턴은 다음과 같은 경우에 데이터베이스 기반 룩업 테이블을 대체합니다:

  • 데이터가 정적이며 코드 배포를 통해서만 변경됩니다.

  • Cells 전반에 걸쳐 전역적으로 일관된 ID가 필요합니다.

  • 룩업 시 데이터베이스 쿼리가 전혀 없어야 합니다.

사용 시기#

다음과 같은 경우에 FixedItemsModel을 사용합니다:

  • 데이터가 코드에 정의되어 있고 배포를 통해서만 변경됩니다.

  • ID가 모든 Cells와 환경에서 동일해야 합니다.

  • 데이터가 런타임에 변경되지 않습니다(사용자가 생성한 레코드 없음).

데이터셋이 작거나 정적으로 정의될 필요는 없습니다. .fixed_items 클래스 메서드를 사용하여 다른 소스로부터 동적으로 항목을 계산할 수 있습니다. 예를 들어, WidgetDefinition은 모든 work item 타입과 해당 위젯 설정을 순회하여 항목을 생성합니다. 항목에 영속성 레이어를 위한 안정적인 ID가 필요하지 않을 경우 auto_generate_ids!를 사용합니다.

다음과 같은 경우에는 FixedItemsModel을 사용하지 않습니다:

  • 사용자나 관리자가 런타임에 레코드를 생성, 수정, 삭제할 수 있습니다.

  • 레코드에 has_many 또는 has_many :through와 같은 데이터베이스 수준의 연관 관계가 필요합니다.

Cells 아키텍처 컨텍스트#

자동 증가 시퀀스가 있는 데이터베이스 테이블은 같은 논리적 엔티티에 대해 서로 다른 Cells에서 서로 다른 ID를 생성합니다. Cell A의 plan_id = 4premium을 의미하는 반면 Cell B의 plan_id = 4gold를 의미할 수 있기 때문에 크로스 셀 참조가 깨질 수 있습니다.

FixedItemsModel은 애플리케이션 코드에 ID를 하드코딩하여 이 문제를 해결합니다. 모든 Cell은 동일한 정의를 로드하고 동일한 ID를 생성합니다. 자세한 내용은 Cells 개발 가이드라인의 정적 데이터 섹션을 참조하세요.

기본 사용법#

ITEMS 상수를 사용한 모델 정의#

가장 간단한 패턴은 인라인으로 항목을 정의합니다:

module Security
  class StaticTrainingProvider
    include ActiveRecord::FixedItemsModel::Model

    ITEMS = [
      { id: 1, name: "Kontra", url: "https://application.security/api/webhook/gitlab/exercises/search" },
      { id: 2, name: "Secure Code Warrior", url: "https://integration-api.securecodewarrior.com/api/v1/trial" },
      { id: 3, name: "SecureFlag", url: "https://knowledge-base-api.secureflag.com/gitlab" }
    ].freeze

    attribute :name, :string
    attribute :url, :string
  end
end

각 항목에는 양의 정수 값을 가진 id 키가 있어야 합니다. ID가 아닌 각 속성은 attribute로 선언합니다. id 속성은 자동으로 선언됩니다.

ITEMS 상수 또는 .fixed_items 클래스 메서드 중 하나만 사용해야 하며, 둘 다 함께 사용할 수 없습니다.

.fixed_items를 사용한 모델 정의#

항목이 다른 소스에서 파생될 때 클래스 메서드를 사용합니다:

module WorkItems
  module TypesFramework
    module SystemDefined
      class Type
        include ActiveRecord::FixedItemsModel::Model

        attribute :name, :string
        attribute :base_type, :string
        attribute :icon_name, :string

        class << self
          def fixed_items
            [
              Definitions::Issue.configuration,
              Definitions::Incident.configuration,
              Definitions::Task.configuration,
              Definitions::Ticket.configuration
            ]
          end
        end
      end
    end
  end
end

각 정의 클래스는 id, name, base_type, icon_name 키가 있는 해시를 반환합니다. 이 패턴은 각 타입의 도메인 로직 가까이에 데이터 정의를 유지합니다.

ID 자동 생성#

항목에 안정적이고 외부에서 참조되는 ID가 필요하지 않을 경우 auto_generate_ids!를 사용합니다. ID는 배열 순서에 따라 1부터 순차적으로 할당됩니다. 이는 ActiveRecord와 유사한 쿼리 인터페이스(find_by, where, all)가 필요하지만 객체가 내부용이고 해당 ID가 절대 영속되거나 API를 통해 노출되지 않을 때 유용합니다.

WidgetDefinition이 좋은 예입니다. 모든 work item 타입과 해당 위젯 설정으로부터 동적으로 항목을 생성합니다. ID는 임시 핸들로, 중요한 것은 widget_typework_item_type_id의 조합입니다:

class WidgetDefinition
  include ActiveRecord::FixedItemsModel::Model
  include ActiveRecord::FixedItemsModel::HasOne

  auto_generate_ids!

  attribute :widget_type, :string
  attribute :work_item_type_id, :integer

  belongs_to_fixed_items :work_item_type,
    fixed_items_class: WorkItems::TypesFramework::SystemDefined::Type

  class << self
    def fixed_items
      Type.all.flat_map do |type|
        type.configuration_class.widgets.map do |widget_type|
          { widget_type: widget_type.to_s, work_item_type_id: type.id }
        end
      end
    end
  end
end

auto_generate_ids!를 사용하면 항목의 순서를 변경하면 ID도 변경됩니다. ID가 데이터베이스에 저장되거나 외부에서 참조될 경우에는 사용하지 마세요.

명시적 ID를 할당할 때 여러 팀이 독립적으로 항목을 추가할 수 있는 경우 ID 범위를 예약합니다. 예를 들어, work item 타입은 시스템 정의 타입에 ID 1-9를, 데이터베이스에 저장된 커스텀 타입에 1001+를 사용합니다.

항목 쿼리#

FixedItemsModel은 ActiveRecord와 유사한 쿼리 인터페이스를 제공합니다:

# ID로 찾기 (찾지 못하면 RecordNotFound 발생)
Security::StaticTrainingProvider.find(1)

# 속성으로 찾기 (찾지 못하면 nil 반환)
Security::StaticTrainingProvider.find_by(name: "Kontra")

# 속성으로 필터링 (배열 반환)
Security::StaticTrainingProvider.where(name: "Kontra")

# 모든 항목
Security::StaticTrainingProvider.all

# 순회
Security::StaticTrainingProvider.find_each { |provider| puts provider.name }

where 메서드는 여러 조건과 배열 값을 지원합니다:

WorkItems::TypesFramework::SystemDefined::Type.where(base_type: %w[issue incident])
WorkItems::TypesFramework::SystemDefined::Type.where(base_type: :issue, icon_name: 'issue-type-issue')

체이닝은 지원되지 않습니다. where 메서드는 관계(relation)가 아닌 Array를 반환합니다. 모든 조건을 단일 where 호출에 전달하거나, 정렬, 순서 지정 또는 기타 쿼리 로직을 위해 모델에 클래스 메서드를 추가합니다.

항목은 첫 번째 접근 시 인메모리 캐시에 로드되고 프로세스 수명 동안 재사용됩니다. 동일한 ID로 .find 또는 .find_by를 반복 호출하면 동일한 객체 인스턴스를 반환합니다(값 동등성뿐만 아니라 동일성):

a = WorkItems::TypesFramework::SystemDefined::Type.find(1)
b = WorkItems::TypesFramework::SystemDefined::Type.find(1)
a.equal?(b) # => true, 메모리상 같은 객체

ActiveRecord 모델과의 연관 관계#

ActiveRecord::FixedItemsModel::HasOne을 사용하여 ActiveRecord 모델에서 fixed items 모델로 belongs_to 스타일의 연관 관계를 만들 수 있습니다.

기본 연관 관계#

class CurrentStatus < ApplicationRecord
  include ActiveRecord::FixedItemsModel::HasOne

  belongs_to_fixed_items :system_defined_status,
    fixed_items_class: WorkItems::Statuses::SystemDefined::Status,
    foreign_key: 'system_defined_status_identifier'
end

연관 관계는 getter, setter, 쿼리 메서드를 제공합니다:

status = CurrentStatus.last
status.system_defined_status                   # fixed items 모델 인스턴스 반환
status.system_defined_status = Status.find(2)  # 객체를 통해 설정
status.system_defined_status_identifier = 2    # 칼럼을 통해 설정
status.system_defined_status?                  # 존재하면 true 반환

칼럼 이름: _id 대신 _identifier 사용#

데이터베이스 칼럼 이름을 <association>_id 대신 <association>_identifier로 지정합니다. PostgreSQL에서 _id 칼럼은 관례적으로 데이터베이스 수준의 무결성 제약이 있는 외래 키를 의미합니다. Fixed items 모델은 메모리에 존재하므로 데이터베이스는 이러한 칼럼에 대한 참조 무결성을 강제할 수 없습니다. _identifier를 사용하면 이 차이가 명확해집니다.

_identifier 칼럼 이름을 사용하려면 항상 belongs_to_fixed_itemsforeign_key:를 전달합니다:

class CustomType < ApplicationRecord
  include ActiveRecord::FixedItemsModel::HasOne

  belongs_to_fixed_items :converted_from_type,
    fixed_items_class: WorkItems::TypesFramework::SystemDefined::Type,
    foreign_key: 'converted_from_system_defined_type_identifier'
end

캐싱 동작#

연관 관계는 resolved된 객체를 캐시하며 외래 키가 변경될 때 자동으로 무효화됩니다. 캐시는 ActiveRecord 모델에서 reset을 호출할 때도 지워집니다.

GlobalID 지원#

GlobalID에 의존하는 GraphQL 또는 다른 시스템에서 fixed items 모델을 사용하려면 GlobalID::Identification을 포함합니다:

class Type
  include ActiveRecord::FixedItemsModel::Model
  include GlobalID::Identification

  # 선택 사항: GlobalID 모델 이름이 클래스 이름과 달라야 하는 경우 오버라이드
  def to_global_id(_options = {})
    ::Gitlab::GlobalId.build(self, model_name: 'WorkItems::Type', id: id)
  end
  alias_method :to_gid, :to_global_id
end

JSON 직렬화#

인스턴스는 :only, :except, :methods 옵션과 함께 as_jsonto_json을 지원합니다:

provider = Security::StaticTrainingProvider.find(1)

provider.as_json(only: [:id, :name])
# => {"id"=>1, "name"=>"Kontra"}

provider.as_json(except: [:url])
# => {"id"=>1, "name"=>"Kontra", "description"=>"..."}

provider.as_json(methods: [:some_computed_method])

객체 동작#

Fixed items 모델 인스턴스는 영속되고 읽기 전용인 레코드로 동작합니다:

메서드 반환 값
persisted? true
new_record? false
readonly? true
changed? false
destroyed? false

두 인스턴스는 같은 클래스이고 같은 id를 가지면 동등합니다.

오류 처리#

모듈은 두 가지 커스텀 에러 클래스를 정의합니다:

  • ActiveRecord::FixedItemsModel::RecordNotFound — 주어진 ID와 일치하는 항목이 없을 때 .find에 의해 발생합니다.

  • ActiveRecord::FixedItemsModel::UnknownAttribute — 쿼리가 선언되지 않은 속성을 참조할 때 .find_by 또는 .where에 의해 발생합니다.

컨트롤러나 서비스에서 ActiveRecord::RecordNotFound를 처리하는 것과 같은 방식으로 RecordNotFound를 처리합니다.

유효성 검사#

인스턴스는 ActiveModel::Validations를 지원합니다. ActiveModel 클래스와 동일한 방식으로 유효성 검사를 추가합니다:

class WidgetDefinition
  include ActiveRecord::FixedItemsModel::Model

  attribute :widget_type, :string
  attribute :work_item_type_id, :integer

  validates :widget_type, presence: true
  validates :work_item_type_id, presence: true
end

항목은 로드 시 유효성이 검사됩니다. 유효하지 않은 항목 정의는 .all을 처음 호출할 때 에러를 발생시킵니다.

테스트#

팩토리 패턴#

create가 아닌 build를 사용합니다. Fixed items 모델은 메모리에 존재하므로 create 시맨틱(데이터베이스에 영속)은 적용되지 않습니다. 팩토리는 분리된 인스턴스를 구성하는 대신 실제 인메모리 객체를 반환하기 위해 skip_createinitialize_with를 사용합니다:

FactoryBot.define do
  factory :work_item_system_defined_type, class: 'WorkItems::TypesFramework::SystemDefined::Type' do
    skip_create
    issue

    initialize_with do
      WorkItems::TypesFramework::SystemDefined::Type.find(attributes[:id] || 1)
    end

    trait :issue do
      id { 1 }
      base_type { 'issue' }
    end

    trait :incident do
      id { 2 }
      base_type { 'incident' }
    end
  end
end

build(:work_item_system_defined_type, :issue)Type.find(1)과 동일한 객체를 반환합니다. 이는 스펙이 분리된 복사본이 아닌 실제 인메모리 인스턴스에서 작동함을 의미합니다.

스펙 작성#

Fixed items 모델을 ActiveRecord 모델과 동일한 방식으로 테스트합니다: 유효성 검사, 클래스 메서드, 인스턴스 메서드, GlobalID 통합을 검증합니다. 차이점은 영속되지 않은 subject 대신 항상 특정 인메모리 객체와 함께 작업한다는 점입니다:

let(:type) { build(:work_item_system_defined_type) }

it 'has name attribute' do
  expect(type.name).to eq('Issue')
end

팩토리가 없는 모델의 경우 쿼리 메서드를 직접 호출합니다:

let(:provider) { Security::StaticTrainingProvider.find(1) }

기여#

FixedItemsModel 구현은 activerecord-gitlab gem의 일부입니다. 질문이나 변경 사항이 있으면 Slack의 #g_project-management 또는 #s_plan을 통해 Plan stage의 Project Management 그룹에 문의하세요.

주요 파일#

파일 목적
gems/activerecord-gitlab/lib/active_record/fixed_items_model/model.rb 핵심 모듈: 쿼리 인터페이스, 스토리지, 유효성 검사, 직렬화
gems/activerecord-gitlab/lib/active_record/fixed_items_model/has_one.rb 연관 관계 지원: belongs_to_fixed_items, 캐싱
gems/activerecord-gitlab/spec/active_record/fixed_items_model/model_spec.rb 핵심 모듈 스펙
gems/activerecord-gitlab/spec/active_record/fixed_items_model/has_one_spec.rb 연관 관계 지원 스펙

gem 스펙 실행#

gem에는 자체 의존성 집합이 있습니다. gem 디렉터리에서 의존성을 설치하고 스펙을 실행합니다:

cd gems/activerecord-gitlab
bundle install
bundle exec rspec spec/active_record/fixed_items_model/

프로덕션 예시#

모델 도메인 패턴 복잡도
Security::StaticTrainingProvider 보안 트레이닝 ITEMS 상수 단순
WorkItems::Statuses::SystemDefined::Status Work item 상태 ITEMS 상수, 연관 관계 중간
Ai::FoundationalChatAgent AI 에이전트 ITEMS 상수, GlobalID, 커스텀 쿼리 중간
WorkItems::TypesFramework::SystemDefined::Type Work item 타입 .fixed_items, GlobalID, 동적 조건자 복잡
WorkItems::TypesFramework::SystemDefined::WidgetDefinition 위젯 설정 auto_generate_ids!, .fixed_items, 연관 관계 복잡

관련 항목#