使用过滤器参数和排序依据时如何在 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游标信息的连接。

光标是否应包含 filterorderBy 详细信息?

意味着查询下一组数据只会意味着:

{
  books(first:10, after:"opaque-cursor")
}

或者 filterorderBy 应该重复吗?

在后一种情况下,用户可以指定不同的 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 参数并设置 LIMITOFFSET:

  • LIMIT = first
  • OFFSET = OFFSET + 1

那么当使用 after = endCursor:

时,结果数据库查询将是这样的
SELECT *
FROM DataTable
WHERE filterField = 42
ORDER BY sortingField,ASC
LIMIT 10
OFFSET 10

如上所述:这只是一个示例,它极易受到 SQL 注入攻击!


在真实世界的应用程序中,您可以简单地在光标内对提供的 filterorderBy 参数进行编码,并添加 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 游标还是重复使用 filterorderBy 并不重要要求。

基本上有两种游标:

(1.) 您可以将光标视为 “指向特定项目的指针”。这样过滤器和排序可以改变,但你的光标可以保持不变。有点像快速排序中的枢轴元素,其中枢轴元素保持原位,周围的一切都可以移动。

Elasticsearch's Search After 是这样工作的。这里的 cursor 只是指向数据集中特定项目的指针。但是 filterorderBy 可以独立改变。

这种游标样式的实现非常简单:只需连接 每个 可排序字段即可。完毕。 示例: 如果您的实体可以按 pricetitle 排序(当然加上 id,因为您需要一些独特的字段作为决胜局) ,您的光标始终由 { id, price, title }.

组成

(2.) 另一方面,"all-in-one 游标" 就像一个 " 指向已过滤和排序的项目的指针结果集。它的好处是,您可以随意编码。例如,服务器可以在客户端不注意的情况下更改 filterorderBy 数据(无论出于何种原因)。

例如,您可以使用 Elasticsearch's Scroll API,它会在服务器上缓存结果集,但在初始搜索请求后不需要 filterorderBy

但是除了 Elasticsearch 的 Scroll API,你每次请求总是需要 filterorderBylimitpointer。尽管我认为这是一个实现细节和品味问题,无论您是将所有内容都包含在 cursor 中,还是将其作为单独的参数发送。结果是一样的。