使用过滤器参数和排序依据时如何在 GraphQL / Relay 中使用(不透明)游标
How to use (opaque) cursors in GraphQL / Relay when using filter arguments and order by
想象一下以下 GraphQL 请求:
{
books(
first:10,
filter: [{field: TITLE, contains: "Potter"}],
orderBy: [{sort: PRICE, direction: DESC}, {sort: TITLE}]
)
}
结果会return一个带有Relay游标信息的连接。
光标是否应包含 filter
和 orderBy
详细信息?
意味着查询下一组数据只会意味着:
{
books(first:10, after:"opaque-cursor")
}
或者 filter
和 orderBy
应该重复吗?
在后一种情况下,用户可以指定不同的 filter
and/or orderBy
详细信息,这将使不透明光标无效。
我在中继规范中找不到任何关于此的内容。
我见过以多种方式完成此操作,但我发现使用基于游标的分页,您的游标仅存在于您的数据集中,并且更改过滤器会更改数据集,使其无效。
如果您正在使用 SQL(或没有基于游标的分页的东西),那么您需要在游标中包含足够的信息才能恢复它。您的光标需要包含所有过滤器/订单信息,并且您需要禁止任何其他过滤。
如果他们发送 "after" 和 "filter / orderBy",您将不得不抛出一个错误。您可以选择检查参数是否与光标中的参数相同,以防出现用户错误,但根本没有用例来获取 "page 2" 不同的数据集。
我遇到了同样的问题/问题,并得出了与@Dan Crews 相同的结论。游标必须包含执行数据库查询所需的所有内容,LIMIT
.
除外
当您的初始查询类似于
SELECT *
FROM DataTable
WHERE filterField = 42
ORDER BY sortingField,ASC
LIMIT 10
-- with implicit OFFSET 0
那么你基本上可以(不要在真实的应用程序中这样做,因为SQL注入!)完全使用此查询作为您的光标。您只需删除 LIMIT x
并为每个节点附加 OFFSET y
。
回复:
{
edges: [
{
cursor: "SELECT ... WHERE ... ORDER BY ... OFFSET 0",
node: { ... }
},
{
cursor: "SELECT ... WHERE ... ORDER BY ... OFFSET 1",
node: { ... }
},
...,
{
cursor: "SELECT ... WHERE ... ORDER BY ... OFFSET 9",
node: { ... }
}
]
pageInfo: {
startCursor: "SELECT ... WHERE ... ORDER BY ... OFFSET 0"
endCursor: "SELECT ... WHERE ... ORDER BY ... OFFSET 9"
}
}
下一个请求将使用 after: CURSOR, first: 10
。然后你将采用 after
参数并设置 LIMIT
和 OFFSET
:
LIMIT = first
OFFSET = OFFSET + 1
那么当使用 after = endCursor
:
时,结果数据库查询将是这样的
SELECT *
FROM DataTable
WHERE filterField = 42
ORDER BY sortingField,ASC
LIMIT 10
OFFSET 10
如上所述:这只是一个示例,它极易受到 SQL 注入攻击!
在真实世界的应用程序中,您可以简单地在光标内对提供的 filter
和 orderBy
参数进行编码,并添加 offset
以及:
function handleGraphQLRequest(first, after, filter, orderBy) {
let offset = 0; // initial offset, if after isn't provided
if(after != null) {
// combination of after + filter/orderBy is not allowed!
if(filter != null || orderBy != null) {
throw new Error("You can't combine after with filter and/or orderBy");
}
// parse filter, orderBy, offset from after cursor
cursorData = fromBase64String(after);
filter = cursorData.filter;
orderBy = cursorData.orderBy;
offset = cursorData.offset;
}
const databaseResult = executeDatabaseQuery(
filter, // = WHERE ...
orderBy, // = ORDER BY ...
first, // = LIMIT ...
offset // = OFFSET ...
);
const edges = []; // this is the resulting edges array
let currentOffset = offset; // this is used to calc the offset for each node
for(let node of databaseResult.nodes) { // iterate over the database results
currentOffset++;
const currentCursor = createCursorForNode(filter, orderBy, currentOffset);
edges.push({
cursor = currentCursor,
node = node
});
}
return {
edges: edges,
pageInfo: buildPageInfo(edges, totalCount, offset) // instead of
// of providing totalCount, you could also fetch (limit+1) from
// database to check if there is a next page available
}
}
// this function returns the cursor string
function createCursorForNode(filter, orderBy, offset) {
return toBase64String({
filter: filter,
orderBy: orderBy,
offset: offset
});
}
// function to build pageInfo object
function buildPageInfo(edges, totalCount, offset) {
return {
startCursor: edges.length ? edges[0].cursor : null,
endCursor: edges.length ? edges[edges.length - 1].cursor : null,
hasPreviousPage: offset > 0 && totalCount > 0,
hasNextPage: offset + edges.length < totalCount
}
}
cursor
的内容主要取决于你的数据库和你的数据库布局。
上面的代码模拟了一个带有限制和偏移量的简单分页。但是您当然可以(如果您的数据库支持)使用其他东西。
与此同时,我得出了另一个结论:我认为使用 all-in-one 游标还是重复使用 filter
和 orderBy
并不重要要求。
基本上有两种游标:
(1.) 您可以将光标视为 “指向特定项目的指针”。这样过滤器和排序可以改变,但你的光标可以保持不变。有点像快速排序中的枢轴元素,其中枢轴元素保持原位,周围的一切都可以移动。
Elasticsearch's Search After 是这样工作的。这里的 cursor
只是指向数据集中特定项目的指针。但是 filter
和 orderBy
可以独立改变。
这种游标样式的实现非常简单:只需连接 每个 可排序字段即可。完毕。 示例: 如果您的实体可以按 price
和 title
排序(当然加上 id
,因为您需要一些独特的字段作为决胜局) ,您的光标始终由 { id, price, title }
.
组成
(2.) 另一方面,"all-in-one 游标" 就像一个 " 指向已过滤和排序的项目的指针结果集。它的好处是,您可以随意编码。例如,服务器可以在客户端不注意的情况下更改 filter
和 orderBy
数据(无论出于何种原因)。
例如,您可以使用 Elasticsearch's Scroll API,它会在服务器上缓存结果集,但在初始搜索请求后不需要 filter
和 orderBy
。
但是除了 Elasticsearch 的 Scroll API,你每次请求总是需要 filter
、orderBy
、limit
、pointer
。尽管我认为这是一个实现细节和品味问题,无论您是将所有内容都包含在 cursor
中,还是将其作为单独的参数发送。结果是一样的。
想象一下以下 GraphQL 请求:
{
books(
first:10,
filter: [{field: TITLE, contains: "Potter"}],
orderBy: [{sort: PRICE, direction: DESC}, {sort: TITLE}]
)
}
结果会return一个带有Relay游标信息的连接。
光标是否应包含 filter
和 orderBy
详细信息?
意味着查询下一组数据只会意味着:
{
books(first:10, after:"opaque-cursor")
}
或者 filter
和 orderBy
应该重复吗?
在后一种情况下,用户可以指定不同的 filter
and/or orderBy
详细信息,这将使不透明光标无效。
我在中继规范中找不到任何关于此的内容。
我见过以多种方式完成此操作,但我发现使用基于游标的分页,您的游标仅存在于您的数据集中,并且更改过滤器会更改数据集,使其无效。
如果您正在使用 SQL(或没有基于游标的分页的东西),那么您需要在游标中包含足够的信息才能恢复它。您的光标需要包含所有过滤器/订单信息,并且您需要禁止任何其他过滤。
如果他们发送 "after" 和 "filter / orderBy",您将不得不抛出一个错误。您可以选择检查参数是否与光标中的参数相同,以防出现用户错误,但根本没有用例来获取 "page 2" 不同的数据集。
我遇到了同样的问题/问题,并得出了与@Dan Crews 相同的结论。游标必须包含执行数据库查询所需的所有内容,LIMIT
.
当您的初始查询类似于
SELECT *
FROM DataTable
WHERE filterField = 42
ORDER BY sortingField,ASC
LIMIT 10
-- with implicit OFFSET 0
那么你基本上可以(不要在真实的应用程序中这样做,因为SQL注入!)完全使用此查询作为您的光标。您只需删除 LIMIT x
并为每个节点附加 OFFSET y
。
回复:
{
edges: [
{
cursor: "SELECT ... WHERE ... ORDER BY ... OFFSET 0",
node: { ... }
},
{
cursor: "SELECT ... WHERE ... ORDER BY ... OFFSET 1",
node: { ... }
},
...,
{
cursor: "SELECT ... WHERE ... ORDER BY ... OFFSET 9",
node: { ... }
}
]
pageInfo: {
startCursor: "SELECT ... WHERE ... ORDER BY ... OFFSET 0"
endCursor: "SELECT ... WHERE ... ORDER BY ... OFFSET 9"
}
}
下一个请求将使用 after: CURSOR, first: 10
。然后你将采用 after
参数并设置 LIMIT
和 OFFSET
:
LIMIT = first
OFFSET = OFFSET + 1
那么当使用 after = endCursor
:
SELECT *
FROM DataTable
WHERE filterField = 42
ORDER BY sortingField,ASC
LIMIT 10
OFFSET 10
如上所述:这只是一个示例,它极易受到 SQL 注入攻击!
在真实世界的应用程序中,您可以简单地在光标内对提供的 filter
和 orderBy
参数进行编码,并添加 offset
以及:
function handleGraphQLRequest(first, after, filter, orderBy) {
let offset = 0; // initial offset, if after isn't provided
if(after != null) {
// combination of after + filter/orderBy is not allowed!
if(filter != null || orderBy != null) {
throw new Error("You can't combine after with filter and/or orderBy");
}
// parse filter, orderBy, offset from after cursor
cursorData = fromBase64String(after);
filter = cursorData.filter;
orderBy = cursorData.orderBy;
offset = cursorData.offset;
}
const databaseResult = executeDatabaseQuery(
filter, // = WHERE ...
orderBy, // = ORDER BY ...
first, // = LIMIT ...
offset // = OFFSET ...
);
const edges = []; // this is the resulting edges array
let currentOffset = offset; // this is used to calc the offset for each node
for(let node of databaseResult.nodes) { // iterate over the database results
currentOffset++;
const currentCursor = createCursorForNode(filter, orderBy, currentOffset);
edges.push({
cursor = currentCursor,
node = node
});
}
return {
edges: edges,
pageInfo: buildPageInfo(edges, totalCount, offset) // instead of
// of providing totalCount, you could also fetch (limit+1) from
// database to check if there is a next page available
}
}
// this function returns the cursor string
function createCursorForNode(filter, orderBy, offset) {
return toBase64String({
filter: filter,
orderBy: orderBy,
offset: offset
});
}
// function to build pageInfo object
function buildPageInfo(edges, totalCount, offset) {
return {
startCursor: edges.length ? edges[0].cursor : null,
endCursor: edges.length ? edges[edges.length - 1].cursor : null,
hasPreviousPage: offset > 0 && totalCount > 0,
hasNextPage: offset + edges.length < totalCount
}
}
cursor
的内容主要取决于你的数据库和你的数据库布局。
上面的代码模拟了一个带有限制和偏移量的简单分页。但是您当然可以(如果您的数据库支持)使用其他东西。
与此同时,我得出了另一个结论:我认为使用 all-in-one 游标还是重复使用 filter
和 orderBy
并不重要要求。
基本上有两种游标:
(1.) 您可以将光标视为 “指向特定项目的指针”。这样过滤器和排序可以改变,但你的光标可以保持不变。有点像快速排序中的枢轴元素,其中枢轴元素保持原位,周围的一切都可以移动。
Elasticsearch's Search After 是这样工作的。这里的 cursor
只是指向数据集中特定项目的指针。但是 filter
和 orderBy
可以独立改变。
这种游标样式的实现非常简单:只需连接 每个 可排序字段即可。完毕。 示例: 如果您的实体可以按 price
和 title
排序(当然加上 id
,因为您需要一些独特的字段作为决胜局) ,您的光标始终由 { id, price, title }
.
(2.) 另一方面,"all-in-one 游标" 就像一个 " 指向已过滤和排序的项目的指针结果集。它的好处是,您可以随意编码。例如,服务器可以在客户端不注意的情况下更改 filter
和 orderBy
数据(无论出于何种原因)。
例如,您可以使用 Elasticsearch's Scroll API,它会在服务器上缓存结果集,但在初始搜索请求后不需要 filter
和 orderBy
。
但是除了 Elasticsearch 的 Scroll API,你每次请求总是需要 filter
、orderBy
、limit
、pointer
。尽管我认为这是一个实现细节和品味问题,无论您是将所有内容都包含在 cursor
中,还是将其作为单独的参数发送。结果是一样的。