InfoGrab DocsInfoGrab Docs

Fixed Items 모델

요약

데이터베이스 테이블 대신 코드에 정적인 읽기 전용 데이터를 정의할 때는 ActiveRecord::FixedItemsModel을 사용합니다. 이 패턴은 다음 조건에 해당하는 데이터베이스 기반 조회 테이블을 대체합니다. 다음 경우에 FixedItemsModel을 사용합니다.

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

이 패턴은 다음 조건에 해당하는 데이터베이스 기반 조회 테이블을 대체합니다.

  • 데이터가 정적이고 코드 배포로만 변경됩니다.
  • Cells 전반에서 전역적으로 일관된 ID가 필요합니다.
  • 조회 시 데이터베이스 쿼리가 전혀 없어도 무방합니다.

사용 시기#

다음 경우에 FixedItemsModel을 사용합니다.

  • 데이터가 코드에 정의되어 있고 배포로만 변경됩니다.
  • 모든 Cells와 환경에서 ID가 동일해야 합니다.
  • 데이터가 런타임에 변경되지 않습니다(사용자가 만든 레코드가 없음).

데이터 집합이 작거나 정적으로 정의되어야 하는 것은 아닙니다. 다른 소스에서 항목을 동적으로 계산하려면 .fixed_items 클래스 메서드를 사용합니다. 예를 들어 WidgetDefinition은 모든 작업 항목 유형과 그 위젯 설정을 순회해 항목을 생성합니다. 영속성 계층에서 항목에 안정적인 ID가 필요하지 않다면 auto_generate_ids!를 사용합니다.

다음 경우에는 FixedItemsModel을 사용하지 않습니다.

  • 사용자나 관리자가 런타임에 레코드를 생성, 수정, 삭제할 수 있습니다.
  • 레코드에 has_many 나 has_many :through 같은 데이터베이스 수준의 연관 관계가 필요합니다.

Cells 아키텍처 컨텍스트#

자동 증가 시퀀스를 사용하는 데이터베이스 테이블은 같은 논리적 엔터티에 대해 Cell 마다 다른 ID를 생성합니다. Cell A의 plan_id = 4는 premium을 뜻하고 Cell B의 plan_id = 4는 gold를 뜻할 수 있으므로 Cell 간 참조가 깨집니다.

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 이 좋은 예입니다. 모든 작업 항목 유형과 그 위젯 설정에서 항목을 동적으로 생성합니다. ID는 일회용 핸들이며, 중요한 것은 widget_type과 work_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
Note

auto_generate_ids!를 사용하면 항목의 순서를 바꿀 때 ID도 바뀝니다. ID를 데이터베이스에 저장하거나 외부에서 참조한다면 사용하지 않습니다.

ID를 명시적으로 할당할 때 여러 팀이 독립적으로 항목을 추가할 수 있다면 ID 범위를 예약합니다. 예를 들어 작업 항목 유형은 시스템 정의 유형에 1-9를, 데이터베이스에 저장되는 커스텀 유형에 1001 이상을 사용합니다.

항목 쿼리#

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

# Find by ID (raises RecordNotFound if not found)
Security::StaticTrainingProvider.find(1)

# Find by attributes (returns nil if not found)
Security::StaticTrainingProvider.find_by(name: "Kontra")

# Filter by attributes (returns an array)
Security::StaticTrainingProvider.where(name: "Kontra")

# All items
Security::StaticTrainingProvider.all

# Iterate
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 메서드는 릴레이션이 아니라 Array를 반환합니다. 모든 조건을 하나의 where 호출에 전달하거나, 정렬이나 순서 지정 등 다른 쿼리 로직은 모델에 클래스 메서드로 추가합니다.

항목은 처음 접근할 때 메모리 내 캐시에 로드되어 프로세스가 살아 있는 동안 재사용됩니다. 같은 ID로 .find 나 .find_by를 반복 호출하면 같은 객체 인스턴스를 반환합니다. 값 동등성뿐 아니라 동일성까지 같습니다.

a = WorkItems::TypesFramework::SystemDefined::Type.find(1)
b = WorkItems::TypesFramework::SystemDefined::Type.find(1)
a.equal?(b) # => true, same object in memory

ActiveRecord 모델과의 연관 관계#

ActiveRecord 모델에서 fixed items 모델로 belongs_to 형태의 연관 관계를 만들려면 ActiveRecord::FixedItemsModel::HasOne을 사용합니다.

