【问题标题】:How to use (opaque) cursors in GraphQL / Relay when using filter arguments and order by使用过滤器参数和排序时如何在 GraphQL / Relay 中使用(不透明)游标
【发布时间】:2019-01-26 09:17:30
【问题描述】:

想象以下 GraphQL 请求:

{
  books(
    first:10,
    filter: [{field: TITLE, contains: "Potter"}],
    orderBy: [{sort: PRICE, direction: DESC}, {sort: TITLE}]
  )
}

结果将返回一个带有 Relay 游标信息的连接。

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

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

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

还是应该重复 filterorderBy

在后一种情况下,用户可以指定不同的filter 和/或orderBy 详细信息,这会使不透明光标无效。

我在 Relay 规范中找不到任何关于此的内容。

【问题讨论】:

    标签: graphql relayjs relay


    【解决方案1】:

    我已经看到这样做的多种方式,但我发现使用基于光标的分页,您的光标仅存在于您的数据集中,并且更改过滤器会更改数据集,使其无效。

    如果您使用的是 SQL(或没有基于游标的分页),那么您需要在游标中包含足够的信息才能恢复它。您的光标需要包含所有过滤器/订单信息,并且您需要禁止任何其他过滤器。

    如果他们与“filter / orderBy”一起发送“after”,您将不得不抛出错误。您可以选择检查参数是否与光标中的参数相同,以防出现用户错误,但根本没有用例来获取不同数据集的“第 2 页”。

    【讨论】:

      【解决方案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的内容主要取决于你的数据库和你的数据库布局。

      上面的代码模拟了一个带有限制和偏移的简单分页。但是您当然可以(如果您的数据库支持)使用其他东西。

      【讨论】:

        【解决方案3】:

        与此同时,我得出另一个结论:我认为是否使用一体式光标,或者是否对每个请求重复 filterorderBy 并不重要。

        游标基本上有两种类型:

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

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

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

        (2.) 另一方面,“一体式光标” 的作用类似于 “指向过滤和排序结果集中某个项目的指针” .它的好处是你可以编码任何你想要的东西。例如,服务器可以更改 filterorderBy 数据(无论出于何种原因),而客户端不会注意到它。

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

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

        【讨论】:

          猜你喜欢
          • 2016-04-29
          • 2021-01-10
          • 2021-04-28
          • 2011-04-30
          • 2019-05-14
          • 1970-01-01
          • 2020-07-14
          • 2020-12-02
          • 1970-01-01
          相关资源
          最近更新 更多