初心者のプログラミング日記

プログラミング初心者の日記

プログラミングに関することを書いていきます。

GraphQLを使ってみた

実行環境

パーケッジ名 バージョン
Laravel 7.15.0
React 16.2.0
GraphQL 15.1.0
nuwave/lighthouse 4.14
graphql-tag 2.10.3
react-apollo 3.1.5
apollo-boost 0.4.9
react-router-dom 5.2.0


本当は@apollo/react-hooksを使ってhookをやりたかったのですがReactのバージョンが対応していないとエラーがでたためできていません。

とりあえず、LaravelでQraphQLを使うための下準備をします。
lighthouseを使用しています。
lighthouseについては下記URLからどうぞ
https://lighthouse-php.com/2.6/getting-started/installation.html#install-via-composer

composer require nuwave/lighthouse
php artisan vendor:publish --provider="Nuwave\Lighthouse\Providers\LighthouseServiceProvider" --tag=schema

composer require mll-lab/laravel-graphql-playground
php artisan vendor:publish --provider="MLL\GraphQLPlayground\GraphQLPlaygroundServiceProvider"

laravel-graphql-playgroundはなくてもいいのですが、クエリの確認に便利なので入れています。
これでプロジェクトの直下にgraphqlというフォルダができます。その中のschema.graphqlにスキーマを書いていきます。

http://127.0.0.1:8000/graphql-playgroundでクエリの確認ができます。

schema.graphql

"A datetime string with format `Y-m-d H:i:s`, e.g. `2018-01-01 13:00:00`."
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")

"A date string with format `Y-m-d`, e.g. `2011-05-23`."
scalar Date @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Date")

type Query {
    users:[User!]! @all(model: "App\\User")
    user(id: ID @eq): User @find(model: "App\\User")
    usercount:Int @count(model: "App\\User")
}

type Mutation {
    createUser(
        name: String @rules(apply: ["required"],
                            messages: { required: "名前は必須です" }
                            )
        email: String @rules(apply: ["required","email","unique:users,email"],
                            messages: { unique: "このメールアドレスは使われています。" ,required: "メールアドレスは必須です"},
                            )
        password: String @rules(apply: ["required"],
                            messages: { required: "パスワードは必須です" }
                            )
    ): User @create(model: "App\\User")

    updateUser(
        id:ID!
        name: String  @rules(apply: ["required"])
        password: String  @rules(apply: ["required"])
    ): User @update(model: "App\\User")

    deleteUser(
        id:ID! @rules(apply: ["required"])
    ): User @delete(model: "App\\User")
}

type User {
    id: ID!
    name: String!
    email: String!
    password: String!
    created_at: DateTime!
    updated_at: DateTime!
}

ここはこんな感じで、GraphQLを使ったことがある人ならわかると思います。

App.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';
import Users from './Users';
import User from './User';

const client = new ApolloClient({
    uri: 'http://127.0.0.1:8000/graphql',
});

function App() {
    return (
        <ApolloProvider client={client}>
            <Router>
                <Switch>
                    <Route exact path="/" component={Users} />
                    <Route path="/user/:id" component={User} />
                </Switch>
            </Router>
        </ApolloProvider>
    )
}

export default App;

ReactDOM.render(<App />, document.getElementById('example'));

clientでエンドポイントを指定。
ApolloProviderでルートを囲むことですべてのルートでQueryやMutationを使うことができます。

Users.js

import React, { useState, useEffect, useRef } from 'react';
import gql from 'graphql-tag';
import { Query, Mutation } from 'react-apollo';
import { Link } from "react-router-dom";
import './style/Users.css';

//playground上で試したクエリ
const getUsers = gql`
query {
    usercount
    users{
      id
      name
    }
  }

`;

const CREAT_USER = gql`
mutation CreateUser($name: String $email: String $password: String){
    createUser(name:$name, email:$email, password:$password){
        id
        name
    }
}
`;

const DELETE_USER=gql`
mutation DeleteUser($id:ID!){
    deleteUser(id: $id){
        id
    }
}
`;