기본 연관 관계#

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

이 연관 관계는 게터, 세터, 쿼리 메서드를 제공합니다.

status = CurrentStatus.last
status.system_defined_status                   # Returns the fixed items model instance
status.system_defined_status = Status.find(2)  # Sets via object
status.system_defined_status_identifier = 2    # Sets via column
status.system_defined_status?                  # Returns true if present

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

데이터베이스 칼럼 이름은 <association>_id가 아니라 <association>_identifier로 짓습니다. PostgreSQL에서 _id 칼럼은 관례상 데이터베이스 수준의 무결성 제약이 있는 외래 키를 뜻합니다. fixed items 모델은 메모리에 존재하므로 데이터베이스가 이러한 칼럼에 참조 무결성을 강제할 수 없습니다. _identifier를 사용하면 이 구분이 분명해집니다.

_identifier 칼럼 이름을 사용하려면 belongs_to_fixed_items에 항상 foreign_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

캐싱 동작#

연관 관계는 확인된 객체를 캐시하며 외래 키가 바뀌면 자동으로 캐시를 무효화합니다. ActiveRecord 모델에서 reset을 호출할 때도 캐시가 비워집니다.

GlobalID 지원#

GraphQL 이나 GlobalID에 의존하는 다른 시스템과 fixed items 모델을 함께 사용하려면 GlobalID::Identification을 include 합니다.

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

  # Optional: Override if the GlobalID model name must differ from the class name
  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_json 및 to_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_create와 initialize_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의 일부입니다. 질문이나 변경 사항은 Plan Stage의 Project Management 그룹에 Slack의 #g_project-management 또는 #s_plan으로 문의합니다.

주요 파일#

파일 목적
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 작업 항목 상태 ITEMS 상수, 연관 관계 중간
Ai::FoundationalChatAgent AI 에이전트 ITEMS 상수, GlobalID, 커스텀 쿼리 중간
WorkItems::TypesFramework::SystemDefined::Type 작업 항목 유형 .fixed_items, GlobalID, 동적 술어 복잡
WorkItems::TypesFramework::SystemDefined::WidgetDefinition 위젯 설정 auto_generate_ids!, .fixed_items, 연관 관계 복잡

관련 주제#

Fixed Items 모델

GitLab v19.4
원문 보기

요약

데이터베이스 테이블 대신 코드에 정적인 읽기 전용 데이터를 정의할 때는 ActiveRecord::FixedItemsModel을 사용합니다. 이 패턴은 다음 조건에 해당하는 데이터베이스 기반 조회 테이블을 대체합니다. 다음 경우에 FixedItemsModel을 사용합니다.

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

이 패턴은 다음 조건에 해당하는 데이터베이스 기반 조회 테이블을 대체합니다.

  • 데이터가 정적이고 코드 배포로만 변경됩니다.
  • Cells 전반에서 전역적으로 일관된 ID가 필요합니다.
  • 조회 시 데이터베이스 쿼리가 전혀 없어도 무방합니다.

사용 시기#

다음 경우에 FixedItemsModel을 사용합니다.

  • 데이터가 코드에 정의되어 있고 배포로만 변경됩니다.
  • 모든 Cells와 환경에서 ID가 동일해야 합니다.
  • 데이터가 런타임에 변경되지 않습니다(사용자가 만든 레코드가 없음).

데이터 집합이 작거나 정적으로 정의되어야 하는 것은 아닙니다. 다른 소스에서 항목을 동적으로 계산하려면 .fixed_items 클래스 메서드를 사용합니다. 예를 들어 WidgetDefinition은 모든 작업 항목 유형과 그 위젯 설정을 순회해 항목을 생성합니다. 영속성 계층에서 항목에 안정적인 ID가 필요하지 않다면 auto_generate_ids!를 사용합니다.

다음 경우에는 FixedItemsModel을 사용하지 않습니다.

  • 사용자나 관리자가 런타임에 레코드를 생성, 수정, 삭제할 수 있습니다.
  • 레코드에 has_many 나 has_many :through 같은 데이터베이스 수준의 연관 관계가 필요합니다.

Cells 아키텍처 컨텍스트#

