0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

YAMAP エンジニアAdvent Calendar 2022

Day 12

GraphQL Ruby で複合主キーなレコードをバッチロードする

Last updated at Posted at 2022-12-11

概要

複合主キーなレコードをバッチロードしてみます。

自分の所属では数年前に GraphQL を導入したこともあり graphql-batch が使われていますが、今だと graphql-ruby の dataloder を使うのが一般的と思うので両方で実装してみます。

前提

以下のようなテーブルがあるとし、

viewer がユーザをフォローしているか否かを以下のクエリで取得するシチュエーションを考えます。

query ($id: ID!) {
  user(id: $id) { isFollowedByViewer }
}

GraphQL Batch の場合

Loaders::MultiColumnRecordLoader として実装します。

module Loaders
  class MultiColumnRecordLoader < GraphQL::Batch::Loader
    def initialize(model, columns:)
      @model = model
      @columns = columns
    end

    def perform(keys)
      conditions = keys.map {|key| @columns.zip(key).to_h }

      scoped = conditions.inject(@model.none) {|s, condition| s.or(@model.where(condition)) }

      scoped.each do |record|
        key = @columns.map {|column| record.public_send(column) }

        fulfill(key, record)
      end

      keys.each do |key|
        next if fulfilled?(key)

        fulfill(key, nil)
      end
    end
  end
end

呼び出しは以下のようになるでしょう

module Types
  class UserType < Types::BaseObject
    field :is_followed_by_viewer, Boolean, resolver_method: :followed_by_viewer?

    def followed_by_viewer?
      return false unless viewer

      loader = Loaders::MultiColumnRecordLoader.for(Follow, columns: %i[follower_id followee_id])

      loader([viewer.id, object.id]).then(&:present?)
    end
  end
end

GraphQL Ruby - Dataloader の場合

Sources::MultiColumnRecordSource として実装します。

module Sources
  class MultiColumnRecordSource < GraphQL::Dataloader::Source
    def initialize(model, columns:)
      @model = model
      @columns = columns
    end

    def fetch(keys)
      conditions = keys.map {|key| @columns.zip(key).to_h }

      scoped = conditions.inject(@model.none) {|s, condition| s.or(@model.where(condition)) }

      mapping = scoped.inject({}) do |record, m|
        key = @columns.map {|column| record.public_send(column) }

        m[key] = record
      end

      keys.map {|key| mapping.fetch(key, nil) }
    end
  end
end

呼び出しは以下のようになるでしょう

module Types
  class UserType < Types::BaseObject
    field :is_followed_by_viewer, Boolean, resolver_method: :followed_by_viewer?

    def followed_by_viewer?
      return false unless viewer

      loader = dataloader.with(Sources::MultiColumnRecordSource, Follow, %i[follower_id followee_id])

      follow = loader.load([viewer.id, object.id])

      follow.present?
    end
  end
end

所感

GraphQL Ruby の Dataloader の方が好きです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?