GraphQL与REST等类似,不一定直接暴露数据库表

本文是GraphQL Advent Calendar 2020的第一天文章。
https://qiita.com/advent-calendar/2020/graphql


这只是根据设计而定的问题,就像REST API一样。

假设有一个组织(Organization)和用户(User)。
假设组织和用户之间存在N:N的关系。

在这种情况下,通常会创建一个中间表来表示组织和用户之间的关系。我们将其称为“成员关系表”。

组织 1-* 成员 *-1 用户

在这种情况下,Membership 可作为 API 的内部实现进行隐藏或暴露。

请把以下的代码保存到foo.rb文件中,然后通过运行”ruby foo.rb”命令即可立即执行。

如果不展示会员资格

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"}]}}}

显示会员资格

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"}}]}}}

总结

可能有人会误解为由于与“QL”有关,将数据库直接暴露出来,就像发出SQL查询一样。因此,我写下了这个担忧。

与展示会员资格一样,您还可以控制是否返回单独的列,如id和created_at。
此外,您还可以选择展示在数据库中实际上不存在的内容,这与隐藏内容的概念相反(例如,显示first_name + last_name作为全名,虽然数据库中并不存在full_name)。

无论是GraphQL API还是REST API,设计易于使用的API对于客户端来说都是一样的,需要考虑要处理的资源是什么。特别是在API客户端和服务器的实施者(团队)完全分开的情况下,从彼此的角度来讨论自然的设计方式和两者实施的便捷性是很重要的。

bannerAds