function Users() {
    const [open, setOpen] = useState(false);
    const [name, setName] = useState("");
    const [email, setEMail] = useState("");
    const [password, setPassword] = useState("");

    function OpenModal() {
        setOpen(!open)
        if (open == true) {
            document.body.removeAttribute('style', 'overflow: hidden;');
        } else {
            document.body.setAttribute('style', 'overflow: hidden;');
        }
    }

    function onClick(e) {
        e.stopPropagation()
    }

    function Name(e) {
        setName(e.target.value);
    }

    function EMail(e) {
        setEMail(e.target.value);
    }

    function Password(e) {
        setPassword(e.target.value);
    }

    return (
        <div>
            <h1>ユーザー一覧</h1>
            <button onClick={OpenModal}>ユーザー登録</button>
            {open && (
                <div className="modal" onClick={OpenModal}>
                    <div className="creat" onClick={onClick}>
                        <h1>新規登録</h1>
                        <Mutation mutation={CREAT_USER} >
                            {(createUser, { data, loading, error }) => {

                                return (
                                    <div>
                                        <form onSubmit={e => {
                                            e.preventDefault();
                                            e.stopPropagation()
                                            createUser({
                                                variables: { name, email, password },
                                                //画面の自働更新
                                                //追加、削除の場合手動で更新をする必要がある
                                                refetchQueries: [{ query: getUsers }]
                                            })
                                        }}>
                                            <p>名前</p>
                                            {error && <p style={{ color: "red" }}>{error.graphQLErrors[0].extensions.validation.name}</p>}
                                            <input onChange={Name}></input>
                                            <p>メールアドレス</p>
                                            {error && <p style={{ color: "red" }}>{error.graphQLErrors[0].extensions.validation.email}</p>}
                                            <input onChange={EMail} type="email"></input>
                                            <p>パスワード</p>
                                            {error && <p style={{ color: "red" }}>{error.graphQLErrors[0].extensions.validation.password}</p>}
                                            <input onChange={Password} type="password"></input>
                                            <button type="submit">登録</button>
                                        </form>
                                        {error && <p>{error.message}</p>}

                                    </div>
                                )
                            }}
                        </Mutation>
                    </div>
                </div>
            )}
            <Query query={getUsers}>
                {({ data, loading, error }) => {
                    // データ取得中はローディングを表示
                    if (loading) return <p>loading...</p>;

                    if (error) return (
                        <div>
                            <p>{error.toString()}</p>
                            <p>{error.message}</p>
                        </div>
                    );

                    const { users } = data;

                    return (
                        <div>
                            <h2>{data.usercount}人</h2>
                            <ul>
                                {users.map(user => (
                                    <li key={user.id}>
                                        <Link to={`/user/${user.id}`}>
                                            <span>{user.name}</span>
                                        </Link>
                                        <Mutation mutation={DELETE_USER}>
                                        {(DeleteUser, { data, loading, error }) => {
                                            return(
                                            <button onClick={e=>{
                                                e.preventDefault();
                                                DeleteUser({
                                                    variables: { id:user.id },
                                                    refetchQueries: [{ query: getUsers }]
                                                });
                                            }}>
                                            削除
                                            </button>)
                                        }}
                                        </Mutation>
                                    </li>
                                ))}
                            </ul>
                        </div>
                    );
                }}
            </Query>
        </div >
    );
}

export default Users;
.modal{
    background-color:rgba(207,214,227,0.3);
    position: absolute;
    top:0;
    left:0;
    width: 100%;
    height: 100%;
    text-align: center;
}

.creat{
    background-color: white;
    width: 50%;
    margin: 0 auto;
}

.creat form{
    width: 50%;
    margin: 0 auto;
}
form p{
    margin-bottom: 0;
    padding-bottom: 0;
    text-align: left;
}
form input{
    width: 90%;
    height: 20px;
    display: block;
    margin-right: auto
}
form button{
    width: 70px;
    height: 50px;
    font-size: 20px;
}

ここではユーザー一覧と新規登録を行っています。

queryやmutationを使う場合はQueryコンポーネントやMutationコンポーネントを作りその中で実行します。
refetchQueriesを使うことで手動でブラウザをリロードすることなく自働で更新されるので便利です

