# app/graphql/association/dsl.rb
module Association
module Dsl
extend ActiveSupport::Concern
class_methods do
def association(name, *args, policy: nil, source: nil, **options)
field name, *args, **options
source ||= name
define_method name do
policy_scope = policy&.then do
scope = "#{policy}Policy::Scope".constantize
policy_scope = scope.new(context[:current_user], policy).resolve
end
Association::Loader.for(object.class, source.to_sym).scoped(policy_scope).load(object)
end
end
def attachment(name, type, *args, source: nil, **options)
source ||= name
source = type.is_a?(Array) ? "#{source}_blobs" : "#{source}_blob"
association name, type, *args, source: source.to_sym, **options
end
end
end
end
# app/graphql/association/loader.rb
module Association
class Loader
# 公式ドキュメントのassociation_loader (https://github.com/Shopify/graphql-batch/blob/main/examples/association_loader.rb) との差分
def scoped(policy_scope = nil)
@policy_scope = policy_scope
self
end
def preload_association(records)
if Rails::VERSION::MAJOR < 7
ActiveRecord::Associations::Preloader.new.preload(records, @association_name, @policy_scope)
else
ActiveRecord::Associations::Preloader.new(records: records,
associations: @association_name,
scope: @policy_scope).call
end
end
end
end
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseType
field :name, String, null: false
association :posts, [Types::PostType], policy: Post, null: false
attachment :icon, Types::IconType, null: false
end
end
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.where(private: false).or(scope.where(user_id: current_user.id))
end
end
end
何も考えずともすべての関連テーブルへのアクセスをGraphQL::Batch化しN+1問題を解決する。
association
というメソッドで関連テーブルを宣言するだけで自動で上手いことやってくれる。
いちいちincludesを考えなければならない昔ながらのRailsよりも遥かに便利。
punditによる制限もちゃんと機能する。