2023.03.31
STAFF BLOG
スタッフブログ
TECHNICAL
テクログ
約3か月ぶりのJGです。
今回はGraphQLを試してみたので、ざっくりと説明したいと思います。
GraphQLの概要
GraphQLは一言で表すとAPIで使用するためのクエリ言語です。従来のRESTと比較して、取得するデータを指定してできるので必要なデータのみ取得できる点と型指定が厳密な点がメリットとして挙げられます。ちなみに型は文字列型、整数型、論理型、オブジェクト型、列挙型及びリスト型等がありますが、ここでの説明は割愛致します。
一方デメリットとしてはRESTと比べてクエリを自由に呼び出せるためにセキュリティの問題、N+1問題(Data Loaderを使えば解決できるらしい)及びクライアント側によるデータのキャッシュが難しい等があります。
GraphQLのエンドポイントは一つで、クエリの種類は下記の3つあります。
・Query→RESTでGETに該当するもので、データを取得する際に使います。
・Mutation→RESTでPOST、PUT及びDELETEに該当するもので、データを追加・変更・削除する際に使います。
・Subscription→イベントが発生した時に実行されるもので、例としてはチャットアプリの通知等に使われます。
GraphQLの開発の流れですが、スキーマにAPIのレスポンスの型等を定義します。そしてResolverにQueryやMutationの処理を書きます。
処理の流れとしては スキーマを元にクライアント側でリクエストを作成し、 GraphQLにリクエストを送ります。 受けっとたリクエストをResolverで処理を行い、レスポンスを返す流れです。
今回のサンプルについて
今回は下記URLのServerの中から、graphql-phpを元に実装したので紹介したいと思います。インターネットで調べた限りではPHPの場合はgraphql-phpとLighthouseの二つの記事が多く、 graphql-phpのほうがStarsが多いので今回はこちらを選択しました。深い意味はないです・・・
ファイル構成
ファイル構成は下記のようになっております。graphqlフォルダには、 graphql-phpをインストールしたファイルで量が膨大なため割愛しており、それ以外のファイルは私が実際にソースを書いたものです。
├──graphqlsample.php
├──graphql/
├──userinfo/
├──data/
│ ├──user.php
├──db/
│ ├──connect_db.php
│ └──user_db.php
├──type/
├──mutation.php
├──query.php
├──user_type.php
└──define/
└──user_type_define.php
今回はxampp環境で開発し、下記のテーブルの内容を取得・追加・編集できるものを作りました。
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`age` int(11) NOT NULL,
`comment` text NOT NULL,
`active_flg` tinyint(1) NOT NULL DEFAULT 1,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
環境構築
PHPとMySQLが動く状態でかつ、composerをインストール済みの状態で、下記を実行します。
composer require webonyx/graphql-php
動作確認をする際にChromeの拡張ツールであるChromeiQLを入れておくと便利ですので、今回はこれを使います。
https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij
今回ですがgraphql-phpの公式にあるソースや幾つかのWEBサイトを参考に実装しています。
https://github.com/webonyx/graphql-php/tree/master/examples/01-blog
https://qiita.com/kazuhei/items/f647a3b60a6e9c1e9c91
スキーマの実装
userinfo/type/user_type.phpにスキーマを書きますが、filedsに返す値の名前と型を定義します。
<?php
namespace GraphQL\userinfo\type;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
class user_type extends ObjectType
{
public function __construct() {
$config = [
'name' => 'user',
'fields' => [
'id' => [
'type' =>Type::id(),
],
'name' => [
'type' => Type::string(),
],
'age' => [
'type' => Type::int()
],
'comment' => [
'type' => Type::string(),
],
'active_flg' => [
'type' => Type::int()
],
],
];
parent::__construct($config);
}
}
userinfo/type/define/user_type_define.phpは型を参照できるようにするために作成します。ソースの書き方はGraphQL内にあるgraphType/Definition/Type.phpを参考に書いてます。
<?php
namespace GraphQL\userinfo\type\define;
require_once(dirname(__FILE__).'/../user_type.php');
use GraphQL\userinfo\type\user_type;
class user_type_define
{
private static $user;
public static function user() {
return static::$user ??= new user_type();
}
}
Resolverの実装
ResolverですがQueryのソースはuserinfo/type/query.phpに書きます。filedsの配列内にQuery名を書き、typeに返す型を指定し、resolveに処理を書きます。
usersは一覧を取得する処理で、userはidでの検索処理になっており、resolve内にある$argsがリクエストで送ったパラメータになります。
<?php
namespace GraphQL\userinfo\type;
require_once(dirname(__FILE__).'/../db/user_db.php');
require_once(dirname(__FILE__).'/define/user_type_define.php');
require_once(dirname(__FILE__).'/../data/user.php');
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\userinfo\db\user_db;
use GraphQL\userinfo\type\define\user_type_define;
use GraphQL\userinfo\data\user;
class query extends ObjectType
{
public function __construct() {
$config = [
'name' => 'Query',
'fields' => [
'users' => [
'type' => Type::listOf(user_type_define::user()),
'resolve' => function() {
$user_db = new user_db();
$ret = $user_db->get_all();
if (empty($ret))
{
return [];
}
foreach ($ret as $val) {
$arr[$val['id']] = new user($val);
}
return $arr;
}
],
'user' => [
'type' => user_type_define::user(),
'args' => [
'id' => [
'type' => Type::id(),
],
],
'resolve' => function($value, $args, $context, ResolveInfo $info) {
$user_db = new user_db();
$ret = $user_db->get_by_id($args['id']);
if (empty($ret))
{
return [];
}
return new user($ret);
}
],
],
];
parent::__construct($config);
}
}
Mutationも書き方はQueryと同じです。addが新規登録の処理で、editが更新処理です。
<?php
namespace GraphQL\userinfo\type;
require_once(dirname(__FILE__).'/../db/user_db.php');
require_once(dirname(__FILE__).'/define/user_type_define.php');
require_once(dirname(__FILE__).'/../data/user.php');
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\userinfo\db\user_db;
use GraphQL\userinfo\type\define\user_type_define;
use GraphQL\userinfo\data\user;
class mutation extends ObjectType
{
public function __construct() {
$config = [
'name' => 'Mutation',
'fields' => [
'add' => [
'type' => user_type_define::user(),
'args' => [
'name' => [
'type' => Type::string(),
],
'age' => [
'type' => Type::int(),
],
'comment' => [
'type' => Type::string(),
],
'active_flg' => [
'type' => Type::int(),
],
],
'resolve' => function($value, $args, $context, ResolveInfo $info) {
$user_db = new user_db();
$ret = $user_db->add($args['name'], $args['age'], $args['comment'], $args['active_flg']);
if ($ret === false)
{
return [];
}
$arr = [
'id' => $ret,
'name' => $args['name'],
'age' => $args['age'],
'comment' => $args['comment'],
'active_flg' => $args['active_flg']
];
return new user($arr);
}
],
'edit' => [
'type' => user_type_define::user(),
'args' => [
'id' => [
'type' => Type::id(),
],
'name' => [
'type' => Type::string(),
],
'age' => [
'type' => Type::int(),
],
'comment' => [
'type' => Type::string(),
],
'active_flg' => [
'type' => Type::int(),
],
],
'resolve' => function($value, $args, $context, ResolveInfo $info) {
$user_db = new user_db();
$ret = $user_db->edit($args['id'], $args['name'] ?? null, $args['age'] ?? null, $args['comment'] ?? null, $args['active_flg'] ?? null);
if ($ret === false)
{
return [];
}
$ret = $user_db->get_by_id($args['id']);
return new user($ret);
}
],
],
];
parent::__construct($config);
}
}
データクラスの実装
userinfo/data/user.phpはResolverで処理を行った後に、GraphQLのレスポンスの形式と同じにするために必要になります。
<?php
namespace GraphQL\userinfo\data;
use GraphQL\Utils\Utils;
class user {
public int $id;
public string $name;
public int $age;
public string $comment;
public int $active_flg;
const KEY_LIST = ['id', 'name', 'age', 'comment', 'active_flg'];
public function __construct(array $data) {
foreach ($data as $key => $val) {
if (!in_array($key, self::KEY_LIST))
{
unset($data[$key]);
}
}
Utils::assign($this, $data);
}
}
データベースの処理の実装
userinfo/db/connect_db.phpにDBの接続処理が書いてあり、userinfo/db/user_db.phpにDBの取得・追加・更新処理が書いてあります。
<?php
namespace GraphQL\userinfo\db;
class connect_db {
protected $dbh = '';
public function __construct() {
$dsn = '';
$user = '';
$password = '';
try {
$this->dbh = new \PDO($dsn, $user, $password);
} catch (\PDOException $e) {
echo '接続失敗';
exit();
}
}
}
<?php
namespace GraphQL\userinfo\db;
require_once(dirname(__FILE__).'/connect_db.php');
use GraphQL\userinfo\db\connect_db;
class user_db extends connect_db {
public function __construct() {
parent::__construct();
}
public function get_all() {
$sql = 'SELECT * FROM user';
$prepare = $this->dbh->prepare($sql);
$prepare->execute();
$ret = $prepare->fetchAll(\PDO::FETCH_ASSOC);
return $ret;
}
public function get_by_id($id) {
$sql = 'SELECT * FROM user WHERE id = :id';
$prepare = $this->dbh->prepare($sql);
$prepare->bindValue(':id', $id, \PDO::PARAM_INT);
$prepare->execute();
$ret = $prepare->fetchAll(\PDO::FETCH_ASSOC);
if (empty($ret))
{
return [];
}
return $ret[0];
}
public function add($name, $age, $comment, $active_flg) {
$sql = 'INSERT INTO user (name, age, comment, active_flg, created_at, updated_at) VALUES(:name, :age, :comment, :active_flg, :created_at, :updated_at)';
$prepare = $this->dbh->prepare($sql);
$prepare->bindValue(':name', $name, \PDO::PARAM_STR);
$prepare->bindValue(':age', $age, \PDO::PARAM_INT);
$prepare->bindValue(':comment', $comment, \PDO::PARAM_STR);
$prepare->bindValue(':active_flg', $active_flg, \PDO::PARAM_INT);
$now = date('Y-m-d H:i:s');
$prepare->bindValue(':created_at', $now, \PDO::PARAM_STR);
$prepare->bindValue(':updated_at', $now, \PDO::PARAM_STR);
$ret = $prepare->execute();
if ($ret !== true) {
return false;
}
$id = $this->dbh->lastInsertId();
if ($id === false) {
return false;
}
return (int)$id;
}
public function edit($id, $name = null, $age = null, $comment = null, $active_flg = null) {
$update_sql = '';
if (isset($name)) {
$update_sql .= 'name = :name,';
}
if (isset($age)) {
$update_sql .= 'age = :age,';
}
if (isset($comment)) {
$update_sql .= 'comment = :comment,';
}
if (isset($active_flg)) {
$update_sql .= 'active_flg = :active_flg,';
}
if (empty($update_sql)) {
return false;
}
$update_sql .= 'updated_at = :updated_at';
$sql = 'UPDATE user SET '.$update_sql.' WHERE id = :id';
$prepare = $this->dbh->prepare($sql);
$prepare->bindValue(':id', $id, \PDO::PARAM_INT);
if (isset($name)) {
$prepare->bindValue(':name', $name, \PDO::PARAM_STR);
}
if (isset($age)) {
$prepare->bindValue(':age', $age, \PDO::PARAM_INT);
}
if (isset($comment)) {
$prepare->bindValue(':comment', $comment, \PDO::PARAM_STR);
}
if (isset($active_flg)) {
$prepare->bindValue(':active_flg', $active_flg, \PDO::PARAM_INT);
}
$now = date('Y-m-d H:i:s');
$prepare->bindValue(':updated_at', $now, \PDO::PARAM_STR);
$ret = $prepare->execute();
if ($ret !== true) {
return false;
}
return true;
}
}
メイン処理の実装
graphqlsample.phpにメイン処理を書きます。
<?php
require_once(dirname(__FILE__).'/graphql/vendor/autoload.php');
require_once(dirname(__FILE__).'/userinfo/type/query.php');
require_once(dirname(__FILE__).'/userinfo/type/mutation.php');
use GraphQL\GraphQL;
use GraphQL\Type\Schema;
use GraphQL\Type\SchemaConfig;
use GraphQL\Server\StandardServer;
use GraphQL\userinfo\type\query;
use GraphQL\userinfo\type\mutation;
$schema = new Schema(
(new SchemaConfig())
->setQuery(new query())
->setMutation(new mutation())
);
$server = new StandardServer([
'schema' => $schema,
]);
$server->handleRequest();
実行方法
下記のコマンドを実行した状態で、ChromeiQLのエンドポイントに「http://localhost:8080」を入力します。
php -S localhost:8080 graphqlsample.php
まずQueryで一覧を指定する場合ですが、下記のようにリクエストを指定すると、DBに登録済みの全データがレスポンスとして返されます。下記のリクエストからnameを削除した状態で実行すると、nameはレスポンスに含まれなくなります。
//リクエスト
{
users {
id
name
age
comment
active_flg
}
}
//レスポンス
{
"data": {
"users": [
{
"id": "1",
"name": "鈴木",
"age": 23,
"comment": "鈴木のコメント",
"active_flg": 1
},
{
"id": "2",
"name": "佐藤",
"age": 35,
"comment": "佐藤のコメントです。",
"active_flg": 1
}
]
}
}
次にQueryで下記のようにリクエストを指定すると、指定したIDのみがレスポンスとして返されます。
//リクエスト
{
user(id: "2") {
id
name
age
comment
active_flg
}
}
//レスポンス
{
"data": {
"user": {
"id": "2",
"name": "佐藤",
"age": 35,
"comment": "佐藤のコメントです。",
"active_flg": 1
}
}
}
次にMutationの新規登録ですが、下記のようにリクエストを指定するとDBに登録され、{}に指定したものが レスポンスとして返されます。
//リクエスト
mutation {
add(
name: "伊藤"
age: 37
comment: "伊藤のテストです。"
active_flg: 1
) {
id
name
age
comment
active_flg
}
}
//レスポンス
{
"data": {
"add": {
"id": "3",
"name": "伊藤",
"age": 37,
"comment": "伊藤のテストです。",
"active_flg": 1
}
}
}
最後にMutationの変更ですが、下記のようにリクエストを指定するとDBのデータが変更され、{}に指定したものがレスポンスとして返されます。
//リクエスト
mutation {
edit(
id: 3
name: "伊藤変更テスト"
age: 42
comment: "伊藤変更テストです。"
active_flg: 0
) {
id
name
age
comment
active_flg
}
}
//レスポンス
{
"data": {
"edit": {
"id": "3",
"name": "伊藤変更テスト",
"age": 42,
"comment": "伊藤変更テストです。",
"active_flg": 0
}
}
}
最後に
長くなりましたが、本当にざっくりとGraphQLについて説明しました。ざっくりとした説明だったので、もし興味を持った方は細かい箇所については自分で調べてみるのが良い勉強になると思います。
試してみた感想はスキーマにAPIのリクエストの型等の仕様に関する部分がまとまっているので、そこを見れば一目瞭然です。そのため業務で使う際にはRESTの時はAPI仕様書の作成が必須でしたが、GraphQLならそこまで詳細に書く必要はないと感じました。
またクライアント側で必要なレスポンスを指定できるので、自分が昔ソーシャルゲームやWEBアップのバックエンドのRESTのAPI開発では似たような処理のAPIを作ってると感じた時もありましたが、それもなくなりそうな気がしました。バックエンドの作業量も減りそうです。
新しい技術を書籍やネットの記事を読むだけなく、実際に動くレベルにするには良い刺激になり、一番の勉強になると改めて実感しました。今回はバリデーション処理がなく、N+1問題についても触れてないので次回はその辺りを紹介できるように精進してまいります。