【问题标题】:Converting Relay Node ID to Rails ID when updating related records?更新相关记录时将中继节点 ID 转换为 Rails ID?
【发布时间】:2020-11-12 11:45:15
【问题描述】:

背景:

我有一个 Location 模型 has_one Addresshas_many Rooms。当我想通过更新其nameaddressrooms 来更新位置时,我使用以下InputObjects 来执行此操作:

module Types
  # Input interface for creating locations
  class LocationUpdateType < Types::BaseInputObject
    argument :id, ID, required: false
    argument :name, String, required: true
    argument :address, AddressUpdateType, required: true, as: :address_attributes
    argument :rooms, [RoomUpdateType], required: true, as: :rooms_attributes
  end

  class AddressUpdateType < Types::BaseInputObject
    argument :id, ID, required: true
    # not sure if I need this or not but it's commented out for now
    # argument :location_id, ID, required: true
    argument :street, String, required: true
    argument :city, String, required: true
    argument :state, String, required: true
    argument :zip_code, String, required: true

    # I'm not using this yet but I'm anticipating it because
    # accepts_nested_attributes can use it as an indicator to destroy this address
    argument :_destroy, Boolean, required: false
  end

  class RoomUpdateType < Types::BaseInputObject
    argument :id, ID, required: false
    # same thing with this ID.
    # argument :location_id, ID, required: true
    argument :name, String, required: true

    # accepts_nested_attributes flag for destroying related room records.
    # like above, I'm not using it yet but I plan to.
    argument :_destroy, Boolean, required: false
  end
end

当我发出 GraphQL 请求时,我的日志中出现以下信息:

Processing by GraphqlController#execute as JSON
  Variables: {"input"=>
  {"id"=>"TG9jYXRpb24tMzM=",
   "location"=>
    {"name"=>"A New Building",
     "address"=>
      {"city"=>"Anytown",
       "id"=>"QWRkcmVzcy0zMw==",
       "state"=>"CA",
       "street"=>"444 New Rd Suite 4",
       "zipCode"=>"93400"},
     "rooms"=>[{"id"=>"Um9vbS00Mw==", "name"=>"New Room"}]}}}

  mutation locationUpdate($input:LocationUpdateInput!) {
    locationUpdate(input: $input) {
      errors
      location {
        id
        name
        address {
          city
          id
          state
          street
          zipCode
        }
        rooms {
          id
          name
        }
      } 
    } 
  }

这是有道理的,我不想在客户端上使用真正的IDs,而是使用经过混淆的中继节点 ID。

问题:

当我的请求得到解决时,我正在使用这个 Mutation:

module Mutations
  # Update a Location, its address and rooms.
  class LocationUpdate < AdminMutation
    null true
    description 'Updates a locations for an account'
    field :location, Types::LocationType, null: true
    field :errors, [String], null: true
    argument :id, ID, required: true
    argument :location, Types::LocationUpdateType, required: true

    def resolve(id:, location:)
      begin
        l = ApptSchema.object_from_id(id)
      rescue StandardError
        l = nil
      end

      return { location: nil, errors: ['Location not found'] } if l.blank?

      print location.to_h
      # return { location: nil }

      # This is throwing an error because it doesn't like the Relay Node IDs.
      l.update(location.to_h)

      return { location: nil, errors: l.errors.full_messages } unless l.valid?

      { location: l }
    end
  end
end

当我打印传递到此解析器的哈希时,我得到以下信息:

{
  :name=>"A New Building",
  :address_attributes=>{
    :id=>"QWRkcmVzcy0zMw==",
    :street=>"444 New Rd Suite 4",
    :city=>"Anytown",
    :state=>"CA",
    :zip_code=>"93400"
  },
  :rooms_attributes=>[{:id=>"Um9vbS00Mw==", :name=>"New Room"}]
}

l.update 运行时,出现以下错误:

Couldn't find Room with ID=Um9vbS00Mw== for Location with ID=33

这对我来说非常有意义,因为中继节点 ID 没有存储在数据库中,所以我想我想弄清楚如何将 room.id 从中继节点 ID 转换为数据库中的 ID .

现在,我可以挖掘哈希并使用 ApptSchema.object_from_id 并将所有中继节点 ID 转换为 Rails ID,但这需要对每个节点进行数据库访问。我看到Connections 的文档列出了here,但这看起来更像是如何处理查询和分页。

如果我打算用相关记录更新记录,是否需要将数据库 ID 发送给客户?这不会违背中继节点 ID 的目的吗?有没有办法配置我的输入对象类型以将中继节点 ID 转换为 Rails ID,以便我在发送到解析器的哈希中获得正确的 ID?

