키셋 페이지네이션
키셋 페이지네이션 라이브러리는 HAML 기반 뷰와 GitLab 프로젝트 내의 REST API에서 사용할 수 있습니다.
키셋 페이지네이션에 대해 읽고, 오프셋 기반 페이지네이션과의 차이를 우리 페이지네이션 가이드라인 페이지에서 확인할 수 있습니다.
API 개요
개요
Rails 컨트롤러의 ActiveRecord
를 이용한 키셋 페이지네이션:
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 쿼리에서 추가 구성 없이 작동합니다:
- 하나의 열로 정렬.
- 마지막 열이 기본 키인 두 개의 열로 정렬.
이 라이브러리는 nullable 및 비중복 열을 감지하고, 이러한 기반으로 기본 키를 사용하여 추가 정렬을 추가합니다.
이는 키셋 페이지네이션이 값에 대해 명확한 정렬을 기대하기 때문에 필요합니다:
Project.order(:created_at).keyset_paginate.records # ORDER BY created_at, id
Project.order(:name).keyset_paginate.records # ORDER BY name, id
Project.order(:created_at, id: :desc).keyset_paginate.records # ORDER BY created_at, id
Project.order(created_at: :asc, id: :desc).keyset_paginate.records # ORDER BY created_at, id DESC
keyset_paginate
메서드는 로드된 레코드와 다양한 페이지 요청을 위한 추가 정보를 포함하는 특별한 페이지네이터 객체를 반환합니다.
이 메서드는 다음과 같은 키워드 인수를 수용합니다:
-
cursor
- 다음 페이지 요청을 위한 인코딩된 정렬 열 값 (nil일 수 있습니다). -
per_page
- 페이지당 로드할 레코드 수 (기본값 20). -
keyset_order_options
- 키셋 페이지네이션 데이터베이스 쿼리를 구축하기 위한 추가 옵션, 성능 섹션에서UNION
쿼리의 예를 참조하세요 (선택 사항).
페이지네이터 객체는 다음 메서드를 포함합니다:
-
records
- 현재 페이지의 레코드를 반환합니다. -
has_next_page?
- 다음 페이지가 있는지 알려줍니다. -
has_previous_page?
- 이전 페이지가 있는지 알려줍니다. -
cursor_for_next_page
- 다음 페이지 요청을 위한 인코딩된 값String
(nil일 수 있습니다). -
cursor_for_previous_page
- 이전 페이지 요청을 위한 인코딩된 값String
(nil일 수 있습니다). -
cursor_for_first_page
- 첫 번째 페이지 요청을 위한 인코딩된 값String
. -
cursor_for_last_page
- 마지막 페이지 요청을 위한 인코딩된 값String
. - 페이지네이터 객체는
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 # 다음 페이지 로드
키셋 페이지네이션은 페이지 번호를 지원하지 않기 때문에 다음 페이지로 이동하는 것이 제한됩니다:
- 다음 페이지
- 이전 페이지
- 마지막 페이지
- 첫 페이지
REST API에서 paginate_with_strategies
의 사용
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 뷰에서 Rails의 사용
이제 프로젝트를 이름으로 정렬하여 나열하는 다음 컨트롤러 작업을 고려해 보겠습니다:
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
절에서 사용할 경우, 생성된 데이터베이스 쿼리를 확인하고 올바른 인덱스 구성이 사용되고 있는지 확인하는 것이 좋습니다. 더 많은 정보는 페이지네이션 가이드라인 페이지에서 찾을 수 있습니다.
참고: 첫 페이지의 쿼리 성능이 좋게 보일 수 있지만, 두 번째 페이지(커서 속성이 쿼리에서 사용되는 경우)의 성능은 나쁠 수 있습니다. 항상 첫 페이지와 두 번째 페이지의 두 쿼리 성능을 확인하는 것이 좋습니다.
타이 브레이커(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 노드가 아니기 때문에 오류를 발생시킵니다. 키셋 라이브러리는 이러한 종류의 쿼리에서 구성 값을 자동으로 유추할 수 없습니다.
키셋 페이지 매김이 작동하려면 사용자 정의 주문 객체를 구성해야 하며, 이를 위해 주문 열에 대한 정보를 수집해야 합니다:
-
relative_position
은 고유 인덱스가 없기 때문에 중복된 값을 가질 수 있습니다. -
relative_position
은 열에 NULL이 아닌 제약 조건이 없기 때문에 NULL 값을 가질 수 있습니다. 이를 위해 결과 집합의 시작 또는 끝에서NULL
값을 어디에서 볼 수 있는지 결정해야 합니다(NULLS LAST
). - 키셋 페이지 매김은 고유한 정렬 열을 요구하므로, 정렬을 고유하게 만들기 위해 기본 키(
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
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
order_expression: Issue.arel_table[:id].asc,
nullable: :not_nullable
)
])
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,
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
절에서 열 표현을 노출하도록 지시합니다. 이는 키셋 페이지 매김이 다음 페이지를 요청하기 위해 레코드에서 마지막 값을 추출해야 하기 때문에 필요합니다.
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
)
])
scope = Issue.where(project_id: 10).order(order)
scope.keyset_paginate.records # 작동합니다