2018ワールドカップのGraphQL APIを作りました

ワールドカップ2018の2次予選が始まった後で、私たちは、参加チームについての人々のあらゆる問いに答える簡単な手段を作りたいと思いました。

要約

ワールドカップ2018のために、グラフデータベースNeo4jを使ったGraphQL APIを作成しました。試してみたい人は、ここをクリックしてください。

グラフデータベースNeo4jを使ったGraphQL APIの作成

既に私たちは、ワールドカップについての全てのデータのデータベースを作成して、人々がクエリに使えるようにしましたが、これを、Neo4jのクエリ言語、Cypherを知らない人でもアクセスできるようにしたいと思いました。

GraphQLに助けてもらおう

GraphQLの話をする前に、まず、私たちが作ったNeo4jグラフモデルを見てください。

グラフの中央にWorldCupノードがあって、その周りにモデルの他のパーツが散らばっています。各トーナメントについて1つのWorldCupノードがあります。

ホスト国であるCountry(国)が、HOSTED_BY(開催)という関係でWorldCupに結び付いています。WorldCupノードには、複数のMatch(試合)が属していて、それぞれのCountryは、複数のPlayer(選手)からなるSquad(チーム)を指名し、これが、ワールドカップトーナメントで選手たちを表します。

選手は、チームが参加する試合のそれぞれについて、スタメン(STARTED)かベンチ入り(SUBSTITUTE)のどちらかとしてAppearance(出場)ノードに結び付けられます。Goal(ゴール)が決まると、AppearanceノードがGoalノードに結ばれます(SCORED GOAL)。

GRANDstackスターターキット

Neo4jの説明はこれで十分なので、GraphQLに戻ります。

GRANDstackは、GraphQLReactApolloNeo4j Databaseを、簡単にAPIとアプリを作るための使いやすいバンドルにまとめたものです。GRANDstackはGraphQLのスキーマを使ってGraphQLのクエリを単一のNeo4jクエリに自動的にトランスパイルし、注釈付きスキーマから、全てのクエリ、ミューテーション、フィールドを自動生成することができます。


GRANDstackのロゴ

GRANDstack.ioスターターキットを使って、既に出来上がっているNeo4jデータベースに重ねてGraphQL APIを作りました。

このGraphQL APIは、バックエンドapiとフロントエンドuiの2つの部分からなります。バックエンドはGraphQL APIと、さらにGraphQL Playgroundをサービスします。GraphQL Playgroundは、GraphQLクエリのためのとても素敵なブラウザ兼エディタで、データスキーマ、ドキュメント、オートコンプリートを備えています。

私たちはそれを自分たちのレポジトリにforkしてから、ブランチworldcupへマージして、使えるようにしました。

最初のステップは、GraphQLスキーマの作成です。私たちの思いついたスキーマは下記のとおりで、これは先ほど見たグラフモデルの中のものに密接に対応しています。
最小限のスキーマは次のとおりです:

type Match {
id: Int!
description: String
round: String
date: String @cypher(statement:"RETURN toString({this}.date) AS date")
homeScore: Int
awayScore: Int
homeTeam: Country @relation(name: "HOME_TEAM", direction: "OUT")
awayTeam: Country @relation(name: "AWAY_TEAM", direction: "OUT")
worldCup: WorldCup @relation(name: "CONTAINS_MATCH", direction: "IN")
}
type WorldCup {
year: Int!,
name: String,
host: [Country] @relation(name: "HOSTED_BY", direction: "OUT")
matches(first: Int = 10, offset: Int = 0, round: String): [Match] @relation(name: "CONTAINS_MATCH", direction: "OUT")
}
type Country {
id: ID!
code: String
name: String
hostedWorldCups: [WorldCup] @relation(name: "HOSTED_BY", direction: "IN")
}
type Player {
id: ID!
name: String
dob: String @cypher(statement:"RETURN toString({this}.dob) AS dob")
squads: [Squad] @relation(name: "IN_SQUAD", direction: "OUT")
}
type Query {
matches(id: Int, description: String, first: Int = 10, offset: Int = 0): [Match]
worldcups(year: Int): [WorldCup]
countries(name: String): [WorldCup]
players(name: String, first: Int = 10, offset: Int = 0): [Player]
}
view raw schema.graphql hosted with ❤ by GitHub

GraphQLスキーマの最小限の部分

これを、GRANDstack専用のNeo4jエクステンションをいくつか使ってかなり拡張し、代替マッピングなどを加えました。


― GRANDstackのフルスタックアプリのためのシンプルなスターターキット

