この記事は GraphQL Advent Calendar 2020 の 1 日目の記事です。
https://qiita.com/advent-calendar/2020/graphql
REST API などと同じようにあくまで設計次第という話です。
例えば、Organization と User があるとします。
Organization と User は N:N の関係にあるとします。
このとき、Organization と User の関連を示すのに、多くの場合中間テーブルを設けるでしょう。
これを Membership とします。
Organization 1-* Membership *-1 User
このとき、Membership はあくまで内部実装として API 上は隠蔽することも、露出することもできます。
以下のコードたちは保存して $ ruby foo.rb
していただければそのまま実行可能です。
Membership を見せない場合
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'graphql'
gem 'activerecord', require: 'active_record'
gem 'sqlite3'
end
require 'logger'
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :organizations, force: true do |t|
end
create_table :memberships, force: true do |t|
t.references :organization
t.references :user
end
create_table :users, force: true do |t|
end
end
class Organization < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
end
class Membership < ActiveRecord::Base
belongs_to :organization
belongs_to :user
end
class User < ActiveRecord::Base
has_many :memberships
has_many :organizations, through: :memberships
end
org1 = Organization.create!
org2 = Organization.create!
user1 = User.create!
user2 = User.create!
user3 = User.create!
Membership.create!(organization: org1, user: user1)
Membership.create!(organization: org1, user: user2)
Membership.create!(organization: org1, user: user3)
Membership.create!(organization: org2, user: user1)
class UserType < GraphQL::Schema::Object
field :id, ID, null: false
end
class OrganizationType < GraphQL::Schema::Object
field :id, ID, null: false
field :users, ['UserType'], null: false
end
class QueryType < GraphQL::Schema::Object
field :organization, 'OrganizationType', null: true do
argument :id, ID, required: true
end
def organization(id:)
Organization.find(id)
end
end
class Schema < GraphQL::Schema
query QueryType
end
result = Schema.execute(<<~GQL, variables: {id: org1.id})
query q($id: ID!) {
organization(id: $id) {
id
users {
id
}
}
}
GQL
pp result.as_json
# {"data"=>
# {"organization"=>
# {"id"=>"1", "users"=>[{"id"=>"1"}, {"id"=>"2"}, {"id"=>"3"}]}}}
result = Schema.execute(<<~GQL, variables: {id: org2.id})
query q($id: ID!) {
organization(id: $id) {
id
users {
id
}
}
}
GQL
pp result.as_json
# {"data"=>{"organization"=>{"id"=>"2", "users"=>[{"id"=>"1"}]}}}
Membership を見せる場合
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'graphql'
gem 'activerecord', require: 'active_record'
gem 'sqlite3'
end
require 'logger'
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :organizations, force: true do |t|
end
create_table :memberships, force: true do |t|
t.references :organization
t.references :user
end
create_table :users, force: true do |t|
end
end
class Organization < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
end
class Membership < ActiveRecord::Base
belongs_to :organization
belongs_to :user
end
class User < ActiveRecord::Base
has_many :memberships
has_many :organizations, through: :memberships
end
org1 = Organization.create!
org2 = Organization.create!
user1 = User.create!
user2 = User.create!
user3 = User.create!
Membership.create!(organization: org1, user: user1)
Membership.create!(organization: org1, user: user2)
Membership.create!(organization: org1, user: user3)
Membership.create!(organization: org2, user: user1)
class UserType < GraphQL::Schema::Object
field :id, ID, null: false
field :memberships, ['MembershipType'], null: false
end
class MembershipType < GraphQL::Schema::Object
field :id, ID, null: false
field :user, UserType, null: false
field :organization, 'OrganizationType', null: false
end
class OrganizationType < GraphQL::Schema::Object
field :id, ID, null: false
field :memberships, ['MembershipType'], null: false
end
class QueryType < GraphQL::Schema::Object
field :organization, 'OrganizationType', null: true do
argument :id, ID, required: true
end
def organization(id:)
Organization.find(id)
end
end
class Schema < GraphQL::Schema
query QueryType
end
result = Schema.execute(<<~GQL, variables: {id: org1.id})
query q($id: ID!) {
organization(id: $id) {
id
memberships {
user {
id
}
}
}
}
GQL
pp result.as_json
# {"data"=>
# {"organization"=>
# {"id"=>"1",
# "memberships"=>
# [{"user"=>{"id"=>"1"}}, {"user"=>{"id"=>"2"}}, {"user"=>{"id"=>"3"}}]}}}
result = Schema.execute(<<~GQL, variables: {id: org2.id})
query q($id: ID!) {
organization(id: $id) {
id
memberships {
user {
id
}
}
}
}
GQL
pp result.as_json
# {"data"=>{"organization"=>{"id"=>"2", "memberships"=>[{"user"=>{"id"=>"1"}}]}}}
まとめ
Graph "QL" とついているからか、SQL を投げるかのように DB をそのまま露出させると勘違いしている方がいるのではないかという懸念があり書きました。
Membership を見せるか見せないかと同じように、id や created_at など、個別のカラムについても返すべきか制御可能です。
また、隠すのとは逆に DB 上に実際は無いものを見せることも可能です。(例: DB 上はない full_name として first_name + last_name を見せる)
GraphQL API にしろ REST API にしろ、クライアントから見て使いやすい API はどういうものか、扱いたいリソースが何かを考えて設計することは変わりません。
特に API クライアントとサーバで実装者 (チーム) が完全に分かれている場合には、どういった形のほうがお互いに実装しやすいか、設計として自然かを、お互いの視点から話し合うことが重要でしょう。