はじめに
どうもこんにちわ。サーバサイドエンジニアのgrnishiです。一度は行ってみたい場所はトルクメニスタンのダルヴァザです。ここにあるガスクレーター通称地獄の門が有名です。
もしこの世がRPGの世界であるならば、ここにはラスボスがいるか最強の武器が眠っているか。そんな妄想が捗る場所です。
本題
タイトルの通りですが、今さらGraphQLを試してみる事にしました。随分前から知ってはいたのですが、自他ともに認めるREST信者なので食わず嫌いを続けていました。
REST信者と言いながらもほんとの信者なら他を知った上で使う事が大切である事、そもそも自分の知識の幅をもう少し広げようという事でほぼサンプルを写経しながら使ってみました。
GraphQLとは
いちいち説明するのは野暮な事なのでウィキペディアに一任。
環境
今回はPHPで実装する事とするので、てっとり早く下記のライブラリを使います。
今回はGraphQLを試すという事でそれ以外の部分は極力シンプルに作りました。 実際に使う場合はもっと色んな部分で考えなくてはいけないです。
こちらの記事を参考に実装しました。
データベース
今回は簡易なRPGを想定して下記のようなデータベースを用意しました。
USER_DATA
Field | Type | Key |
---|---|---|
user_id | int(11) | PRI |
user_name | varchar(16) | |
level | tinyint(4) | |
exp | int(11) | |
gold | int(11) | |
hp | int(11) | |
mp | int(11) | |
attack | int(11) | |
guard | int(11) |
USER_ITEM_DATA
Field | Type | Key |
---|---|---|
user_id | int(11) | PRI |
item_id | int(11) | PRI |
equip | tinyint(4) |
USER_MAGIC_DATA
Field | Type | Key |
---|---|---|
user_id | int(11) | PRI |
magic_id | int(11) | PRI |
ITEM_MST
Field | Type | Key |
---|---|---|
item_id | int(11) | PRI |
item_name | varchar(64) | |
description | text |
MAGIC_MST
Field | Type | Key |
---|---|---|
magic_id | int(11) | PRI |
magic_name | varchar(64) | |
description | text |
簡単に説明すると、基本的なレベルとか経験値の情報、所持アイテム、覚えた魔法、あとはアイテムと魔法のマスタデータ。とりあえずこれだけ用意しました。
オブジェクトのスキーマ
type User { user_id: ID! user_name: String! level: Int! exp: Int! gold: Int! hp: Int! mp: Int! attack: Int! guard: Int! } type UserItem { user_id: ID! item_id: ID! equip: Int! } type UserMagic { user_id: ID! magic_id: ID! } type Item { item_id: ID! item_name: String! description: String! } type Magic { magic_id: ID! magic_name: String! description: String! }
クエリのスキーマ
type Query { user(user_id: ID): [User] user_item(user_id: ID): [UserItem] user_magic(user_id: ID): [UserMagic] item: [Item] magic: [Magic] }
まずクエリのスキーマを実装
<?php namespace Type; use Model\UserModel; use Model\UserItemModel; use Model\UserMagicModel; use Model\ItemModel; use Model\MagicModel; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; class Query extends ObjectType { private static $singleton; public static function getInstance(): self { return self::$singleton ? self::$singleton : new self(); } public function __construct() { $config = [ 'name' => 'Query', 'fields' => [ 'user' => [ 'type' => User::getInstance(), 'args' => [ 'user_id' => [ 'type' => Type::id(), ], ] ], 'user_item' => [ 'type' => Type::listOf(UserItem::getInstance()), 'args' => [ 'user_id' => [ 'type' => Type::id(), ], ] ], 'user_magic' => [ 'type' => Type::listOf(UserMagic::getInstance()), 'args' => [ 'user_id' => [ 'type' => Type::id(), ], ] ], 'item' => [ 'type' => Type::listOf(Item::getInstance()), ], 'magic' => [ 'type' => Type::listOf(Magic::getInstance()), ], ], 'resolveField' => function($value, $args, $context, ResolveInfo $info) { switch($info->fieldName) { case 'user': return UserModel::getData($args["user_id"]); case 'user_item': return UserItemModel::getList($args["user_id"]); case 'user_magic': return UserMagicModel::getList($args['user_id']); case 'item': return ItemModel::getList(); case 'magic': return MagicModel::getList(); } } ]; parent::__construct($config); } }
次にオブジェクトのスキーマを実装
ドキュメントをしっかり書けばすべて自動生成出来そうなぐらい今は単純です。
<?php namespace Type; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; class User extends ObjectType { private static $singleton; public static function getInstance(): self { return self::$singleton ? self::$singleton : self::$singleton = new self(); } public function __construct() { $config = [ 'name' => 'User', 'fields' => [ 'user_id' => Type::id(), 'user_name' => Type::string(), 'level' => Type::int(), 'exp' => Type::int(), 'gold' => Type::int(), 'hp' => Type::int(), 'mp' => Type::int(), 'attack' => Type::int(), 'guard' => Type::int(), ], 'resolveField' => function ($value, $args, $context, ResolveInfo $info) { $method = 'resolve' . ucfirst($info->fieldName); if (method_exists($this, $method)) { return $this->{$method}($value, $args, $context, $info); } else { return $value->{$info->fieldName}; } } ]; parent::__construct($config); } }
<?php namespace Type; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; class UserItem extends ObjectType { private static $singleton; public static function getInstance(): self { return self::$singleton ? self::$singleton : self::$singleton = new self(); } public function __construct() { $config = [ 'name' => 'UserItem', 'fields' => [ 'user_id' => Type::id(), 'item_id' => Type::id(), 'equip' => Type::int(), ], 'resolveField' => function ($value, $args, $context, ResolveInfo $info) { $method = 'resolve' . ucfirst($info->fieldName); if (method_exists($this, $method)) { return $this->{$method}($value, $args, $context, $info); } else { return $value->{$info->fieldName}; } } ]; parent::__construct($config); } }
<?php namespace Type; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; class UserMagic extends ObjectType { private static $singleton; public static function getInstance(): self { return self::$singleton ? self::$singleton : self::$singleton = new self(); } public function __construct() { $config = [ 'name' => 'UserMagic', 'fields' => [ 'user_id' => Type::id(), 'magic_id' => Type::id(), ], 'resolveField' => function ($value, $args, $context, ResolveInfo $info) { $method = 'resolve' . ucfirst($info->fieldName); if (method_exists($this, $method)) { return $this->{$method}($value, $args, $context, $info); } else { return $value->{$info->fieldName}; } } ]; parent::__construct($config); } }
<?php namespace Type; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; class Item extends ObjectType { private static $singleton; public static function getInstance(): self { return self::$singleton ? self::$singleton : self::$singleton = new self(); } public function __construct() { $config = [ "name" => "Item", "fields" => [ "item_id" => Type::id(), "item_name" => Type::string(), "description" => Type::string(), ], "resolveField" => function ($value, $args, $context, ResolveInfo $info) { $method = "resolve" . ucfirst($info->fieldName); if (method_exists($this, $method)) { return $this->{$method}($value, $args, $context, $info); } else { return $value->{$info->fieldName}; } } ]; parent::__construct($config); } }
<?php namespace Type; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; class Magic extends ObjectType { private static $singleton; public static function getInstance(): self { return self::$singleton ? self::$singleton : self::$singleton = new self(); } public function __construct() { $config = [ "name" => "Magic", "fields" => [ "magic_id" => Type::id(), "magic_name" => Type::string(), "description" => Type::string(), ], "resolveField" => function ($value, $args, $context, ResolveInfo $info) { $method = "resolve" . ucfirst($info->fieldName); if (method_exists($this, $method)) { return $this->{$method}($value, $args, $context, $info); } else { return $value->{$info->fieldName}; } } ]; parent::__construct($config); } }
次にデータクラスを実装
レスポンスに使っている部分です。
<?php namespace Data; class User { public $user_id; public $user_name; public $level; public $exp; public $gold; public $hp; public $mp; public $attack; public $guard; public function __construct($user) { $this->user_id = $user["user_id"]; $this->user_name = $user["user_name"]; $this->level = $user["level"]; $this->exp = $user["exp"]; $this->gold = $user["gold"]; $this->hp = $user["hp"]; $this->mp = $user["mp"]; $this->attack = $user["attack"]; $this->guard = $user["guard"]; } }
<?php namespace Data; class UserItem { public $user_id; public $item_id; public $equip; public function __construct($user_item) { $this->user_id = $user_item["user_id"]; $this->item_id = $user_item["item_id"]; $this->equip = $user_item["equip"]; } }
<?php namespace Data; class UserMagic { public $user_id; public $magic_id; public function __construct($user_magic) { $this->user_id = $user_magic["user_id"]; $this->magic_id = $user_magic["magic_id"]; } }
<?php namespace Data; class Item { public $item_id; public $item_name; public $description; public function __construct($item) { $this->item_id = $item["item_id"]; $this->item_name = $item["item_name"]; $this->description = $item["description"]; } }
<?php namespace Data; class Magic { public $magic_id; public $magic_name; public $description; public function __construct($magic) { $this->magic_id = $magic["magic_id"]; $this->magic_name = $magic["magic_name"]; $this->description = $magic["description"]; } }
次にモデルクラスを実装
データベースからデータを取得してデータクラスに渡すだけです。
<?php namespace Model; use Data\User; class UserModel { private static $dbi; public static function init($dbi) { self::$dbi = $dbi; } public static function getData($user_id) { $sql = "SELECT * FROM USER_DATA WHERE USER_ID = {$user_id}"; $ret = self::$dbi->query($sql); $item = $ret->fetch_assoc(); return new User($item); } }
<?php namespace Model; use Data\UserItem; class UserItemModel { private static $dbi; public static function init($dbi) { self::$dbi = $dbi; } public static function getList($user_id) { $sql = "SELECT * FROM USER_ITEM_DATA WHERE USER_ID = {$user_id}"; $ret = self::$dbi->query($sql); $data = []; while ($item = $ret->fetch_assoc()) { $data[] = new UserItem($item); } return $data; } }
<?php namespace Model; use Data\UserMagic; class UserMagicModel { private static $dbi; public static function init($dbi) { self::$dbi = $dbi; } public static function getList($user_id) { $sql = "SELECT * FROM USER_MAGIC_DATA WHERE USER_ID = {$user_id}"; $ret = self::$dbi->query($sql); $data = []; while ($item = $ret->fetch_assoc()) { $data[] = new UserMagic($item); } return $data; } }
<?php namespace Model; use Data\Item; class ItemModel { private static $dbi; public static function init($dbi) { self::$dbi = $dbi; } public static function getList() { $sql = "SELECT * FROM ITEM_MST"; $ret = self::$dbi->query($sql); $data = []; while ($item = $ret->fetch_assoc()) { $data[] = new Item($item); } return $data; } }
<?php namespace Model; use Data\Magic; class MagicModel { private static $dbi; public static function init($dbi) { self::$dbi = $dbi; } public static function getList() { $sql = "SELECT * FROM MAGIC_MST"; $ret = self::$dbi->query($sql); $data = []; while ($item = $ret->fetch_assoc()) { $data[] = new Magic($item); } return $data; } }
エンドポイントの実装
もっと綺麗に書けるだろと思いつつとりあえず。
<?php require_once '../vendor/autoload.php'; use GraphQL\Server\StandardServer; use GraphQL\Type\Schema; try { $dbi = new \mysqli("host", "user", "password", "database"); $dbi->set_charset("utf8"); \Model\UserModel::init($dbi); \Model\UserItemModel::init($dbi); \Model\UserMagicModel::init($dbi); \Model\ItemModel::init($dbi); \Model\MagicModel::init($dbi); $query = \Type\Query::getInstance(); $schema = new Schema([ 'query' => $query, ]); $server = new StandardServer([ 'schema' => $schema ]); $server->handleRequest(); } catch (\Exception $e) { StandardServer::send500Error($e); }
ではクエリを発行してみる
まずはユーザID1のユーザ情報を取得する
query { user(user_id:"1") { user_id user_name level exp gold hp mp attack guard } }
curl -H "Content-Type: application/json" -X POST -d "{\"query\":\"query{user(user_id:"1"){user_id user_ name level exp gold hp mp attack guard}}\"}" http://xxx.xxx.xx.xx/
{ "data": { "user": { "user_id": "1", "user_name": "もょもと", "level": 48, "exp": 10000, "gold": 3000, "hp": 100, "mp": 30, "attack": 110, "guard": 80 } } }
いくつかまとめて取得してみる
まずはユーザID1のユーザ情報と所持アイテムと覚えた魔法を取得する
query { user(user_id:"1") { user_id user_name level exp gold hp mp attack guard } user_item(user_id:"1") { user_id item_id equip } user_magic(user_id:"1") { user_id magic_id } }
curl -H "Content-Type: application/json" -X POST -d "{\"query\":\"query{user(user_id:"1"){user_id user_ name level exp gold hp mp attack guard} user_item(user_id:"1"){user_id item_id equip} user_magic(user_id:"1"){user_id magic_id}}\"}" http://xxx.xxx.xx.xx/
{ "data": { "user": { "user_id": "1", "user_name": "もょもと", "level": 48, "exp": 10000, "gold": 3000, "hp": 100, "mp": 30, "attack": 110, "guard": 80 }, "user_item": [ { "user_id": "1", "item_id": "100", "equip": 1 } ], "user_magic": [ { "user_id": "1", "magic_id": "100" }, { "user_id": "1", "magic_id": "101" } ] } }
マスタデータを取得する
アイテムマスタと魔法マスタを取得する
query { item { item_id item_name description } magic { magic_id magic_name description } }
curl -H "Content-Type: application/json" -X POST -d "{\"query\":\"query{item{item_id item_name description} magic{magic_id magic_name description}}\"}" http://xxx.xxx.xx.xx/
{ "data": { "item": [ { "item_id": "100", "item_name": "はかいのつるぎ", "description": "" }, { "item_id": "200", "item_name": "あくまのつるぎ", "description": "" }, { "item_id": "300", "item_name": "やくそう", "description": "\t味方1人のHPを30前後回復する" }, { "item_id": "301", "item_name": "どくけしそう", "description": "毒を消し去る" } ], "magic": [ { "magic_id": "100", "magic_name": "ギラ", "description": "敵1体に20前後のダメージ" }, { "magic_id": "101", "magic_name": "ベギラマ", "description": "敵全体に60前後のダメージ" }, { "magic_id": "102", "magic_name": "イオナズン", "description": "敵全体に80前後のダメージ" } ] } }
欲しいデータだけ指定する
ユーザの情報を取得するが、名前とレベルだけで良いとか
query { user(user_id:"1") { user_name level } }
curl -H "Content-Type: application/json" -X POST -d "{\"query\":\"query{user(user_id:"1"){user_ name level}}\"}" http://xxx.xxx.xx.xx/
{ "data": { "user": { "user_name": "もょもと", "level": 48 } } }
所感
色々参考にしながら簡単なものを実装してみました。
スキーマを定義して、クエリとレスポンスの構造の対応関係というのが特徴の一つだとは思いますが、いまいちその利点を実感できていません。
もう少し大きめで実用的なアプリケーションで採用してみるとまた違ったものが見えてくると思いますが、サンプルに毛が生えた程度の実装ではわかるものもわかりません。
今後の予定
さわりの部分だけある程度実装してきましたので、次は簡単なアプリケーション(TODOアプリとか)で一度がっつり作ってみようかなと思いました。
その時にはレベルがあと3ぐらい上がっていると思うのでもう少し論じれるのではないかと思います。