키셋 페이징
키셋 페이징 라이브러리는 HAML 기반 뷰 및 GitLab 프로젝트 내 REST API에서 사용할 수 있습니다.
keyset pagination guidelines 페이지에서 키셋 페이징 및 오프셋 기반 페이징을 비교하는 방법에 대해 자세히 알아볼 수 있습니다.
API 개요
개요
ActiveRecord
를 사용한 Rails 컨트롤러에서의 키셋 페이징:
cursor = params[:cursor] # 첫 번째 페이지를 요청할 때는 'nil'입니다.
paginator = Project.order(:created_at).keyset_paginate(cursor: cursor, per_page: 20)
paginator.each do |project|
puts project.name # 최대 20개의 프로젝트 출력
end
사용법
이 라이브러리는 단일 메서드를 ActiveRecord 관계에 추가합니다: #keyset_paginate
.
이는 Kaminari의 paginate
메서드와 유사하지만 구현은 다릅니다.
키셋 페이징은 다음 간단한 ActiveRecord 쿼리에 대해 추가 구성 없이 작동합니다:
- 하나의 열을 기준으로 정렬합니다.
- 마지막 열이 기본 키인 두 개의 열을 기준으로 정렬합니다.
이 라이브러리는 널 가능한 및 중복되지 않는 열을 감지하고 이를 기반으로 주요 키를 사용하여 추가적인 순서 지정을 추가합니다. 이는 키셋 페이징이 대체 순서정이 필요하기 때문에 필요합니다. 예를 들어:
Project.order(:created_at).keyset_paginate.records # created_at, id로 정렬됨
Project.order(:name).keyset_paginate.records # name, id로 정렬됨
Project.order(:created_at, id: :desc).keyset_paginate.records # created_at, id로 정렬됨
Project.order(created_at: :asc, id: :desc).keyset_paginate.records # created_at, id로 정렬됨(내림차순)
keyset_paginate
메서드는 특별한 페이징 객체를 반환하며 로드된 레코드 및 다양한 페이지를 요청하기 위한 추가 정보를 포함합니다.
이 메서드는 다음 키워드 인수를 허용합니다:
-
cursor
- 다음 페이지를 요청하기 위한 인코딩된 순서대로 된 열 값(일 수 있음). -
per_page
- 페이지당 로드할 레코드 수(기본값은 20). -
keyset_order_options
- 키셋 페이징 데이터베이스 쿼리를 작성하기 위한 추가 옵션, 성능 부분의UNION
쿼리에 대한 예제를 참조하세요(옵션).
페이징 객체에는 다음과 같은 메서드가 있습니다:
-
records
- 현재 페이지의 레코드를 반환합니다. -
has_next_page?
- 다음 페이지가 있는지 여부를 알려줍니다. -
has_previous_page?
- 이전 페이지가 있는지 여부를 알려줍니다. -
cursor_for_next_page
- 다음 페이지를 요청하기 위한 인코딩된 값(일 수 있음). -
cursor_for_previous_page
- 이전 페이지를 요청하기 위한 인코딩된 값(일 수 있음). -
cursor_for_first_page
- 첫 번째 페이지를 요청하기 위한 인코딩된 값. -
cursor_for_last_page
- 마지막 페이지를 요청하기 위한 인코딩된 값. - 페이징 객체에는
Enumerable
모듈이 포함되어 있으며records
메서드/배열에 대한 열거 기능을 위임합니다.
첫 번째 및 두 번째 페이지를 가져오는 예제:
paginator = Project.order(:name).keyset_paginate
paginator.to_a # .records와 동일
cursor = paginator.cursor_for_next_page # 다음 페이지를 위한 인코딩된 열 속성
paginator = Project.order(:name).keyset_paginate(cursor: cursor).records # 다음 페이지 로드
페이지 번호를 지원하지 않기 때문에 다음과 같은 페이지로 이동이 제한됩니다:
- 다음 페이지
- 이전 페이지
- 마지막 페이지
- 첫 번째 페이지
paginate_with_strategies
를 사용한 REST API에서의 사용법
REST API에서는 paginate_with_strategies
도우미를 사용하여 관계에 대해 키셋 페이징 또는 오프셋 페이징을 사용할 수 있습니다.
desc '프로젝트와 관련된 사항 가져오기' do
detail '이 기능은 GitLab 16.1에서 소개되었습니다.'
success code: 200, model: ::API::Entities::Thing
failure [
{ code: 401, message: '인증되지 않음' },
{ code: 403, message: '금지됨' },
{ code: 404, message: '찾을 수 없음' }
]
end
params do
use :pagination
requires :project_id, type: Integer, desc: '프로젝트 ID'
optional :cursor, type: String, desc: '레코드의 다음 세트를 얻기 위한 커서'
optional :order_by, type: String, values: %w[id name], default: 'id',
desc: '정렬 기준'
optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: '정렬 순서'
end
route_setting :authentication
get ':project_id/things' do
project = Project.find_by_id(params[:project_id])
not_found! if project.blank?
things = project.things
present paginate_with_strategies(things), with: ::API::Entities::Thing
end
키셋 페이징을 사용하려면 다음 조건을 충족해야 합니다:
-
params[:pagination]
은'keyset'
을 반환해야 합니다. -
params[:order_by]
및params[:sort]
는 모델의supported_keyset_orderings
클래스 메서드에서 반환된 객체에 모두 나타나야 합니다. 다음 예에서Thing
은 ID를 기준으로 오름차순 또는 내림차순으로 정렬할 때 키셋 페이징을 지원합니다.class Thing < ApplicationRecord def self.supported_keyset_orderings { id: [:asc, :desc] } end end
HAML 뷰에서의 사용법
다음 컨트롤러 액션을 고려해보겠습니다. 여기서는 이름별로 정렬된 프로젝트를 나열합니다:
def index
@projects = Project.order(:name).keyset_paginate(cursor: params[:cursor])
end
HAML 파일에서 레코드를 렌더링할 수 있습니다:
- if @projects.any?
- @projects.each do |project|
.project-container
= project.name
= keyset_paginate @projects
성능
키셋 페이징의 성능은 데이터베이스 인덱스 구성 및 ORDER BY
절에서 사용하는 열 수에 따라 달라집니다.
기본 키(id
)로 정렬하는 경우 생성된 쿼리는 데이터베이스 인덱스에 의해 효율적입니다.
ORDER BY
절에 두 개 이상의 열을 사용하는 경우 생성된 데이터베이스 쿼리를 확인하고 올바른 인덱스 구성이 사용되었는지 확인하는 것이 좋습니다. 더 많은 정보는 pagination guideline page에서 확인할 수 있습니다.
키가 분리되었을 때의 예제 데이터베이스 쿼리(id
열이 추가된 경우):
SELECT "issues".*
FROM "issues"
WHERE (("issues"."id" > 99
AND "issues"."created_at" = '2021-02-16 11:26:17.408466')
OR ("issues"."created_at" > '2021-02-16 11:26:17.408466')
OR ("issues"."created_at" IS NULL))
ORDER BY "issues"."created_at" DESC NULLS LAST, "issues"."id" DESC
LIMIT 20
OR
쿼리는 PostgreSQL에서 최적화하기 어려우며, 일반적으로 UNION
쿼리를 사용하는 것이 좋습니다. 키셋 페이징 라이브러리는 ORDER BY
절에 여러 열이 포함된 경우 효율적인 UNION
을 생성할 수 있습니다. 이는 Relation#keyset_paginate
에 전달된 옵션에서 use_union_optimization: true
옵션을 지정했을 때 트리거됩니다.
예제:
# 첫 번째 페이지에 대한 간단한 쿼리를 트리거합니다.
paginator1 = Project.order(:created_at, id: :desc).keyset_paginate(per_page: 2, keyset_order_options: { use_union_optimization: true })
cursor = paginator1.cursor_for_next_page
# 두 번째 페이지에 대해 UNION 쿼리를 트리거합니다.
paginator2 = Project.order(:created_at, id: :desc).keyset_paginate(per_page: 2, cursor: cursor, keyset_order_options: { use_union_optimization: true })
puts paginator2.records.to_a # UNION 쿼리
복잡한 주문 구성
일반적인 ORDER BY
구성은 keyset_paginate
메서드에서 자동으로 처리되므로 매뉴얼 구성이 필요하지 않습니다. 주문 객체 구성이 필요한 몇 가지 특수한 경우가 있습니다.
-
NULLS LAST
순서 지정. - 함수 기반 순서 지정.
-
iid
와 같은 사용자 정의 터미네이션 열을 사용한 순서 지정.
이러한 주문 객체들은 모델 클래스에서 표준 ActiveRecord 스코프로 정의될 수 있으며, 이러한 스코프를 다른 곳(Kaminari, 백그라운드 작업)에서 사용하는 데에 아무런 제약이 없습니다.
NULLS LAST
순서 지정
다음 스코프를 고려해보십시오.
scope = Issue.where(project_id: 10).order(Issue.arel_table[:relative_position].desc.nulls_last)
# SELECT "issues".* FROM "issues" WHERE "issues"."project_id" = 10 ORDER BY relative_position DESC NULLS LAST
scope.keyset_paginate # raises: Gitlab::Pagination::Keyset::UnsupportedScopeOrder: The order on the scope does not support keyset pagination
keyset_paginate
메서드는 쿼리의 주문 값이 사용자 정의 SQL 문자열이 아니라 Arel
AST 노드가 아니기 때문에 오류가 발생합니다. Keyset 라이브러리는 이러한 유형의 쿼리에서 구성 값을 자동으로 유추할 수 없습니다.
Keyset 페이지네이션을 작동시키려면 구성된 사용자 정의 주문 객체가 필요하며, 이를 위해 주문 열에 대한 정보를 수집해야 합니다.
-
relative_position
은 고유한 인덱스가 없기 때문에 중복된 값이 있을 수 있습니다. -
relative_position
에는NULL
값이 있을 수 있으며, 이 열에 NOT NULL 제약 조건이 없습니다. 이를 위해NULL
값을 시작 부분이나 끝 부분(NULLS LAST
)에 위치시키는지를 결정해야 합니다. - Keyset 페이지네이션에는 고유한 순서 열이 필요하므로, 주문을 고유하게 만들기 위해 기본 키(
id
)를 추가해야 합니다. - 마지막 페이지로 이동하고 뒤로 페이징하는 것은 실제로
ORDER BY
절을 반전시킵니다. 이를 위해 반전된ORDER BY
절을 제공해야 합니다.
예시:
order = Gitlab::Pagination::Keyset::Order.build([
# 이러한 속성들은 `lib/gitlab/pagination/keyset/column_order_definition.rb` 파일에 문서화되어 있습니다.
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'relative_position',
column_expression: Issue.arel_table[:relative_position],
order_expression: Issue.arel_table[:relative_position].desc.nulls_last,
reversed_order_expression: Issue.arel_table[:relative_position].asc.nulls_first,
nullable: :nulls_last,
order_direction: :desc,
distinct: false
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
order_expression: Issue.arel_table[:id].asc,
nullable: :not_nullable,
distinct: true
)
])
scope = Issue.where(project_id: 10).order(order) # 또는 reorder()
scope.keyset_paginate.records # 작동
함수 기반 순서 지정
다음 예시에서는 id
를 10배하여 그 값으로 정렬합니다. id
열이 고유하기 때문에 한 열만 정의합니다.
order = Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id_times_ten',
order_expression: Arel.sql('id * 10').asc,
nullable: :not_nullable,
order_direction: :asc,
distinct: true,
add_to_projections: true
)
])
paginator = Issue.where(project_id: 10).order(order).keyset_paginate(per_page: 5)
puts paginator.records.map(&:id_times_ten)
cursor = paginator.cursor_for_next_page
paginator = Issue.where(project_id: 10).order(order).keyset_paginate(cursor: cursor, per_page: 5)
puts paginator.records.map(&:id_times_ten)
add_to_projections
플래그는 페이징기에게 SELECT
절에서 열 표현을 노출하도록 알려줍니다. 이것은 keyset 페이지네이션이 레코드에서 마지막 값을 추출하여 다음 페이지를 요청해야 하기 때문에 필요합니다.
iid
기반 순서 지정
이슈를 정렬할 때, 데이터베이스는 프로젝트 내에서 고유한 iid
값을 보장합니다. project_id
필터가 존재한다면 한 열로 정렬하는 것은 페이징을 작동시키는 데에 충분합니다.
order = Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'iid',
order_expression: Issue.arel_table[:iid].asc,
nullable: :not_nullable,
distinct: true
)
])
scope = Issue.where(project_id: 10).order(order)
scope.keyset_paginate.records # 작동