我想使用graphql-ruby和外部库进行分页

The background

大家都在用GraphQL吗?我个人非常喜欢它,无论是在工作中的产品还是私下开发,我都主要使用Ruby on Rails和graphql-ruby。GraphQL在遇到N+1查询的时候确实会有问题,但在一般的has_many关系中,可以使用goldiloader解决,而在复杂情况下,使用graphql-batch编写批处理代码通常可以避免这个问题。

关于GraphQL的问题,由于许多博客都介绍了Relay风格(基于游标)的分页方式,所以默认情况下,将其引入现有的基于偏移的应用程序可能会遇到困难。对此,有很多解决方法被提出。

然而,就我这个案例而言,我已经在项目中使用pagy作为页面标记的库。如果使用kaminari,可以通过引入名为graphql-kaminari_connection的Gem来实现kaminari的分页功能。然而,pagy没有这样的Gem,因此一开始我是参考graphql-kaminari_connection,在GraphQL中使用pagy进行分页。这个Gem非常简单,因此我可以很容易地进行模仿。但当时我必须自己定义参数如page和items,并在解析器中编写分页部分(现在不清楚现状如何)。

为了尽可能简洁地介绍如何在graphql-ruby中引入基于偏移的分页功能,我们在这里稍微深入一些,可能有点不按常规走?同时,将所有繁琐的事情交给外部库pagy来处理。

目标

假设我们有一个名为Test(从名字来看非常随意)的模型。在这种情况下,如果我们要使用graphql-ruby编写Relay风格的解析器,可能有很多写法,但下面的实现是可行的。

module Types
  class Tests1Resolver < GraphQL::Schema::Resolver

    type Test.connection_type, null: false

    def resolve
      ::Test.all
    end
  end
end

本次目标是以与上述内容几乎相同的描述,通过具体的实施,即与Types::Test.connection_type相似的描述(本次称为collection_type),实现在pagy下的分页功能。

module Types
  class Tests2Resolver < GraphQL::Schema::Resolver

    type Test.collection_type, null: false

    def resolve
      ::Test.all
    end
  end
end

环境 – .

    • ruby 3.0.1

 

    • rails 6.1.3.2

 

    • graphql-ruby 1.12.12

 

    pagy 4.9.0

准备

首先,我们需要安装graphql-ruby和pagy来进行基本设置。由于这方面已经在许多博客中有详细介绍,所以我将简要地写一下。

gem 'graphql'
gem 'pagy'
$ bundle
$ bin/rails generate graphql:install

我认为这样就会在app/graphql的文件夹下生成GraphQL的基本文件集。我希望能在这个基础上进行添加和编辑工作。

实施

我希望在graphql-ruby中实际引入pagy。

定义一个执行pagy处理的类

首先要定义的是实际执行pagy处理的部分实现。对于在控制器中使用pagy的人来说,我认为没有什么特别困难的地方。

module Types
  class Pagy
    include ::Pagy::Backend

    attr_reader :metadata, :collection

    def initialize nodes, page: nil, items: nil
      @metadata, @collection = paginate nodes, page: page, items: items
    end

    private

    def paginate nodes, page: nil, items: nil
      case nodes
      when ActiveRecord::Relation then pagy nodes, page: page, items: items
      when Array then pagy_array nodes, page: page, items: items
      end
    end

    def params
      {}
    end
  end
end

为了使用Pagy的每个方法,要像在控制器中引入一样,需要包括Pagy::Backend。此外,Pagy不仅为ActiveRecord提供分页功能,还为数组提供分页功能,因此在case语句中判断是ActiveRecord::Relation还是数组,并调用不同的方法来处理。

最后提及的是一个奇怪的params,这是因为在控制器的情况下,pagy需要通过params读取page和items,但这次将从GraphQL参数中提供,因此我们将使用一个空的哈希来处理。

定义一个自动定义参数和字段的类的定义。

通过在graphql-ruby中使用Relay,可以使用Test.connection_type这样的连接类型,即使不特别指定first或after等参数也会自动定义。同样地,我们将使用pagy作为要使用的字段,自动定义page和items。

module Types
  class PagyExtension < GraphQL::Schema::FieldExtension

    def apply
      field.argument :page, Integer, required: false
      field.argument :items, Integer, required: false
    end

    def resolve object:, arguments:, **_rest
      args = arguments.dup
      page = args.delete :page
      items = args.delete :items
      obj = yield object, args
      Pagy.new obj, page: page, items: items
    end
  end
