API 페이지 매김 모범 사례


288

나는 내가 만들고있는 페이지 매김 된 API로 이상한 경우를 처리하는 데 도움이되는 것을 좋아합니다.

많은 API와 마찬가지로이 API는 큰 결과를 매 깁니다. / foos를 쿼리하면 100 개의 결과 (예 : foo # 1-100)와 foo # 101-200을 반환하는 / foos? page = 2에 대한 링크가 표시됩니다.

불행히도 API 소비자가 다음 쿼리를하기 전에 데이터 세트에서 foo # 10이 삭제되면 / foos? page = 2는 100만큼 오프셋되고 foos # 102-201을 반환합니다.

이는 모든 foo를 가져 오려는 API 소비자에게 문제가됩니다. foo # 101을받지 못합니다.

이것을 처리하는 가장 좋은 방법은 무엇입니까? 최대한 경량으로 만들고 싶습니다 (예 : API 요청에 대한 세션 처리 방지). 다른 API의 예제는 대단히 감사하겠습니다!


1
여기서 뭐가 문제 야? 나에게 괜찮은 것 같아, 어느 쪽이든 사용자가 100 항목을 얻을 것입니다.
NARKOZ

2
나는이 같은 문제에 직면하고 해결책을 찾고 있습니다. AFAIK에서 각 페이지가 새 쿼리를 실행하는 경우이를 달성 할 수있는 확실한 메커니즘이 없습니다. 내가 생각할 수있는 유일한 해결책은 활성 세션을 유지하고 결과를 서버 측에 유지하고 각 페이지에 대해 새 쿼리를 실행하는 대신 다음 캐시 된 레코드 세트를 가져 오는 것입니다.
Jerry Dodge


1
@java_geek since_id 매개 변수는 어떻게 업데이트됩니까? 트위터 웹 페이지에서는 since_id에 대해 동일한 값으로 두 요청을 모두하는 것처럼 보입니다. 새로운 트윗이 추가되면 그것들을 설명 할 수 있도록 언제 업데이트 될지 궁금합니다.
Petar

1
@Petar since_id 매개 변수는 API 소비자가 업데이트해야합니다.
보시다시피

답변:


176

데이터가 어떻게 처리되는지 잘 모르겠으므로 이것이 작동하거나 작동하지 않을 수 있지만 타임 스탬프 필드로 페이지 매김을 고려 했습니까?

/ foos를 쿼리하면 100 개의 결과가 나타납니다. 그런 다음 API는 다음과 같은 것을 반환해야합니다 (JSON을 가정하지만 XML이 필요한 경우 동일한 원칙을 따를 수 있습니다).

{
    "data" : [
        {  data item 1 with all relevant fields    },
        {  data item 2   },
        ...
        {  data item 100 }
    ],
    "paging":  {
        "previous":  "http://api.example.com/foo?since=TIMESTAMP1" 
        "next":  "http://api.example.com/foo?since=TIMESTAMP2"
    }

}

단 하나의 타임 스탬프 만 사용하면 결과에 암시 적 '제한'이 사용됩니다. 명시적인 제한을 추가하거나 until속성을 사용할 수도 있습니다 .

타임 스탬프는 목록의 마지막 데이터 항목을 사용하여 동적으로 결정될 수 있습니다. 이것은 페이스 북이 그래프 API 에서 페이지를 매기는 방법과 거의 비슷 합니다 (위로 내린 형식의 페이지 매김 링크를 보려면 아래로 스크롤하십시오).

한 가지 문제는 데이터 항목을 추가하는 것이지만 설명에 따라 데이터가 끝에 추가되는 것처럼 들립니다 (알려지지 않은 경우 알려 주시면 개선 할 수 있는지 확인할 것입니다).


30
타임 스탬프가 고유하지 않을 수도 있습니다. 즉, 동일한 타임 스탬프를 사용하여 여러 리소스를 만들 수 있습니다. 따라서이 방법은 다음 페이지가 현재 페이지의 마지막 (몇?) 항목을 반복 할 수 있다는 단점이 있습니다.
루블

4
@prmatta 실제로 데이터베이스 구현에 따라 타임 스탬프는 고유해야합니다 .
ramblinjan

2
@jandjorgensen 링크에서 : "타임 스탬프 데이터 형식은 증가하는 숫자이며 날짜 나 시간을 유지하지 않습니다. ... SQL Server 2008 이상에서 타임 스탬프 형식의 이름이 rowversion으로 바뀌 었 습니다. 목적과 가치. " 따라서 타임 스탬프 (실제로 시간 값을 포함하는 타임 스탬프)가 고유하다는 증거는 없습니다.
놀란 에이미

3
@jandjorgensen 귀하의 제안이 마음에 들지만 리소스 링크에 어떤 종류의 정보가 필요하지 않으므로 이전 또는 다음에 갈지 여부를 알고 있습니까? "이전": " api.example.com/foo?before=TIMESTAMP " "다음": " api.example.com/foo?since=TIMESTAMP2 "타임 스탬프 대신 시퀀스 ID도 사용합니다. 그것에 문제가 있습니까?
longliveenduro 2016 년

5
다른 유사한 옵션은 RFC 5988 (섹션 5)에 지정된 링크 헤더 필드를 사용하는 것입니다. tools.ietf.org/html/rfc5988#page-6
Anthony F

28

몇 가지 문제가 있습니다.

먼저, 당신이 인용 한 예가 있습니다.

행이 삽입되는 경우에도 비슷한 문제가 있지만이 경우 사용자는 중복 데이터를 얻습니다 (아마도 누락 된 데이터보다 관리하기가 쉽지만 여전히 문제가 있음).

원래 데이터 세트의 스냅 샷을 작성하지 않은 경우 이는 사실입니다.

사용자가 명시적인 스냅 샷을 만들도록 할 수 있습니다.

POST /createquery
filter.firstName=Bob&filter.lastName=Eubanks

어떤 결과 :

HTTP/1.1 301 Here's your query
Location: http://www.example.org/query/12345

그런 다음 하루 종일 페이징 할 수 있습니다. 이제는 정적 인 상태입니다. 전체 행이 아닌 실제 문서 키만 캡처 할 수 있으므로 상당히 가벼울 수 있습니다.

유스 케이스가 단순히 사용자가 모든 데이터를 원하고 필요로하는 것이라면 간단히 데이터를 제공 할 수 있습니다.

GET /query/12345?all=true

키트 전체를 보내면됩니다.


1
(행 삽입은 문제가되지 않도록 FOOS의 초기 정렬이 생성 날짜이다.)
2arrs2ells

실제로는 문서 키만 캡처하는 것만으로는 충분하지 않습니다. 이렇게하면 사용자가 요청할 때 ID로 전체 개체를 쿼리해야하지만 더 이상 존재하지 않을 수 있습니다.
Scadge

27

페이지 매김이 있으면 키로 데이터를 정렬합니다. API 클라이언트가 이전에 반환 된 컬렉션의 마지막 요소 키를 URL에 포함시키고 WHERESQL 쿼리 (또는 SQL을 사용하지 않는 경우 동등한 항목)에 절을 추가하여 해당 요소 만 반환하도록해서는 안됩니다. 키가이 값보다 큽니까?


4
이것은 나쁜 제안은 아니지만, 값을 기준으로 정렬한다고해서 이것이 '키', 즉 고유하다는 의미는 아닙니다.
Chris Peacock

바로 그거죠. 예를 들어, 제 경우에는 정렬 필드가 날짜이며 고유하지 않습니다.
토요일 Thiru

19

서버 측 로직에 따라 두 가지 접근 방식이있을 수 있습니다.

접근법 1 : 서버가 객체 상태를 처리 할만큼 똑똑하지 않은 경우.

캐시 된 모든 레코드 고유 ID를 서버에 보낼 수 있습니다 (예 : [ "id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8", "id9", "id10"] 및 부울 매개 변수를 사용하여 새 레코드를 요청하는지 (새로 고치기 위해) 또는 기존 레코드를로드하는지 (더 많이로드)를 알 수 있습니다.

서버는 [ "id1", "id2", "id3", "id4", "id5", "에서 삭제 된 레코드의 ID뿐만 아니라 새 레코드 (풀 레코드를 통해 더 많은 레코드 또는 새 레코드를로드)를 반환해야합니다. id6 ","id7 ","id8 ","id9 ","id10 "].

예 :- 더로드를 요청하는 경우 요청은 다음과 같습니다.

{
        "isRefresh" : false,
        "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10"]
}

이제 이전 레코드를 요청하고 (더로드) 누군가가 "id2"레코드를 업데이트하고 "id5"및 "id8"레코드가 서버에서 삭제되었다고 가정하면 서버 응답은 다음과 같아야합니다.

{
        "records" : [
{"id" :"id2","more_key":"updated_value"},
{"id" :"id11","more_key":"more_value"},
{"id" :"id12","more_key":"more_value"},
{"id" :"id13","more_key":"more_value"},
{"id" :"id14","more_key":"more_value"},
{"id" :"id15","more_key":"more_value"},
{"id" :"id16","more_key":"more_value"},
{"id" :"id17","more_key":"more_value"},
{"id" :"id18","more_key":"more_value"},
{"id" :"id19","more_key":"more_value"},
{"id" :"id20","more_key":"more_value"}],
        "deleted" : ["id5","id8"]
}

그러나이 경우 500 개의 로컬 캐시 된 레코드가 500이라고 가정하면 요청 문자열이 다음과 같이 너무 길어집니다.

{
        "isRefresh" : false,
        "cached" : ["id1","id2","id3","id4","id5","id6","id7","id8","id9","id10",………,"id500"]//Too long request
}

접근 방식 2 : 서버가 날짜에 따라 객체 상태를 처리 할 수있을 정도로 똑똑한 경우.

첫 번째 레코드의 ID, 마지막 레코드 및 이전 요청 시간을 보낼 수 있습니다. 이런 식으로 캐시 된 레코드가 많더라도 요청이 항상 적습니다.

예 :- 더로드를 요청하는 경우 요청은 다음과 같습니다.

{
        "isRefresh" : false,
        "firstId" : "id1",
        "lastId" : "id10",
        "last_request_time" : 1421748005
}

서버는 last_request_time 이후에 삭제 된 삭제 된 레코드의 ID를 반환하고 "id1"과 "id10"사이의 last_request_time 이후에 업데이트 된 레코드를 리턴해야합니다.

{
        "records" : [
{"id" :"id2","more_key":"updated_value"},
{"id" :"id11","more_key":"more_value"},
{"id" :"id12","more_key":"more_value"},
{"id" :"id13","more_key":"more_value"},
{"id" :"id14","more_key":"more_value"},
{"id" :"id15","more_key":"more_value"},
{"id" :"id16","more_key":"more_value"},
{"id" :"id17","more_key":"more_value"},
{"id" :"id18","more_key":"more_value"},
{"id" :"id19","more_key":"more_value"},
{"id" :"id20","more_key":"more_value"}],
        "deleted" : ["id5","id8"]
}

당겨 새로 고침 :-

여기에 이미지 설명을 입력하십시오

더로드

여기에 이미지 설명을 입력하십시오


14

API가있는 대부분의 시스템은이 시나리오에 적합하지 않기 때문에 모범 사례를 찾기가 어려울 수 있습니다. 극단적 인 측면이거나 일반적으로 레코드를 삭제하지 않기 때문입니다 (Facebook, Twitter). 페이스 북은 실제로 각 "페이지"에 페이지 매김 후 필터링으로 인해 요청 된 결과 수가 없을 수 있다고 말합니다. https://developers.facebook.com/blog/post/478/

이 엣지 케이스를 실제로 수용해야하는 경우 중단 한 부분을 "기억"해야합니다. jandjorgensen 제안은 거의 확실하지만 기본 키와 같은 고유 한 필드를 사용합니다. 하나 이상의 필드를 사용해야 할 수도 있습니다.

Facebook의 흐름에 따라 이미 요청한 페이지를 캐시하고 이미 요청한 페이지를 요청하면 삭제 된 행이 필터링 된 페이지를 반환 할 수 있습니다.


2
이것은 수용 가능한 해결책이 아닙니다. 시간과 메모리가 많이 소모됩니다. 요청 된 데이터와 함께 삭제 된 모든 데이터는 동일한 사용자가 더 이상 항목을 요청하지 않으면 전혀 사용되지 않을 수있는 메모리에 보관해야합니다.
Deepak Garg

3
동의하지 않습니다. 고유 ID를 유지하는 것만으로는 많은 메모리를 사용하지 않습니다. "세션"에 대해서만 데이터를 무기한 보유하지 않습니다. memcache를 사용하면 간단합니다. 만료 기간 (예 : 10 분) 만 설정하면됩니다.
브렌트 바이 슬리

메모리는 네트워크 / CPU 속도보다 저렴합니다. 따라서 페이지를 만드는 데 비용이 많이 드는 경우 (네트워크 측면에서 또는 CPU를 많이 사용하는 경우) 캐싱 결과는 올바른 접근 방식입니다. @DeepakGarg
U Avalos

9

페이지 매김은 일반적으로 "사용자"작업이며 컴퓨터와 사람의 뇌에 과부하가 걸리는 것을 방지하기 위해 일반적으로 하위 세트를 제공합니다. 그러나 우리가 전체 목록을 얻지 못한다고 생각하기보다는 그것이 중요합니까?

정확한 라이브 스크롤보기가 필요한 경우 본질적으로 요청 / 응답 인 REST API는이 목적에 적합하지 않습니다. 이를 위해 변경 사항을 처리 할 때 프런트 엔드에 알리려면 WebSocket 또는 HTML5 서버 전송 이벤트를 고려해야합니다.

이 있다면 지금 필요한 데이터의 스냅 샷을 얻을 수는, 그냥 아무 매김 한 요청에 모든 데이터를 제공하는 API 호출을 제공한다. 큰 데이터 세트가있는 경우 출력을 메모리에 일시적으로로드하지 않고 출력을 스트리밍하는 무언가가 필요합니다.

필자의 경우 전체 정보 (주로 참조 테이블 데이터)를 가져올 수 있도록 일부 API 호출을 암시 적으로 지정합니다. 시스템에 해를 끼치 지 않도록 이러한 API를 보호 할 수도 있습니다.


8

옵션 A : 타임 스탬프를 사용한 키셋 페이지 매김

언급 한 오프셋 페이지 매김의 단점을 피하기 위해 키 세트 기반 페이지 매김을 사용할 수 있습니다. 일반적으로 엔터티에는 생성 또는 수정 시간을 나타내는 타임 스탬프가 있습니다. 이 타임 스탬프는 페이지 매김에 사용될 수 있습니다. 마지막 요소의 타임 스탬프를 다음 요청의 쿼리 매개 변수로 전달하십시오. 서버는 차례로 필터 기준으로 타임 스탬프를 사용한다 (예 WHERE modificationDate >= receivedTimestampParameter)

{
    "elements": [
        {"data": "data", "modificationDate": 1512757070}
        {"data": "data", "modificationDate": 1512757071}
        {"data": "data", "modificationDate": 1512757072}
    ],
    "pagination": {
        "lastModificationDate": 1512757072,
        "nextPage": "https://domain.de/api/elements?modifiedSince=1512757072"
    }
}

이런 식으로, 당신은 어떤 요소를 놓치지 않을 것입니다. 이 방법은 많은 사용 사례에 충분해야합니다. 그러나 다음 사항에 유의하십시오.

  • 단일 페이지의 모든 요소에 동일한 타임 스탬프가 있으면 무한 루프가 발생할 수 있습니다.
  • 타임 스탬프가 동일한 요소가 두 페이지로 겹치는 경우 여러 요소를 클라이언트에 여러 번 전달할 수 있습니다.

페이지 크기를 늘리고 밀리 초 정밀도로 타임 스탬프를 사용하면 이러한 단점을 줄일 수 있습니다.

옵션 B : 연속 토큰을 사용한 확장 키 세트 페이지 매김

일반적인 키 세트 페이지 매김의 언급 된 단점을 처리하기 위해 타임 스탬프에 오프셋을 추가하고 소위 "Continuation Token"또는 "Cursor"를 사용할 수 있습니다. 오프셋은 타임 스탬프가 동일한 첫 번째 요소를 기준으로 한 요소의 위치입니다. 일반적으로 토큰의 형식은 다음과 같습니다 Timestamp_Offset. 응답에서 클라이언트로 전달되며 다음 페이지를 검색하기 위해 서버로 다시 제출할 수 있습니다.

{
    "elements": [
        {"data": "data", "modificationDate": 1512757070}
        {"data": "data", "modificationDate": 1512757072}
        {"data": "data", "modificationDate": 1512757072}
    ],
    "pagination": {
        "continuationToken": "1512757072_2",
        "nextPage": "https://domain.de/api/elements?continuationToken=1512757072_2"
    }
}

"1512757072_2"토큰은 페이지의 마지막 요소를 가리키고 "클라이언트는 이미 타임 스탬프가 1512757072 인 두 번째 요소를 얻었습니다"라고 표시합니다. 이런 식으로 서버는 계속할 위치를 알고 있습니다.

두 요청간에 요소가 변경된 경우를 처리해야합니다. 이것은 일반적으로 체크섬을 토큰에 추가하여 수행됩니다. 이 체크섬은이 타임 스탬프가있는 모든 요소의 ID에 대해 계산됩니다. 따라서 다음과 같은 토큰 형식으로 끝납니다.Timestamp_Offset_Checksum .

이 방법에 대한 자세한 내용은 블로그 게시물 " Continuation Tokens로 Web API 페이지 매김 "을 확인하십시오 . 이 접근법의 단점은 고려해야 할 많은 코너 사례가 있기 때문에 까다로운 구현입니다. 이것이 연속 토큰 과 같은 라이브러리 가 편리한 이유입니다 (Java / JVM 언어를 사용하는 경우). 면책 조항 : 나는 게시물의 저자이자 도서관의 공동 저자입니다.


4

나는 현재 당신의 API가 실제로 해야하는 방식으로 응답한다고 생각합니다. 페이지에서 처음 100 개의 레코드는 유지 보수중인 오브젝트의 전체 순서로 기록됩니다. 귀하의 설명은 페이지 매김을 위해 객체의 순서를 정의하기 위해 일종의 주문 ID를 사용하고 있음을 알려줍니다.

이제 페이지 2가 항상 101에서 시작하여 200에서 끝나도록하려면 페이지의 항목 수를 변수로 만들어야합니다. 삭제 될 수 있기 때문입니다.

아래 의사 코드와 같은 것을해야합니다.

page_max = 100
def get_page_results(page_no) :

    start = (page_no - 1) * page_max + 1
    end = page_no * page_max

    return fetch_results_by_id_between(start, end)

1
나는 동의한다. 신뢰할 수없는 레코드 번호로 쿼리하는 대신 ID로 쿼리해야합니다. "ID> x를 사용하여 ID별로 정렬 된 m 개의 레코드까지 반환"을 의미하도록 쿼리 (x, m)를 변경 한 다음 x를 이전 쿼리 결과의 최대 id로 간단히 설정할 수 있습니다.
존 헨켈

사실, ID를 기준으로 정렬하거나 creation_date 등과 같이 정렬 할 구체적인 비즈니스 필드가있는 경우
mickeymoon

4

Kamilk 의이 답변에 추가하기 위해 : https://www.stackoverflow.com/a/13905589

작업중인 데이터 세트의 양에 따라 다릅니다. 작은 데이터 세트는 오프셋 페이지 매김 에서 효과적으로 작동 하지만 큰 실시간 데이터 세트에는 커서 페이지 매김 이 필요합니다 .

슬랙 방법에 대한 훌륭한 기사를 찾았습니다. 이 데이터 세트는 모든 단계에서 긍정적 인면과 부정적인를 설명 증가로 자사의 API의 페이지 매김 진화를 : https://slack.engineering/evolving-api-pagination-at-slack-1c1f644f8e12


3

나는 이것에 대해 길고 열심히 생각하고 마침내 아래에서 설명 할 솔루션으로 끝났습니다. 복잡성이 상당히 높아지지만이 단계를 수행하면 실제로 수행 한 결과로 이어질 수 있으며 이는 향후 요청에 대한 결정적인 결과입니다.

항목이 삭제되는 예는 빙산의 일각에 불과합니다. 필터링 color=blue하지만 누군가 요청 사이에서 항목 색상을 변경 하면 어떻게 됩니까? 페이지 히스토리 방식으로 모든 항목을 안정적으로 가져 오는 것은 불가능합니다 . .

나는 그것을 구현했으며 실제로 예상보다 덜 어렵습니다. 내가 한 일은 다음과 같습니다.

  • 하나의 테이블을 만들었습니다 changelogs자동 증분 ID 열이 을
  • 나의 실체는 id 필드가 있지만 이것이 기본 키가 아닙니다
  • 엔터티에는 changeId기본 키와 변경 로그의 외래 키 필드가 있습니다.
  • 사용자가 레코드를 생성, 업데이트 또는 삭제할 때마다 시스템은에 새 레코드를 삽입 changelogs하고 ID를 가져 와서 버전의 엔티티에 할당 한 다음 DB에 삽입합니다.
  • 내 쿼리는 모든 레코드의 최신 버전을 가져 오기 위해 최대 changeId (ID로 그룹화) 및 자체 조인을 선택합니다.
  • 필터는 최신 레코드에 적용됩니다
  • 상태 필드는 항목 삭제 여부를 추적합니다
  • max changeId가 클라이언트에 리턴되고 후속 요청에서 조회 매개 변수로 추가됩니다.
  • 새로운 변경 사항 만 생성되므로 매번 changeId 시점의 기본 데이터에 대한 고유 한 스냅 샷을 나타냅니다.
  • 이는 매개 변수가있는 요청의 결과를 changeId영원히 캐시 할 수 있음을 의미합니다 . 결과는 절대 변경되지 않으므로 만료되지 않습니다.
  • 또한 롤백 / 리버 트, 클라이언트 캐시 동기화 등과 같은 흥미로운 기능을 제공합니다. 변경 기록에서 혜택을받는 모든 기능.

혼란 스러워요. 이것이 당신이 언급 한 유스 케이스를 어떻게 해결합니까? (캐시에서 임의의 필드가 변경되고 캐시를 무효화하려는 경우)
U Avalos

자신이 변경 한 내용은 응답을 참조하십시오. 서버는 새로운 changeId를 제공하고 다음 요청에서이를 사용합니다. 다른 사람이 변경 한 다른 변경 사항의 경우 최신 changeId를 가끔씩 한 번씩 폴링하고 자신보다 높으면 눈에 띄는 변경 사항이 있음을 알고 있습니다. 또는 미해결 변경 사항이있을 때 클라이언트에게 알리는 알림 시스템 (롱 폴링. 서버 푸시, 웹 소켓)을 설정합니다.
Stijn de Witt

0

RESTFul API에서 페이지 매김에 대한 다른 옵션은 여기에 소개 된 링크 헤더를 사용하는 입니다. 예를 들어 Github 는 다음과 같이 사용합니다 :

Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",
  <https://api.github.com/user/repos?page=50&per_page=100>; rel="last"

가능한 값 relfirst, last, next, previous 입니다. 그러나 Linkheader를 사용하면 total_count (총 요소 수) 를 지정하지 못할 수 있습니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.