자동 증가 시퀀스를 사용하는 데이터베이스 테이블은 같은 논리적 엔터티에 대해 Cell 마다 다른 ID를 생성합니다. Cell A의 plan_id = 4는 premium을 뜻하고 Cell B의 plan_id = 4는 gold를 뜻할 수 있으므로 Cell 간 참조가 깨집니다.

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 이 좋은 예입니다. 모든 작업 항목 유형과 그 위젯 설정에서 항목을 동적으로 생성합니다. ID는 일회용 핸들이며, 중요한 것은 widget_type과 work_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
Note

auto_generate_ids!를 사용하면 항목의 순서를 바꿀 때 ID도 바뀝니다. ID를 데이터베이스에 저장하거나 외부에서 참조한다면 사용하지 않습니다.

ID를 명시적으로 할당할 때 여러 팀이 독립적으로 항목을 추가할 수 있다면 ID 범위를 예약합니다. 예를 들어 작업 항목 유형은 시스템 정의 유형에 1-9를, 데이터베이스에 저장되는 커스텀 유형에 1001 이상을 사용합니다.

항목 쿼리#

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

# Find by ID (raises RecordNotFound if not found)
Security::StaticTrainingProvider.find(1)

# Find by attributes (returns nil if not found)
Security::StaticTrainingProvider.find_by(name: "Kontra")

# Filter by attributes (returns an array)
Security::StaticTrainingProvider.where(name: "Kontra")

# All items
Security::StaticTrainingProvider.all

# Iterate
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 메서드는 릴레이션이 아니라 Array를 반환합니다. 모든 조건을 하나의 where 호출에 전달하거나, 정렬이나 순서 지정 등 다른 쿼리 로직은 모델에 클래스 메서드로 추가합니다.

항목은 처음 접근할 때 메모리 내 캐시에 로드되어 프로세스가 살아 있는 동안 재사용됩니다. 같은 ID로 .find 나 .find_by를 반복 호출하면 같은 객체 인스턴스를 반환합니다. 값 동등성뿐 아니라 동일성까지 같습니다.

a = WorkItems::TypesFramework::SystemDefined::Type.find(1)
b = WorkItems::TypesFramework::SystemDefined::Type.find(1)
a.equal?(b) # => true, same object in memory

ActiveRecord 모델과의 연관 관계#

ActiveRecord 모델에서 fixed items 모델로 belongs_to 형태의 연관 관계를 만들려면 ActiveRecord::FixedItemsModel::HasOne을 사용합니다.

기본 연관 관계#

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

이 연관 관계는 게터, 세터, 쿼리 메서드를 제공합니다.

status = CurrentStatus.last
status.system_defined_status                   # Returns the fixed items model instance
status.system_defined_status = Status.find(2)  # Sets via object
status.system_defined_status_identifier = 2    # Sets via column
status.system_defined_status?                  # Returns true if present

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

데이터베이스 칼럼 이름은 <association>_id가 아니라 <association>_identifier로 짓습니다. PostgreSQL에서 _id 칼럼은 관례상 데이터베이스 수준의 무결성 제약이 있는 외래 키를 뜻합니다. fixed items 모델은 메모리에 존재하므로 데이터베이스가 이러한 칼럼에 참조 무결성을 강제할 수 없습니다. _identifier를 사용하면 이 구분이 분명해집니다.

_identifier 칼럼 이름을 사용하려면 belongs_to_fixed_items에 항상 foreign_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

캐싱 동작#

연관 관계는 확인된 객체를 캐시하며 외래 키가 바뀌면 자동으로 캐시를 무효화합니다. ActiveRecord 모델에서 reset을 호출할 때도 캐시가 비워집니다.

GlobalID 지원#

GraphQL 이나 GlobalID에 의존하는 다른 시스템과 fixed items 모델을 함께 사용하려면 GlobalID::Identification을 include 합니다.

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

  # Optional: Override if the GlobalID model name must differ from the class name
  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_json 및 to_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_create와 initialize_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의 일부입니다. 질문이나 변경 사항은 Plan Stage의 Project Management 그룹에 Slack의 #g_project-management 또는 #s_plan으로 문의합니다.

주요 파일#

파일 목적
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 작업 항목 상태 ITEMS 상수, 연관 관계 중간
Ai::FoundationalChatAgent AI 에이전트 ITEMS 상수, GlobalID, 커스텀 쿼리 중간
WorkItems::TypesFramework::SystemDefined::Type 작업 항목 유형 .fixed_items, GlobalID, 동적 술어 복잡
WorkItems::TypesFramework::SystemDefined::WidgetDefinition 위젯 설정 auto_generate_ids!, .fixed_items, 연관 관계 복잡

관련 주제#