⚡️ Pro Feature ⚡️ This feature is bundled with GraphQL-Pro.
GraphQL::Pro includes a mechanism for serving stable connections for
ActiveRecord::Relations based on column values. If objects are created or destroyed during pagination, the list of items won’t be disrupted.
These connection implementations are database-specific so that they can build proper queries with regard to
NULL handling. (Postgres treats nulls as larger than other values while MySQL and SQLite treat them as smaller than other values.)
GraphQL::Pagination::ActiveRecordRelationConnection (which turns an
ActiveRecord::Relation into a GraphQL-ready connection) uses offset as a cursor. This naive approach is sufficient for many cases, but it’s subject to a specific set of bugs.
Let’s say you’re looking at the second page of 10 items (
LIMIT 10 OFFSET 10). During that time, one of the items on page 1 is deleted. When you navigate to page 3 (
LIMIT 10 OFFSET 20), you’ll actually miss one item. The entire list shifted “up” one position when a previous item was deleted.
To solve this bug, we should use a value to page through items (instead of offset). For example, if items are ordered by
id, use the
id for pagination:
LIMIT 10 -- page 1 WHERE id > :last_id LIMIT 10 -- page 2
This way, even when items are added or removed, pagination will continue without interruption.
For more information about this issue, see “Pagination: You’re (Probably) Doing It Wrong”.
You can use a stable connection for all
ActiveRecord::Relations by installing it at the schema level:
class MyAppSchema < GraphQL::Schema # Add the connection plugin use GraphQL::Pagination::Connections # Hook up the stable connection that matches your database connections.add(GraphQL::Pro::PostgresStableRelationConnection, ActiveRecord::Relation) # Or... # connections.add(GraphQL::Pro::MySQLStableRelationConnection, ActiveRecord::Relation) # connections.add(GraphQL::Pro::SqliteStableRelationConnection, ActiveRecord::Relation) end
Alternatively, you can apply the stable connection wrapper on a field-by-field basis. For example:
field :items, Types::ItemType.connection_type, null: false def items # Build an ActiveRecord::Relation relation = Item.all # And wrap it with a connection implementation, then return the connection GraphQL::Pro::MySQLStableRelationConnection.new(relation) end
That way, you can adopt stable cursors bit-by-bit. (See below for backwards compatibility notes.)
Similarly, if you enable stable connections for the whole schema, you can wrap specific relations with
GraphQL::Pagination::ActiveRecordRelationConnection when you want to use index-based cursors. (This is handy for relations whose ordering is too complicated for cursor generation.)
Keep these points in mind when using value-based cursors:
ActiveRecord::Relation, only columns of that specific model can be used in pagination. (This is because column names are turned into
primary_keyordering to ensure that the cursor value is unique. This behavior is inspired by
Relation#reverse_orderwhich also assumes that
primary_keyis the default sort.
SELECTclause, so that cursors can be reliably constructed from the database results.
When using a grouped
ActiveRecord::Relation, include a unique ID in your sort to ensure that each row in the result has a unique cursor. For example:
# Bad: If two results have the same `max(price)`, # they will be identical from a pagination perspective: Products.select("max(price) as price").group("category_id").order("price") # Good: `category_id` is used to disambiguate any results with the same price: Products.select("max(price) as price").group("category_id").order("price, category_id")
For ungrouped relations, this issue is handled automatically by adding the model’s
primary_key to the order values.
If you provide an unordered, grouped relation,
GraphQL::Pro::RelationConnection::InvalidRelationError will be raised because an unordered relation cannot be paginated in a stable way.
GraphQL::Pro’s stable relation connection is backwards-compatible. If it receives an offset-based cursor, it uses that cursor for the next resolution, then returns value-based cursors in the next result.
Stable relation connections support ActiveRecord