end

因为resolve函数中定义的参数会作为arguments传递,所以我们要在取出page和items的同时,将其他参数传递给每个解析器,并将结果传递给之前定义的Types::Pagy。

将pagy的元数据定义为GraphQL对象

在graphql-ruby中,如果使用默认的Relay,您可以获取到像hasNextPage这样的页面信息作为pageInfo。类似地,我们可以定义一个GraphQL对象来存储从pagy方法返回的元数据。

module Types
  class Metadata < BaseObject

    field :count, Integer, null: false
    field :page, Integer, null: false
    field :items, Integer, null: false
    field :pages, Integer, null: false
    field :last, Integer, null: false
    field :offset, Integer, null: false
    field :from, Integer, null: false
    field :to, Integer, null: false
    field :prev, Integer, null: true
    field :next, Integer, null: true, resolver_method: :object_next

    delegate :next, to: :object, prefix: true
  end
end

在Pagy中,它会返回元数据作为返回值,其中包含一个名为”next”的实例变量。我想直接调用它,但由于这样会无法区分已存在的Ruby中的”next”,所以它会在日志中显示以下警告和处理方法。

Metadata's `field :next` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_next` and `def resolve_next`). Or use `method_conflict_warning: false` to suppress this warning.

所以,我們使用resolver_method來更改呼叫方法的名稱,並使用delegate使該方法能直接對應到object。

定義了一个生成动态包装了GraphQL对象的GraphQL对象的类。

标题看起来非常复杂,但正如目标所述,在graphql-ruby中,通过使用collection_type来针对GraphQL对象生成名为TestConnection的动态对象。为了实现类似的功能,我们将定义一个名为TestCollection的对象来创建该类。

module Types
  class Collection < BaseObject

    def self.create type
      Class.new self do
        graphql_name "#{type.graphql_name}Collection"
        field :collection, [type], null: false
        field :metadata, Metadata, null: false
      end
    end
  end
end

self.create函数动态生成继承Types::Collection对象(的类)。生成的类使用Types::Pagy中的pagy方法返回的collection和metadata作为字段进行定义。这里使用Types::Collection而不是Types::BaseObject是为了后续判断之用。

基于基本对象的collection_type实现。

为了使任何对象都能使用,我们将在基础对象中实现一个名为collection_type的类方法。

module Types
  class BaseObject < GraphQL::Schema::Object
    edge_type_class Types::BaseEdge
    connection_type_class Types::BaseConnection
    field_class Types::BaseField

    def self.collection_type
      @collection_type ||= Collection.create self
    end
  end
end

Types::Collection.create这个方法接受一个self参数(代表自身的类),但是要知道,collection_type方法本身是一个继承自Types::BaseObject的类(比如Types::Test)会调用的。也就是说,我们可以知道self参数中会传入Types::Test这样的对象。这样一来,就可以动态地生成包含Types::Test的新类。

拡大基地领域

使用上述的Types::PagyExtension,在最后来扩展Types::BaseField,以实现自动定义参数。

module Types
  class BaseField < GraphQL::Schema::Field
    argument_class Types::BaseArgument

    def initialize **kwargs, &block
      super
      return unless kwargs[:type].is_a? Class
      return unless kwargs[:type] < Collection
      extension PagyExtension
    end
  end
end

在这里,我们判断kwargs[:type]是否是Types::Collection。这样,只有在需要的情况下(即继承了Types::Collection的类),Types::PagyExtension中定义的page和items才会自动定义。

这样一来,实施就完成了。

考试 shì)

我使用随机数据在graphiql-rails中进行了测试。

Screen Shot 2021-06-23 at 1.23.33.png

虽然不太需要同时使用游标基准和偏移量基准,但出于样本实现的目的,我还是同时写了两种。源代码已经上传到GitHub,如果想参考的话,请务必查看。

总结

这次可能有点邪门,但我故意使用现有的库来实现分页。顺便说一下,偏移基础的分页在官方文档中也有提到,因其本质的原因,在删除或添加数据时难免会产生偏移。但我认为在管理界面等很多情况下,这不会成为大问题,而且pagy还有一个叫做pagy-cursor的扩展实现,可以支持基于游标的分页,所以可以尝试结合这些来实现(未经验证)基于游标的分页。

请参考

广告
将在 10 秒后关闭
bannerAds