Как использовать (непрозрачные) курсоры в GraphQL/Relay при использовании аргументов фильтра и порядка

Представьте себе следующий запрос GraphQL:

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

Результат вернет соединение с информацией о курсоре реле.

Должен ли курсор содержать детали filter и orderBy?

Это означает, что запрос следующего набора данных будет означать только:

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

Или filter и orderBy нужно повторять?

В последнем случае пользователь может указать другие детали filter и/или orderBy, которые сделают непрозрачный курсор недействительным.

Я не могу найти ничего в спецификации реле по этому поводу.


person Marcel Overdijk    schedule 20.08.2018    source источник


Ответы (3)


Я видел, как это делается несколькими способами, но я обнаружил, что при нумерации страниц на основе курсора ваш курсор существует только в вашем наборе данных, и изменение фильтров изменит набор данных, сделав его недействительным.

Если вы используете SQL (или что-то еще без разбиения на страницы на основе курсора), вам нужно будет включить в свой курсор достаточно информации, чтобы иметь возможность восстановить его. Ваш курсор должен включать всю информацию о вашем фильтре / заказе, и вам нужно будет запретить любую дополнительную фильтрацию.

Вы должны были бы выдать ошибку, если бы они отправили «после» вместе с «filter / orderBy». При желании вы можете проверить, совпадают ли аргументы с аргументами вашего курсора, в случае ошибки пользователя, но просто нет варианта использования для получения «страницы 2» РАЗЛИЧНОГО набора данных.

person Dan Crews    schedule 21.08.2018

Я столкнулся с тем же вопросом/проблемой и пришел к тому же выводу, что и @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 зависит главным образом от вашей базы данных и ее макета.

Приведенный выше код эмулирует простую разбивку на страницы с ограничением и смещением. Но вы могли бы (если поддерживается вашей базой данных), конечно, использовать что-то еще.

person Benjamin M    schedule 09.02.2019

Тем временем я пришел к другому выводу: я думаю, что на самом деле не имеет значения, используете ли вы универсальный курсор или повторяете filter и orderBy с каждым запросом.

Существует два основных типа курсоров:

(1.) Вы можете рассматривать курсор как указатель на определенный элемент. Таким образом, фильтр и сортировка могут измениться, но ваш курсор останется прежним. Что-то вроде элемента поворота в быстрой сортировке, где элемент поворота остается на месте, а все вокруг него может двигаться.

Поиск Elasticsearch после работает так. Здесь cursor — это просто указатель на конкретный элемент в наборе данных. Но filter и orderBy могут меняться независимо.

Реализация этого стиля курсора предельно проста: просто объедините каждое сортируемое поле. Готово. Пример. Если ваш объект можно отсортировать по price и title (плюс, конечно, id, потому что вам нужно какое-то уникальное поле для разрешения конфликтов), ваш курсор всегда состоит из { id, price, title }.

(2.) С другой стороны, курсор «все в одном» действует как указатель на элемент в отфильтрованном и отсортированном наборе результатов. Преимущество в том, что вы можете кодировать все, что хотите. Сервер может, например, изменить данные filter и orderBy (по любой причине) так, что клиент этого не заметит.

Например, вы можете использовать Scroll API Elasticsearch. , который кэширует набор результатов на сервере и не требует filter и orderBy после первоначального поискового запроса.

Но помимо Elasticsearch Scroll API вам всегда нужны filter, orderBy, limit, pointer в каждом запросе. Хотя я думаю, что это деталь реализации и дело вкуса, включаете ли вы все в свой cursor или отправляете его в виде отдельных аргументов. Результат тот же.

person Benjamin M    schedule 14.10.2020