【问题讨论】:

  • 对relay不是很熟悉,但是好像需要缓存relay节点id?通过为它们提供查找(redis?)或将中继节点ID存储在rails模型中?基本上要花费高昂的前期成本来进行转换,因此您以后不需要这样做。如果不是,为什么这是不可能的,可能会为其他解决方案提供一些见解。
  • 如果你不熟悉 Relay,我猜你可能也不熟悉 GraphQL?如果您是,那么我可以告诉您:我可以将带有模型类型的数据库 ID 作为 JSON 响应传递,然后在执行 GQL 请求时将其发回。我只是认为这违背了使用 Relay 的目的,因为有人可以使用调试器访问该数据库 ID,这将使他们能够深入了解数据库中的其他数据。我不认为将中继 ID 存储在数据库中是解决此问题的正确方法,但我可能在这里错误地使用了 GraphQL。
  • 其实对 GraphQL 有点熟悉,所以我没有提到它,只是没有使用 Relay。再次快速检查显示 Relay 用于 React。在这种情况下,我的建议更简单。切勿将实际 ID 传递给客户端。看看friendly_id gem。虽然这可能会解决一个不同的问题。不清楚您试图防止泄漏的确切见解,如果它是顺序排序(IE:给定 ID 5000,ID 1-4999 可能有效)然后使用友好 id 和 SecureRandom.alphanumeric(24) 创建 slug 是一个相当标准的解决方案以防止这种情况发生。
  • 请注意,SecureRandom.alphanumeric(24) 的随机性大约等于 UUID(和更清洁)的随机性,这就是我提到它的原因。
  • Relay 和 GQL 标准是由 Facebook 创建的(我相信),因此您可以将它与 React.js+Relay 客户端解决方案一起使用,但 Relay 和 GQL 一样是一个标准。你可以跟随或不跟随,这取决于你。 Relay 支持clientMutationId,它可以通过请求传递,并与响应一起返回。我在我的项目中不支持这一点,但它是一个 Relay 支架,所以我猜你可以在后端支持 Node.js 上的 Relay,或者在前端使用 Angular。不过,我会考虑研究这两种解决方案,谢谢。

标签: ruby-on-rails ruby graphql relay


【解决方案1】:

经过大量研究,似乎 Relay UUID 在尝试通过关系更新记录时并不可行。我认为 Relay 假设您使用的是 MongoDB 之类的东西,默认情况下会使用记录的主键来执行此操作。

按照@Nuclearman 的建议使用friendly_id 似乎是混淆网址的最佳方式。我决定做的是将friendly_id 添加到我想以“详细模式”查看的记录,例如/posts/3aacmw9mudnoitrswkh9vdrt,方法是创建这样的关注点:

module Sluggable
  extend ActiveSupport::Concern

  included do
    validates :slug, presence: true

    extend FriendlyId
    friendly_id :create_slug_id, use: :slugged

    private def create_slug_id
      # Try to see if the slug has already been created before generating a new one.
      @create_slug_id ||= self.slug
      @create_slug_id ||= SecureRandom.alphanumeric(24)
    end
  end
end

然后将关注点包含在您想要friendly_ids 的任何模型中,如下所示:

class Location < ApplicationRecord
  include Sluggable
end

然后在您的 GraphQL 类型中执行以下操作:

module Types
  class LocationType < Types::BaseObject
    field :id, ID, null: false

    # Only include this for model types that have friendly_id
    field :friendly_id String, null: false

    field :name, String, null: false

    # AddressType and RoomType classes won't have friendly_id because
    # I'm not going to have a url like /location/3aacmw9mudn/address/oitrswkh9vdrt
    # or /location/3aacmw9mudn/rooms/oitrswkh9vdrt
    field :address, AddressType, null: false
    field :rooms, [RoomType], null: false
  end
end

module Types
  class LocationUpdateType < Types::BaseInputObject
    argument :name, String, required: true
    argument :address, AddressUpdateType, required: true, as: :address_attributes
    argument :rooms, [RoomUpdateType], required: true, as: :rooms_attributes
  end
end

您可能希望将friendly_id 添加到您的所有模型中,但我猜您可能只希望它用于“重要”模型。传递给查询的变量不再将 UUID 用于依赖关系,而仅用于您在通过 ID 查找它们时更新的对象,如下所示:

{"input"=>
  {"id"=>"3aacmw9mudn",
   "location"=>
    {"name"=>"A New Building",
     "address"=>
      {"city"=>"Anytown",
       "id"=>"4",
       "state"=>"CA",
       "street"=>"444 New Rd Suite 4",
       "zipCode"=>"93400"},
     "rooms"=>[{"id"=>"13", "name"=>"New Room"}]}}}

现在你的解析器可能看起来像:

module Mutations
  # Update a Location, its address and rooms.
  class LocationUpdate < AdminMutation
    null true
    description 'Updates a locations for an account'
    field :location, Types::LocationType, null: true
    field :errors, [String], null: true
    argument :id, ID, required: true
    argument :location, Types::LocationUpdateType, required: true

    def resolve(id:, location:)
      # Here, we're passing the friendly_id to this resolver (see above)
      # not that actual ID of the record.
      l = Location.friendly.find(id)

      return { l: nil, errors: ['Location not found'] } if l.blank?

      l.update(location.to_h)

      return { location: nil, errors: l.errors.full_messages } unless l.valid?

      { location: l }
    end
  end
end

【讨论】:

    猜你喜欢
    • 2021-07-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多