【问题标题】:GraphQL Ruby: How to write DRY mutations with multiple parameters?GraphQL Ruby:如何编写具有多个参数的 DRY 突变?
【发布时间】:2020-03-22 10:45:57
【问题描述】:

我正在使用应用程序为我的graphql-ruby 编写一个基于devise-jwt 的身份验证系统。在这个过程中,我为创建一个新的用户帐户做了一个突变,它需要 7 个参数,这在我的代码中创建了很多重复:

module Mutations
  class SignUpMutation < Mutations::BaseMutation
    argument :email, String, required: true
    argument :password, String, required: true
    argument :family_name, String, required: true
    argument :family_name_phonetic, String, required: true
    argument :given_name, String, required: true
    argument :given_name_phonetic, String, required: true
    argument :newsletter_optin, Boolean, required: false

    field :token, String, null: true
    field :user, Types::UserType, null: true

    def resolve(email:, password:,
                family_name:, family_name_phonetic:,
                given_name:, given_name_phonetic:,
                newsletter_optin:
               )
      result = {
        token: nil,
        user: nil
      }
      new_user = User.new(
        email: email,
        password: password,
        family_name: family_name,
        family_name_phonetic: family_name_phonetic,
        given_name: given_name,
        given_name_phonetic: given_name_phonetic,
        newsletter_optin: newsletter_optin
      )
      if new_user.save!
        result[:token] = new_user.token
        result[:user] = new_user
      end
      result
    end
  end
end

我怎样才能干掉它以避免在整个地方重复突变参数的名称?

提前谢谢你!

【问题讨论】:

  • 通常当人们提到 DRY 时,它是关于不重复知识。变量名出现的次数不一定是个问题。如果您看到在多个不同的文件中声明了相同的函数,或者在多个不同的地方声明了相同的常量生活,则不是 DRY。该常数应该有一个具体的来源。虽然您可能会觉得这很烦人,但我实际上并没有发现这段代码有问题。
  • @unflores 说得有道理,我觉得很奇怪,因为 Ruby 让我习惯了更简洁的代码,所以无法绕过这种冗长:) 感谢您的反馈!
  • 老实说,过去我花了很多时间试图通过某种元编程来消除重复,结果却发现我无法搜索它。有很多情况可以保持 DRY,但有时更多的代码是正确的答案。 /耸肩

标签: ruby-on-rails ruby graphql


【解决方案1】:

在不干扰现有 GraphQL 模式的情况下,更好的方法是定义一个 InputType,其中包含所有常见参数,例如:

module Types
    module Inputs
        class CommonInputType < Types::Root::BaseInputObject
            graphql_name("my_common_input_type")

            argument :email, String, required: true
            argument :newsletter_optin, Boolean, required: true
            ...
            argument :posts, [Types::Inputs::Post], required: true
        end
    end
end

& 在某些突变中使用它并带有其他参数,例如:

module Mutations
    class CreateUser < Mutations::BaseMutation

        argument :additional_arg_one, ID, required: true
        argument :additional_arg_two, String, required: false
        ...

        Types::Inputs::CommonInputType.arguments.each do |arg,properties|
          argument arg.to_sym, properties.graphql_definition.type
        end

    end
end