他にも自働更新の方法があるので気になる人は下記URLを参考にしてください。
http://tomoima525.hatenablog.com/entry/2019/08/06/164506
https://qiita.com/yasuhiro-yamada/items/3bcb36504fb5aaa0416c

エラー時はgraphQLErrorsでエラー内容を受け取ることができます。
参考URL
https://www.apollographql.com/docs/react/data/error-handling/#network-errors
https://github.com/vuejs/vue-apollo/issues/498

一応、error.toString()とerror.messageでエラーメッセージを受け取れますが、利用者側からしたら意味不明なエラーになるので、使わない方がいいです。

User.js

import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import gql from 'graphql-tag';
import { Query, Mutation } from 'react-apollo';
import './style/User.css';


const getUser = gql`
query USER($id: ID!){
    user(id: $id){
        id
        name
        email
        password
    }
}

`;

const UPDATE_USER = gql`
mutation UpdateUser($id: ID! $name: String  $password: String){
    updateUser(id:$id, name:$name, password:$password){
        name
        password
    }
}
`;

function User() {

    const { id } = useParams();
    const [open, setOpen] = useState(false);
    const [name, setName] = useState("");
    const [password, setPassword] = useState("");



    function OpenModal() {
        setOpen(!open)
        if (open == true) {
            document.body.removeAttribute('style', 'overflow: hidden;');
        } else {
            document.body.setAttribute('style', 'overflow: hidden;');
        }
    }

    function onClick(e) {
        e.stopPropagation()
    }

    function Name(e) {
        setName(e.target.value);
    }

    function Password(e) {
        setPassword(e.target.value);
    }

    return (
        <div>
            <h1>ユーザー情報</h1>
            <Query query={getUser} variables={{ id }}>
                {({ loading, error, data }) => {
                    if (loading) return <p>loading...</p>;

                    if (error) return (
                        <div>
                            <p>{error.toString()}</p>
                            <p>{error.message}</p>
                        </div>
                    );

                    const { user } = data;

                    return (
                        <div className="user">
                            <p><label>ID</label>:{user.id}</p>
                            <p><label>名前</label>:{user.name}</p>
                            <p><label>メールアドレス</label>:{user.email}</p>
                            <p><label>パスワード</label>:{user.password}</p>
                        </div>
                    );
                }}

            </Query>
            <button onClick={OpenModal}>編集</button>
            {open && (
                <div className="modal" onClick={OpenModal}>
                    <div class="update" onClick={onClick}>
                        <h1>プロフィール編集</h1>
                        <Mutation mutation={UPDATE_USER}>
                            {(updateUser, { data, loading, error }) => {


                                return (
                                    <div>
                                        <form onSubmit={e => {
                                            e.preventDefault();
                                            e.stopPropagation()
                                            updateUser({
                                                variables: { id, name, password },
                                                refetchQueries: [{ query: getUser, variables: {id} }]
                                            })
                                        }}>
                                            <p>名前</p>
                                            {error && <p style={{ color: "red" }}>{error.graphQLErrors[0].extensions.validation.name}</p>}
                                            <input onChange={Name}></input>
                                            <p>パスワード</p>
                                            {error && <p style={{ color: "red" }}>{error.graphQLErrors[0].extensions.validation.password}</p>}
                                            <input onChange={Password} type="password"></input>
                                            <button type="submit">編集</button>
                                        </form>
                                        {error && <p>{error.message}</p>}

                                    </div>
                                )
                            }}

                        </Mutation>
                    </div>
                </div>
            )}
        </div>
    )
}

export default User;

URLからuseParamsでパラメータを受け取りそれを元にgetUserでDBから情報を取ってきます。
今回はユーザー全員の情報ではなく、一人の情報のみなので以下のように
refetchQueries: [{ query: getUser, variables: {id} }]
variablesに探すユーザーのidを渡します。

まとめ

とりあえず、登録、更新、削除と一通り試してみました。
更新、削除をする時にはPRIMARY KEY(主キー)が必要でした。自分はデフォルトのままなので、idで行いました。
リレーションも試したかったのですが、今回はここまでにしておきます。

QraphQLはRESTと比べて、コードが読みやすいです。