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
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, 연관 관계 |
복잡 |