ワールドカップ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は、GraphQL、React、Apollo、Neo4j 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] | |
} |
GraphQLスキーマの最小限の部分
これを、GRANDstack専用のNeo4jエクステンションをいくつか使ってかなり拡張し、代替マッピングなどを加えました。
― GRANDstackのフルスタックアプリのためのシンプルなスターターキット
スキーマを定義し終わってから、.envファイルを更新して、Neo4jクラウド(https://neo4j.com/cloud/)にホスティングされた私たちのデータベースを指すようにしました。
- NEO4J_URI=bolt://c27d992b.databases.neo4j.io
- NEO4J_USER=worldcup
- NEO4J_PASSWORD=worldcup
これをローカルで動作させるには、yarn && yarn start
を実行します。すると、Playgroundがhttp://localhost:4000でローンチされ、クエリを使って遊ぶことができます。
世界でベストの選手は誰か、というクエリを書くこともできます。
query { | |
players(name:”Lionel Messi”) { | |
name | |
dob | |
allGoals | |
} | |
} |
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; |
今のReactコードは、ご覧のとおり、とても醜悪で悲惨です。これを課題として残しておきますので、ぜひ、World Cup GraphQL APIを使って、美しいウェブアプリやモバイルアプリを作ってください。
私の(醜悪な)ワールドカップ画面
もちろん、VueやAngularなど、あなたの好きなUIフレームワークを使っても構いません。
アプリは、.env
ファイルの中のURLに接続します。私たちは、このファイルにローカルのhttp://localhost:4000
または自分たちのnow.sh URIを書き込みました。
- REACT_APP_GRAPHQL_URI=https://worldcup-2018.now.sh/
ここでも、ただ1つのnow
コマンドが、私たちのUIもデプロイします。私たちのケースには必要ありませんが、Zeitでは現在、secret credentialsがサポートされています。
GRANDstackハッカソン
幸いなことに、GRANDstackハッカソンが優れたアイデアの収集を今も続けていて、とても素敵な報償も用意されています。
データとモデルの取りまとめに尽力してくれた、同僚のMark Needhamに感謝します。