スキーマを定義し終わってから、.envファイルを更新して、Neo4jクラウド(https://neo4j.com/cloud/)にホスティングされた私たちのデータベースを指すようにしました。

  1. NEO4J_URI=bolt://c27d992b.databases.neo4j.io
  2. NEO4J_USER=worldcup
  3. NEO4J_PASSWORD=worldcup

これをローカルで動作させるには、yarn && yarn startを実行します。すると、Playgroundがhttp://localhost:4000でローンチされ、クエリを使って遊ぶことができます。

世界でベストの選手は誰か、というクエリを書くこともできます。

query {
players(name:”Lionel Messi”) {
name
dob
allGoals
}
}
view raw messi.graphql hosted with ❤ by GitHub


GraphQL Playgroundの画面

もちろん、その選手について、もっと詳しいことを尋ねることもできます。

query {
players(name:"Lionel Messi") {
name
dob
squads {
year
}
appearances {
match { date }
opposition {name}
type
}
}
}


メッシの詳細についてのクエリの結果

zeit.nowへのデプロイ

これで、デプロイの準備ができました。Node.jsアプリをホスティングしているところなら、どこにでも自分たちのサービスをデプロイできますが、@Will.LyonにZeit Nowを勧められました。これは、アプリのホスティングに簡単に使える素晴らしいサービスで、小さなプロジェクト向けの使いやすい無料プランがあります。

このサービスをインストールしてから、デプロイ先のディレクトリでnowコマンドを実行するだけです。不変のURLにするには、固定の名前でプロジェクトのエイリアスを作成できます。

このGraphQLサーバはhttps://worldcup-2018.now.sh/にデプロイされ、使う準備ができました。このデータセットにかけられるクエリのタイプを見てみましょう。

ポルトガル対モロッコ

この記事を書いている時点で、ポルトガルモロッコと対戦しています。このGraphQLクエリを先ほど定義したプレイグラウンドで実行すると、最新のスコアをチェックできます。

query {
matches(id: 300331511) {
date
homeTeam {name}
awayTeam {name}
homeScore
awayScore
goals {
scorer {
name
}
time
}
}
}


ポルトガル対モロッコの結果

現在ポルトガルが1-0でリードしており、得点者は当然ながらクリスティアーノ・ロナウドでした。

ロナウドってどんな選手?

クリスティアーノについて詳しく知りたいなら、選手についてのクエリも可能です。例えば、下記のクエリでは、彼の生年月日や、これまでのワールドカップでのゴールの回数、さらには、今回のゴールの回数も問い合わせることができます。

query {
players(name:"Cristiano Ronaldo") {
dob
goals(year:2018)
allGoals
}
}
{
"data": {
"players": [
{
"dob": "1985-02-05T00:00:00Z",
"goals": 4,
"allGoals": 7
}
]
}
}

彼はワールドカップ2018で4つのゴールを獲得し、通算で7つになりました。つまり、今回のトーナメントで獲得したゴールの数が、前回までのトーナメントでのゴールの合計を超えたことになります。

1990年のドイツのスコア

ドイツは今回のワールドカップでは好調なスタートを切ったとは言えませんが、1990年ワールドカップ決勝のスコアを懐かしむクエリを書いてみましょう。

query {
worldcups(year: 1990) {
year
matches(round: "Final") {
homeTeam {
name
}
awayTeam {
name
}
winner {
name
}
homeScore
awayScore
}
}
}
{
"data": {
"worldcups": [
{
"year": 1990,
"matches": [
{
"homeTeam": {
"name": "Germany FR"
},
"awayTeam": {
"name": "Argentina"
},
"winner": {
"name": "Germany FR"
},
"homeScore": 1,
"awayScore": 0
}
]
}
]
}
}

1966年の敗北

同僚のマークがどうしても見たいそうなので、1996年の結果のクエリも書いてみました。

query {
worldcups(year: 1966) {
year
matches(round: "Final") {
homeTeam {
name
}
awayTeam {
name
}
winner {
name
}
homeScore
awayScore
goals {
scorer {
name
}
time
}
}
}
}
{
"data": {
"worldcups": [
{
"year": 1966,
"matches": [
{
"homeTeam": {
"name": "England"
},
"awayTeam": {
"name": "Germany FR"
},
"winner": {
"name": "England"
},
"homeScore": 4,
"awayScore": 2,
"goals": [
{
"scorer": {
"name": "Martin Peters"
},
"time": "78'"
},
{
"scorer": {
"name": "Geoff Hurst"
},
"time": "120'"
},
{
"scorer": {
"name": "Geoff Hurst"
},
"time": "18'"
},
{
"scorer": {
"name": "Geoff Hurst"
},
"time": "101'"
},
{
"scorer": {
"name": "Wolfgang Weber"
},
"time": "89'"
},
{
"scorer": {
"name": "Helmut Haller"
},
"time": "12'"
}
]
}
]
}
]
}
}

データを最新に維持する

このデータベースは試合が行われている間、数分ごとにLambdaのジョブで更新されるので、いつクエリを行ってもデータは適切に最新の状態になっているはずです。

ReactのUI

フロントエンドuiは、基本的には単なるReactアプリで、Apollo Clientを使って私たちのAPIにクエリをかけ、結果をコンポーネントにレンダリングします。

import React from "react";
import { Query } from "react-apollo";
import gql from "graphql-tag";
import './UserList.css';
const UserList = () => (
<Query
query={gql`
{
matches(first:10) {
id
description
homeTeam {
name
}
awayTeam {
name
}
homeScore
awayScore
date
}
}
`}
>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return (
<div className="UserList">
<h1>Matches:</h1>
<ul>
{data.matches.map(({date, home_team, away_team, h_score, a_score}, i) => (
<li key={i}>On {date}: {home_team.name} {h_score}-{a_score} {away_team.name}</li>
))}
</ul>
</div>
);
}}
</Query>
);
export default UserList;
view raw UserList.js hosted with ❤ by GitHub

今のReactコードは、ご覧のとおり、とても醜悪で悲惨です。これを課題として残しておきますので、ぜひ、World Cup GraphQL APIを使って、美しいウェブアプリやモバイルアプリを作ってください。


私の(醜悪な)ワールドカップ画面

もちろん、VueやAngularなど、あなたの好きなUIフレームワークを使っても構いません。

アプリは、.envファイルの中のURLに接続します。私たちは、このファイルにローカルのhttp://localhost:4000または自分たちのnow.sh URIを書き込みました。

  1. REACT_APP_GRAPHQL_URI=https://worldcup-2018.now.sh/

ここでも、ただ1つのnowコマンドが、私たちのUIもデプロイします。私たちのケースには必要ありませんが、Zeitでは現在、secret credentialsがサポートされています。

GRANDstackハッカソン

幸いなことに、GRANDstackハッカソン優れたアイデアの収集を今も続けていて、とても素敵な報償も用意されています。

データとモデルの取りまとめに尽力してくれた、同僚のMark Needhamに感謝します。