【讨论】:

    【解决方案2】:

    在你的情况下,我也会使用输入对象,但是如果你有一个现有的 API,客户端依赖于模式,并且你想“干掉”那些在不同突变中都相同的重复参数,你会怎么做/字段?

    如果您继续实施新的输入对象,您将更改架构并且客户端很可能会中断。我想在将现有参数移动到输入对象时无法保持架构相同,对吧?

    【讨论】:

      【解决方案3】:

      您可以尝试双 splat (**) 运算符。

      module Mutations
        class SignUpMutation < Mutations::BaseMutation
          argument :email, String, required: true
          argument :password, String, required: true
          argument :family_name, String, required: true
          argument :family_name_phonetic, String, required: true
          argument :given_name, String, required: true
          argument :given_name_phonetic, String, required: true
          argument :newsletter_optin, Boolean, required: false
      
          field :token, String, null: true
          field :user, Types::UserType, null: true
      
          def resolve(**arguments)
            result = {
              token: nil,
              user: nil
            }
      
            new_user = User.new(
              email: arguments[:email],
              password: arguments[:password],
              family_name: arguments[:family_name],
              family_name_phonetic: arguments[:family_name_phonetic],
              given_name: arguments[:given_name],
              given_name_phonetic: arguments[:given_name_phonetic],
              newsletter_optin: arguments[:newsletter_optin]
            )
      
            if new_user.save!
              result[:token] = new_user.token
              result[:user] = new_user
            end
      
            result
          end
        end
      end
      

      当然,像你所做的那样创建一个新类型会更整洁。但是在某些情况下,您可以将它们组合在一起,例如

      module Mutations
        class SignUpMutation < Mutations::BaseMutation
          argument :another_attribute, String, required: true
          argument :attributes, Types::UserAttributes, required: true
      
          field :token, String, null: true
          field :user, Types::UserType, null: true
      
          def resolve(**arguments)
            result = {
              token: nil,
              user: nil
            }
      
            # use arguments[:another_attribute] for something else.
      
            new_user = User.new(arguments[:attributes].to_hash)
      
            if new_user.save!
              result[:token] = new_user.token
              result[:user] = new_user
            end
      
            result
          end
        end
      end
      

      【讨论】:

        【解决方案4】:

        回答我自己的问题。不必处理这么多参数的正确方法是使用输入对象而不是单独的参数。来自the graphql-ruby documentation

        输入对象类型是 GraphQL 操作的复杂输入。它们非常适合需要大量结构化输入的字段,例如突变或搜索字段。

        所以我已经这样定义了我的输入对象:

        module Types
          class UserAttributes < Types::BaseInputObject
            description 'Attributes for creating or updating a user'
            argument :email, String, required: true
            argument :password, String, required: true
            argument :family_name, String, required: true
            argument :family_name_phonetic, String, required: true
            argument :given_name, String, required: true
            argument :given_name_phonetic, String, required: true
            argument :newsletter_optin, Boolean, required: false
          end
        end
        

        然后像这样重构我的突变:

        module Mutations
          class SignUpMutation < Mutations::BaseMutation
            argument :attributes, Types::UserAttributes, required: true
        
            field :token, String, null: true
            field :user, Types::UserType, null: true
        
            def resolve(attributes:)
              result = {
                token: nil,
                user: nil
              }
              new_user = User.new(attributes.to_hash)
              if new_user.save!
                result[:token] = new_user.token
                result[:user] = new_user
              end
              result
            end
          end
        end
        

        最后,这段代码感觉更像 ruby​​ :)

        【讨论】:

          【解决方案5】:

          如果你愿意,你可以这样做:

          [
            :email,
            :password,
            :family_name,
            :family_name_phonetic,
            :given_name,
            :given_name_phonetic
          ].each do |arg|
            argument arg, String, required: true
          end
          

          您可能会认为这太过分了,但 Ruby 非常灵活。如果你真的想,你甚至可以做类似的事情

          def resolve(email:, password:,
                      family_name:, family_name_phonetic:,
                      given_name:, given_name_phonetic:,
                      newsletter_optin:)
            result = {
              token: nil,
              user: nil
            }
            params = method(__method__).parameters.map(&:last)
            opts = params.map{|p| [p, eval(p.to_s)]}.to_h
            new_user = User.new(opts)
            if new_user.save!
              result[:token] = new_user.token
              result[:user] = new_user
            end
            result
          end
          

          你可以看this answer解释

          如果您想要更多,您可以使用更详细的字段列表和define_method - 您可以一直到只输入例如:email 一次。

          这样会更好吗?也许,如果你有数百个这样的事情要做。 或者,如果您想在运行时开始定​​义事物。

          【讨论】:

          • 谢谢,这正是我想要弄清楚的事情。我已经接受了答案!
          猜你喜欢
          • 1970-01-01
          • 2022-01-07
          • 2021-08-23
          • 2018-07-02
          • 2019-04-10
          • 2023-02-07
          • 2020-08-23
          • 1970-01-01
          • 2020-09-22
          相关资源
          最近更新 更多