키셋 페이징
키셋 페이징 라이브러리는 HAML 기반 뷰와 GitLab 프로젝트 내 REST API에서 사용할 수 있습니다.
keyset pagination guidelines 페이지에서 키셋 페이징과 오프셋 기반 페이징을 비교하는 내용을 확인할 수 있습니다.
API 개요
시놉시스
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 및 non-distinct 열을 감지하고, 이를 기반으로 기본 키를 사용하여 추가적인 순서를 추가합니다. 이는 키셋 페이징이 고유한 순서 값을 예상하기 때문에 필요합니다:
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
- 다음 페이지를 요청하기 위해 인코딩된 정렬 열 값 (null일 수 있음). -
per_page
- 페이지 당 로드할 레코드 수 (기본값 20). -
keyset_order_options
-UNION
쿼리에 대한 키셋으로 정렬된 데이터베이스 쿼리를 작성하는 추가 옵션, 성능 섹션에서 예제를 확인하세요 (선택 사항).
페이징 객체에는 다음과 같은 메소드가 있습니다:
-
records
- 현재 페이지의 레코드를 반환합니다. -
has_next_page?
- 다음 페이지가 있는지 알려줍니다. -
has_previous_page?
- 이전 페이지가 있는지 알려줍니다. -
cursor_for_next_page
- 다음 페이지를 요청하기 위한 인코딩된 값(문자열 형식)을 반환합니다 (null일 수 있음). -
cursor_for_previous_page
- 이전 페이지를 요청하기 위한 인코딩된 값(문자열 형식)을 반환합니다 (null일 수 있음). -
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
Rails에서 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
)로 정렬하는 경우, 생성된 쿼리는 효율적입니다. 왜냐하면 기본 키는 데이터베이스 인덱스로 covered되기 때문입니다.
ORDER BY
절에 두 개 이상의 열을 사용하는 경우 생성된 데이터베이스 쿼리를 확인하고 올바른 인덱스 구성이 사용되는지 확인하는 것이 좋습니다. 더 많은 정보는 페이지네이션 가이드 페이지에서 확인할 수 있습니다.
참고: 첫 번째 페이지의 쿼리 성능이 좋아 보일 수 있지만, 두 번째 페이지(커서 속성이 쿼리에 사용되는 페이지)는 성능이 나빠질 수 있습니다. 항상 첫 번째 페이지와 두 번째 페이지의 성능을 확인하는 것이 좋습니다.
id
열을 tie-breaker로 사용하는 예시 데이터베이스 쿼리:
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
PostgreSQL에서 OR
쿼리는 최적화하기 어려우며 보통 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
와 같은 사용자 정의 tie-breaker 열을 사용한 순서매김.
이러한 순서 객체는 표준 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 값을 가질 수 있기 때문에 열에 NOT 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,
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) # or 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
절에 열 식을 노출하도록 지시합니다. 이것은 킷셋 페이징이 레코드에서 마지막 값을 어떤 식으로든 추출하여 다음 페이지를 요청해야 하기 때문에 필요합니다.
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 # 작동