gracetory’s blog

東池袋にある合同会社グレストリのエンジニアブログです

今さらながらGraphQLを試してみた

f:id:grnishi:20200220003242p:plain

はじめに

どうもこんにちわ。サーバサイドエンジニアのgrnishiです。一度は行ってみたい場所はトルクメニスタンのダルヴァザです。ここにあるガスクレーター通称地獄の門が有名です。

ja.wikipedia.org

もしこの世がRPGの世界であるならば、ここにはラスボスがいるか最強の武器が眠っているか。そんな妄想が捗る場所です。

本題

タイトルの通りですが、今さらGraphQLを試してみる事にしました。随分前から知ってはいたのですが、自他ともに認めるREST信者なので食わず嫌いを続けていました。

REST信者と言いながらもほんとの信者なら他を知った上で使う事が大切である事、そもそも自分の知識の幅をもう少し広げようという事でほぼサンプルを写経しながら使ってみました。

GraphQLとは

いちいち説明するのは野暮な事なのでウィキペディアに一任。

ja.wikipedia.org

環境

今回はPHPで実装する事とするので、てっとり早く下記のライブラリを使います。

github.com

今回はGraphQLを試すという事でそれ以外の部分は極力シンプルに作りました。 実際に使う場合はもっと色んな部分で考えなくてはいけないです。

こちらの記事を参考に実装しました。

qiita.com

データベース

今回は簡易な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ぐらい上がっていると思うのでもう少し論じれるのではないかと思います。