美文网首页
Active Storage 遇见GraphQL :公开附件的U

Active Storage 遇见GraphQL :公开附件的U

作者: 乐哈网 | 来源:发表于2019-07-17 00:17 被阅读0次

    上一篇分享了增加将Active Stroage的直接上传功能添加到Rails+GraphQL应用程序的技巧。

    现在我们知道如何上传,下一步:通过GraphQL API公开附件的URL

    • 处理N+1查询
    • 用户能够请求特定的图像变体

    N + 1问题:批量加载到救援
    让我们首先尝试以天真的方式将avatarUrl字段添加到我们的User类型中:

    module Types
      class User < GraphQL::Schema::Object
        field :id, ID, null: false
        field :name, String, null: false
        field :avatar_url, String, null: true
    
        def avatar_url
          # That's an official way for generating
          # Active Storage blobs URLs outside of controllers 😕
          Rails.application.routes.url_helpers
               .rails_blob_url(user.avatar)
        end
      end
    end
    

    假设我们有一个返回所有用户的端点,例如{ users { name avatarUrl } }。如果您在开发中运行此查询并查看控制台中的Rails服务器日志,您将看到如下内容:

    D, [2019-04-15T22:46:45.916467 #2500] DEBUG -- :   User Load (0.9ms)  SELECT users".* FROM "users"
    D, [2019-04-15T22:46:45.919362 #2500] DEBUG -- :   ActiveStorage::Attachment Load (0.9ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" = $1 AND "active_storage_attachments"."name" = $2 AND "active_storage_attachments"."record_id" = $3 [["record_type", "User"], ["name", "avatar"], ["record_id", 12]]
    D, [2019-04-15T22:46:45.922420 #2500] DEBUG -- :   ActiveStorage::Blob Load (1.0ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1  [["id", 9]]
    D, [2019-04-15T22:46:45.919362 #2500] DEBUG -- :   ActiveStorage::Attachment Load (0.9ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" = $1 AND "active_storage_attachments"."name" = $2 AND "active_storage_attachments"."record_id" = $3 [["record_type", "User"], ["name", "avatar"], ["record_id", 13]]
    D, [2019-04-15T22:46:45.922420 #2500] DEBUG -- :   ActiveStorage::Blob Load (1.0ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1  [["id", 10]]
    D, [2019-04-15T22:46:45.919362 #2500] DEBUG -- :   ActiveStorage::Attachment Load (0.9ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" = $1 AND "active_storage_attachments"."name" = $2 AND "active_storage_attachments"."record_id" = $3 [["record_type", "User"], ["name", "avatar"], ["record_id", 14]]
    D, [2019-04-15T22:46:45.922420 #2500] DEBUG -- :   ActiveStorage::Blob Load (1.0ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1  [["id", 15]]
    

    对于每个用户,我们加载一个ActiveStorage::Attachment和一个ActiveStorage::Blob记录:2 * N + 1个记录(其中N是用户数)。

    我们已经在“Rails 5.2:Active Storage and beyond”帖子中讨论了这个问题,所以,我不打算在此重复技术细节。

    tl; dr对于经典的 Rails应用程序,我们有一个内置的预加载附件范围(例如User.with_attached_avatar)或者可以自己生成范围,了解Active Storage命名内部关联的方式。

    GraphQL使预加载数据有点棘手 - 我们事先不知道客户端需要哪些数据,并且不能只添加with_attached_<smth>到每个Active Record集合(因为当我们不需要这些数据时会增加额外的开销)。

    这就是为什么经典的预压方法(includeseager_load等)并不是建造GraphQL的API非常有帮助。相反,大多数应用程序使用批量加载技术

    在Ruby应用程序中执行此操作的方法之一是通过Shopify 添加graphql-batchgem。它提供了一个核心API,用于编写具有类似Promise接口的批处理加载器。

    虽然默认情况下没有批量加载器包含在gem中,但association_loader我们可以使用一个示例来完成我们的任务(更确切地说,我们使用这个支持范围和嵌套关联的增强版本)。

    让我们用它来解决我们的N + 1问题:

    def avatar_url
      AssociationLoader.for(
        object.class,
        # We should provide the same arguments as
        # the `preload` or `includes` call when do a classic preloading
        avatar_attachment: :blob
      ).load(object).then do |avatar|        
        next if avatar.nil?        
        Rails.application.routes.url_helpers.rails_blob_url(avatar)        
      end
    end
    

    注意:then我们上面使用的方法不是#yield_self别名,它是promise.rbgem提供的API 。

    代码看起来有点过载,但它可以工作,并且仅根据用户数量进行3次查询。继续阅读,看看我们如何将其转化为人性化的API。

    处理变种

    我们希望利用GraphQL的强大功能,并允许客户指定所需的图像变体(例如,拇指,封面等):

    API示例

    从代码的角度来看,我们希望执行以下操作:

    user.avatar.variant(:thumb) # == user.avatar.variant(resize_to_fill: [64, 64])
    

    不幸的是,Active Storage还没有变体的概念(预定义的,命名的转换)。当PR(或其变体)合并时,可能会包含在Rails 6.x(其中x> 0)中。

    我们决定不再等待和实现这个功能我们自己:这个小补丁通过@bibendi增加定义YAML文件名为变种的能力:

    # config/transformations.yml
    
    thumb:
      convert: jpg
      resize_to_fill: [64, 64]
    
    medium:
      convert: jpg
      resize_to_fill: [200, 200]
    

    由于我们对应用程序中的所有附件都具有相同的转换设置,因此这种全局配置对我们很有用。

    现在我们需要将此功能集成到我们的API中。

    首先,我们在代表特定变体的模式中添加一个枚举类型transformations.yml

    class ImageVariant < GraphQL::Schema::Enum
      description <<~DESC
        Image variant generated with libvips via the image_processing gem.
        Read more about options here https://github.com/janko/image_processing/blob/master/doc/vips.md#methods
      DESC
    
      ActiveStorage.transformations.each do |key, options|
        value key.to_s, options.map { |k, v| "#{k}: #{v}" }.join("\n"), value: key
      end
    end
    
    

    感谢Ruby的元编程特性,我们可以使用配置对象动态定义我们的类型 - 我们transfromations.ymlImageVariant枚举将始终保持同步!

    image.png

    最后,让我们更新我们的字段定义以支持变体:

    module Types
      class User < GraphQL::Schema::Object
        field :avatar_url, String, null: true do
          argument :variant, ImageVariant, required: false
        end
    
        def avatar_url(variant: nil)
          AssociationLoader.for(
            object.class,
            avatar_attachment: :blob
          ).load(object).then do |avatar|
            next if avatar.nil?
            avatar = avatar.variant(variant) if variant
            Rails.application.routes.url_helpers.url_for(avatar)
          end
        end
      end
    end
    
    

    额外奖励:添加字段扩展名

    每次我们想要将附件url字段添加到类型时添加这么多代码似乎不是一个优雅的解决方案,是吗?

    在寻找更好的选择时,我找到了一个Field Extensions API graphql-ruby。“看起来就像我在找什么!”,我想。

    让我先向您展示最终的字段定义:

    field :avatar_url, String, null: true, extensions: [ImageUrlField]
    

    而已!没有更多argument-s和装载机。添加扩展程序使一切都按照我们想要的方式工作!

    这是扩展的带注释的代码:

    class ImageUrlField < GraphQL::Schema::FieldExtension
      attr_reader :attachment_assoc
    
      def apply
        # Here we try to define the attachment name:
        #  - it could be set explicitly via extension options
        #  - or we imply that is the same as the field name w/o "_url"
        # suffix (e.g., "avatar_url" => "avatar") 
        attachment = options&.[](:attachment) ||
                      field.original_name.to_s.sub(/_url$/, "")
    
        # that's the name of the Active Record association
        @attachment_assoc = "#{attachment}_attachment"
    
        # Defining an argument for the field
        field.argument(
          :variant,
          ImageVariant,
          required: false
        )
      end
    
      # This method resolves (as it states) the field itself
      # (it's the same as defining a method within a type)
      def resolve(object:, arguments:, **rest)
        AssociationLoader.for(
          object.class,
          # that's where we use our association name
          attachment_assoc => :blob
        )
      end
    
      # This method is called if the result of the `resolve`
      # is a lazy value (e.g., a Promise – like in our case)
      def after_resolve(value:, arguments:, object:, **rest)
        return if value.nil?
    
        variant = arguments.fetch(:variant, :medium)
        value = value.variant(variant) if variant
    
        Rails.application.routes.url_helpers.url_for(value)
      end
    end
    

    相关文章

      网友评论

          本文标题:Active Storage 遇见GraphQL :公开附件的U

          本文链接:https://www.haomeiwen.com/subject/sixflctx.html