这里用GraphQL很好
你好。我是一名自由职业者,主要从事使用Rails进行开发工作的 ymstshinichiro。
在过去的大约三年中,我一直在GraphQL的现场工作,从今年4月开始,我同时在OpenAPI的现场也有工作。通过使用这两个工具,我个人认为GraphQL有以下优点。
抱歉:
-
- 「これからGraphQLを使っていくべきか迷っている」という方へ向けた記事になったので、既にGraphQLをガンガン使いこなしている人には当たり前のことしか書いてないかもです
-
- あくまで僕の 好み・主観・経験 に基づく記事なので、その辺考慮の上で読んでいただけると助かります
- 記事中のサンプルコードはRailsで書かれています
使用GraphQL可以自然地接近CQS。
我在不久前参加了YOUTRUST主办的会话学习会,我们一边看实际的production代码,一边交谈。我听说他们在遵循一般的Rails结构(REST)的同时,也严格实施了CQS。
有一些重要的要点,但只提取我们现在想讨论的部分,据说实施如下。
-
- Clientがデータを取得するとき、コントローラではQueryというサフィックスのクラスが呼び出される
データを絞り込むロジックはこのQueryクラスの中に閉じ込める
モデルのscopeは極力使わない
データを更新する際にはエンドポイントでUseCaseという概念のクラスを定義する
UseCaseからはCommandというサフィックスのクラスを呼び出す。具体的な更新ロジックはこのCommandクラスの中で記述する
認可はUseCase、データ整合はCommandの中でそれぞれバリデーションする
モデルにはバリデーションをできるだけ書かない
将其编写成代码大致如下(从https://github.com/team-youtrust/sample-webapp提取的部分):
# Queryの使用箇所。発表では 参照系 と呼ばれていた
class Api::FriendRequest::ReceivingController < Api::ApplicationController
before_action :authenticate_user!
def index
@receiving_friend_request_ids = ReceivingFriendRequestsQuery
.run(operation_user: current_user)
.map(&:encrypted_id)
end
end
# 補足: indexではidのみを返し、clientが描画に必要なデータはこのidを使って別のAPIを叩きにいく実装とおっしゃっていた記憶
# --------------
# UseCaseの使用箇所。発表では 更新系 と呼ばれていた
class Api::FriendRequest::AcceptController < Api::ApplicationController
before_action :authenticate_user!
def update
friend_request = FriendRequest.find_by_encrypted_id!(params[:id])
use_case = FriendRequest::AcceptUseCase.run(operation_user: current_user, friend_request: friend_request)
if use_case.success?
@friend_request = use_case.friend_request
render :update, status: :ok
else
head :bad_request
end
end
end
# 補足: UseCaseの中では対象レコードの行ロックを取ってCommandクラスを実行する
听着你说的话,我突然觉得这个跟 GraphQL 的 QueryType 和 MutationType 的各个 Resolver 有点相似呢。
我对GraphQL比REST更喜欢,觉得更清爽,这个想法我在以前就有了,而这个原因是
使用GraphQL时,通过单一的终端点(/graphql)-> 查询||变异-> 分别跟踪其解析器,可以自然地将其分离到接近CQS的UseCase级别。
我终于弄明白这是从哪儿来的了。这个问题我想了整整三年才有答案。
为了进行比较,将之前的代码改写成类似GraphQL的样式可能会变成这样。
# 認証はGraphqlControllerでやる && current_user はcontextから取れるように実装してある前提
# 参照系
class Resolvers::FriendRequestRecieving < BaseResolver
type [EncryptedId], null: false # カスタムスカラ型を定義する
def resolve
FriendRequest.where(user: current_user).map(&:encrypted_id)
end
end
# resolve内はQueryクラス相当と見做してモデルを直接呼んでいるが、
# 先のサンプル同様Queryクラスを作ってもいい。(そうするとテストがちょっと楽かも)
# 逆にResolverに全てを閉じ込めたテストにすると引数と返り値の型も一箇所にテストがまとまる良さはあるかも
# 先ほどの実装と比較になるようここではidを返す実装を踏襲しているが、
# 普通のGraphQLっぽくするならTypes::FriendRequesTypeを用意して,
# clientがidフィールドだけをリクエストするという実装にしても良い。
# また、その場合はid配列を引数に取れるようにこのqueryを実装して、
# id指定 + 詳細フィールドでもリクエストできるようにすると実装が一つで良いというメリットがある。
# が、逆に言うとclientが叩き分ける前提になってしまうので、サーバー主体で
# 何を返すかコントロールしたいケースでは用途ごとにちゃんと分割した方が良さそう。
# --------------
# 更新系 (こっちは中身ほぼ一緒)
class Mutations::FriendRequestAccept < BaseMutation
field :result, Boolean, null: false
argument :id, ID, required: true
def resolve(id:)
friend_request = FriendRequest.find_by_encrypted_id!(id)
use_case = FriendRequest::AcceptUseCase.run(operation_user: current_user, friend_request: friend_request)
raise GraphQL::ExecutionError, '更新処理に失敗しました' unless use_case.success?
{ result: true }
end
end
与常规的Rails控制器相比,以下可能是更有优势的一些点。
-
- このAPIが「何を引数にして 中で何をして 最後に何を返すのか」がResolverを見れば一目で全部わかる
- フィールドとResolverが1対1なのでコントローラの実装が見通しやすい
我之前没有使用过,但是在Resolver或InputType中可以编写参数的验证,所以我觉得这样做可以提高理解性,知道在哪里可以找到什么(应该做什么)。
作为相反的不好之处,在普通的Rails实现中,如果在控制器内有一个针对资源的共享 before_action,那么在GraphQL中需要单独在Resolver中实现它。
另一个重要的是,尽管引入GraphQL后代码看起来更加整洁,但仅凭此并不能实现CQS。
要真正实现CQS,需要像YOUTRUST公司一样,尽可能地避免在模型中编写逻辑,始终通过命令进行更新和验证。还需要将数据库分为主要/读取以面向命令/查询,以及进行正确的锁定并进行原子化实现。请注意还有许多其他要做的事情。
其实,我觉得从服务进行到一半开始进行这样的转变应该相当困难。你们采取了什么样的战术来推进架构迁移?我希望未来能在OPEN CODE中听到关于这些开发过程的讨论。
请说明补充如下:
在勉强会上介绍的资料如下所示。
YOUTRUST寺井先生,在你忙碌的时候,非常感谢你的合作!
在路由中需要考虑的事情很少。
(※ 与上述不同,以下是一段关于某个现场的对话)
正在进行的是实现一个功能,该功能涉及到表示人类的Account模型、表示所属的Organization模型,以及连接两者的中间表AccountOrganization。通过更新该功能,可以确定人类与哪个所属相关联。
记得当时我实现了一个像 PUT account/:account_id/organization/ 这样的URL路径,但是如果习惯了GraphQL,单从这个路径看的话,感觉有些难以理解在做什么。
此外,由于实际上更新的是中间表AccountOrganization,所以并不需要符合该表的相应终端节点?如果要进行更改,那么更新中间表记录的操作应该是创建或删除,而不是更新,这样的话就不是使用PUT方法了。
-
- 本来フォーカスすべき設計実装の話ではないところで迷いが発生する
- Clientがエンティティのことをかなり具体で知っている前提で諸々実装していく感じになる
我有些觉得这有点微妙呢,比如说,如果改进一下密码的名字可能会更好,或者根据库的使用方法,也许就不会有这样的烦恼,总之,好像你只是过度考虑了而已,这样的调侃完全有可能。
顺便提一下,如果使用GraphQL,只能使用POST方法,并且命名Mutation只需要将要执行的操作作为字段名称,所以客户端可以更容易地将必要的知识与后端的实现分离。
就以这次的例子来说,
解析器 -> 变异::账户::更改关联组织
字段 -> 账户更改关联组织
好像可以按照这样来实施。
因为可以从实现中生成模式,所以管理变得容易。
在REST系统中,经常使用API,但没有定义模式的情况比较常见。我想在初期创业阶段,最重要的是先创建出一些东西,所以这种情况是不可避免的。然而,随着服务的扩大,这将成为一种像针脚一样的束缚,这是可以想象的。
在GraphQL中,如果没有模式或字段不匹配,请求将无法通过,因此不太可能发生这种情况,并且可以从实现中生成模式,这对开发非常有帮助。
例如在实际推进任务时,
-
- まずFE/BEエンジニア感で「こんな感じのスキーマにしよう」という事前設計をする
- その後このスキーマが成立する、下記のような空の実装を作ってスキーマをgenerateし、開発中のベースブランチにコミットする
class Resolvers::AdminUser::Products < Resolvers::BaseResolver
field :products, [Types::ProductType], null: false
argument :keyword, String, null: true
def resolve(keyword: nil)
# 実装がこのような状態でもスキーマは出力可能
[]
end
end
从此刻开始,可以以基础分支的架构为基础,分别推进FE/BE的开发。
再以代码生成器的角度来看,OpenAPI已经有很长历史,因此客户端和服务器都有大量的库可用,我认为可以构建出像我们这次提出的生态系统那样的结构。
有时候,你会觉得受到工具的左右吗?特别是前端方面,工具的变迁非常频繁,对于长期存在的产品来说,最终使用的是哪个库呢?还有,用旧的生成器创建的代码与当前的实现相差甚远,这种情况可能会让人感到困难。
目前大多数情况下,如果使用Apollo框架来支持GraphQL,无论是加入一个新项目还是维护现有项目都能够轻松顺畅,给人留下这样的印象。
注意:可能称应用程序Relay更具优势,也可能有其他角度需要考虑。
错误处理的规范已经确定下来。
这篇文章太棒了,说想说的全部都写在上面了…!
写下以下的个人观点,我个人认为:
通过使用RESTful API时,“在出现这种错误时应该使用什么状态?”或者“返回什么样的消息?”这样的问题,意外地经常会引发争论,甚至根本没有讨论,变得随意而没有规范。当发生故障时,很难确定从何处跟踪问题的根源。
在GraphQL的情况下,如果想要以最低成本的方式处理,可以在基本的GraphqlController中使用Rescue来处理所有情况,并将日志发送到Sentry,然后通过GraphQL::ExecutionError返回一个类似于”发生了错误”的消息给前端。这种方法可以在一段时间内解决问题。(可以以最低成本提供必要的机制在发布阶段)
总结
最近我感觉到,在构建基于REST(以及Rails方式)的API服务器时,随着服务器规模的不断增大,最终可能会朝着使用GraphQL的实践方向发展。
如果在之后要启动的产品中,从一开始就使用GraphQL,可能能够减少后续的成本,这就是我在这篇文章中想要传达的内容(尤其是针对面向消费者的产品)。
也许有一些人在以往只用传统的Rails进行开发,从未接触过GraphQL,这些人在开展新项目时可能会继续以追求速度为重并延续以往的做法。
然而,从我个人的观点来看,学习GraphQL的成本并不是很高,并且如我之前所述,持续开发所带来的收益非常大。
如果您打算在接下来使用Rails建立一个新的API服务器,不妨考虑将GraphQL作为选项之一来引入。
参考文献