@@ -13,23 +13,22 @@ implementing RESTful APIs.
1313Cursor-based pagination is a very powerful and valuable technique (see also
1414<<160>>) that allows to efficiently provide a stable view on changing data.
1515This is obtained by using an anchor element that allows to retrieve all page
16- elements directly via an ordering combined-index, usually based on `created_at`
17- or `modified_at`. Simple said, the cursor is the information set needed to
18- reconstruct the database query that retrieves the minimal page information from
19- the data storage.
16+ elements directly via an ordering index, usually based on `created_at` or
17+ `modified_at` in combination with a tie breaker, mostly the resource `id` or a
18+ unique `key`. In addition, the cursor contains all other information needed
19+ to reconstruct a database query that retrieves a minimal information set from
20+ the data storage to create a response page.
2021
2122The {cursor} itself is an opaque string, transmitted forth and back between
2223service and clients, that must never be *inspected* or *constructed* by
2324clients. Therefore, it is good practice to encode (encrypt) its content in a
2425non-human-readable form.
2526
2627The {cursor} content usually consists of a pointer to the anchor element
27- defining the page position in the collection, a flag whether the element is
28- included or excluded into/from the page, the retrieval direction, and a hash
28+ defining the page position in the collection, a flag whether the anchor element
29+ is included or excluded into/from the page, the retrieval direction, and a hash
2930over the applied query filters (or the query filter itself) to safely re-create
30- the collection. It is important to note, that a {cursor} should be always
31- defined in relation to the current page to anticipate all occurring changes
32- when progressing.
31+ the collection.
3332
3433The {cursor} is usually defined as an encoding of the following information:
3534
@@ -64,8 +63,8 @@ Cursor:
6463 description: >
6564 Flag for the retrieval direction that is defining which elements to
6665 choose from the collection resource starting from the anchor elements
67- position. It is either *ascending* or *descending* based on the
68- ordering combined index.
66+ position. It is either *ascending* or *descending* in relation to the
67+ ordering index - usually in sync with the pagination direction .
6968 type: string
7069 enum: [ ASCENDING, DESCENDING ]
7170
@@ -88,17 +87,55 @@ Cursor:
8887 - direction
8988----
9089
91- *Note:* In case of complex and long search requests, e.g. when {GET-with-body}
92- is already required, the {cursor} may not be able to include the `query` because
93- of common HTTP parameter size restrictions. In this case the `query` filters
94- should be transported via body - in the request as well as in the response,
95- while the pagination consistency should be ensured via the `query_hash`.
96-
97- *Remark:* It is also important to check the efficiency of the data-access.
98- You need to make sure that you have a fully ordered stable index, that allows
99- to efficiently resolve all elements of a page. If necessary, you need to
100- provide a combined index that includes the `id` to ensure the full order and
101- additional filter criteria to ensure efficiency.
90+ It is important to note, that {cursor} should always point to the last or first
91+ element of the current page - also `next` and `prev` cursors, depending on the
92+ retrieval direction. This allows the pagination to react on db-changes when
93+ (re-)evaluating a cursor, e.g. when elements are added or removed after the
94+ cursor was created.
95+
96+ image::assets/cursor.png[Cursor pagination illustration]
97+
98+ [cols="15%,15%,25%,25%",options="header",]
99+ |=========================================
100+ |Cursor | Position | Element | Direction
101+ | prev | F | EXCLUDED | DESCENDING
102+ | self~next~ | F | INCLUDED | ASCENDING
103+ | self~prev~ | I | INCLUDED | DESCENDING
104+ | next | I | EXCLUDED | ASCENDING
105+ |=========================================
106+
107+ *Remark:* In case of complex and long search requests, e.g. when
108+ {GET-with-body} is already required, the {cursor} may not be able to include
109+ the `query` because of common HTTP parameter size restrictions. In this case
110+ the `query` filters should be transported via body - in the request as well as
111+ in the response, while the pagination consistency should be ensured via the
112+ `query_hash`.
113+
114+ *Note:* The efficiency of cursor-based pagination depends on the efficiency
115+ of the ordering index. In most cases a single ordering index, e.g. based on
116+ `modified_at` is sufficient. Only in case of a low selectivity a combined
117+ index including the `id` or other filter criteria is needed. This should be
118+ carefully evaluated regularly on a case-by-case basis to avoid unnecessary
119+ complexity but also to avoid scalability and performance issues.
120+
121+ === Example
122+
123+ A real-world example can be found in
124+ https://github.com/zalando-build/api-repository/[api-repository] (internal
125+ link).
126+
127+ * The actual {cursor} implementation in
128+ https://github.com/zalando-build/api-repository/blob/main/src/main/java/org/zalando/infrastructure/api/repository/domain/model/ApiRevisions.kt#L16-L132[api-repository/domain/model/ApiRevisions.kt#L16-L132]
129+ (internal) is varying from the above pattern using a cursor-`type` instead of
130+ `element` to simplify cursor representation following above figure.
131+ * The {cursor} is passed down to data access object implementation in
132+ https://github.com/zalando-build/api-repository/blob/main/src/main/java/org/zalando/infrastructure/api/repository/repository/ApiRevisionViewRepository.kt#L48-L76[api-repository/repository/ApiRevisionViewRepository.kt#L48-L76]
133+ (internal) to create the SQL request.
134+
135+ The API repository, in this case does not need any combined index, since the
136+ primary ordering based on `modified_at` allows to efficiently access the data
137+ in combination with the additional filter criteria such as `states`, `teams`,
138+ and `applications`.
102139
103140=== Further reading
104141
0 commit comments