メインコンテンツへスキップ

The Graph: Web3データクエリ問題の解決

Solidity
スマートコントラクト
クエリ
the graph
react
中級
Markus Waas
2020年9月6日
14 分の読書

今回は、The Graphを詳しく見ていきます。これは昨年、dappsを開発するための標準スタックの一部となりました。 まずは、従来のやり方から見ていきましょう...

The Graphを使用しない場合...

それでは、説明のために簡単な例を見ていきましょう。 私たちは皆ゲームが好きなので、ユーザーがベットするシンプルなゲームを想像してみましょう:

1pragma solidity 0.7.1;
2
3contract Game {
4 uint256 totalGamesPlayerWon = 0;
5 uint256 totalGamesPlayerLost = 0;
6 event BetPlaced(address player, uint256 value, bool hasWon);
7
8 function placeBet() external payable {
9 bool hasWon = evaluateBetForPlayer(msg.sender);
10
11 if (hasWon) {
12 (bool success, ) = msg.sender.call{ value: msg.value * 2 }('');
13 require(success, "送金に失敗しました");
14 totalGamesPlayerWon++;
15 } else {
16 totalGamesPlayerLost++;
17 }
18
19 emit BetPlaced(msg.sender, msg.value, hasWon);
20 }
21}
すべて表示

さて、私たちのdappで合計ベット数、勝敗の合計ゲーム数を表示し、誰かが再度プレイするたびにそれを更新したいとしましょう。 アプローチは次のようになります:

  1. totalGamesPlayerWonを取得します。
  2. totalGamesPlayerLostを取得します。
  3. BetPlacedイベントをサブスクライブします。

右に示すようにWeb3のイベントopens in a new tabをリッスンできますが、かなりの数のケースを処理する必要があります。

1GameContract.events.BetPlaced({
2 fromBlock: 0
3}, function(error, event) { console.log(event); })
4.on('data', function(event) {
5 // イベントが発火
6})
7.on('changed', function(event) {
8 // イベントが再度削除された
9})
10.on('error', function(error, receipt) {
11 // トランザクションが拒否された
12});
すべて表示

この単純な例では、これでもまだある程度は問題ありません。 しかし、今度は現在のプレイヤーの勝敗ベット額のみを表示したいとしましょう。 残念ながら、それらの値を保存して取得する新しいコントラクトをデプロイするしかありません。 そして、はるかに複雑なスマートコントラクトとdappを想像してみてください。事態はすぐに厄介になる可能性があります。

単純にクエリはできない

これが最適ではない理由は、以下の通りです:

  • すでにデプロイ済みのコントラクトでは機能しない。
  • それらの値を保存するための追加のガス代。
  • イーサリアムノードのデータを取得するために、別の呼び出しが必要になる。

これでは不十分

では、より良い解決策を見ていきましょう。

GraphQLのご紹介

まず、GraphQLについてお話ししましょう。これは元々Facebookによって設計・実装されたものです。 従来のREST APIモデルには馴染みがあるかもしれません。 代わりに、欲しいデータを正確に取得するためのクエリを書けると想像してみてください。

GraphQL API 対 REST API

この2つの画像は、GraphQLの本質をよく捉えています。 右側のクエリでは、欲しいデータを正確に定義できます。そのため、1回のリクエストで必要なものだけをすべて取得できます。 GraphQLサーバーは必要なすべてのデータの取得を処理するため、フロントエンドの利用者側にとっては非常に使いやすくなっています。 ご興味があれば、こちらの分かりやすい説明opens in a new tabで、サーバーがクエリをどのように処理するかを正確に知ることができます。

この知識をもとに、いよいよブロックチェーンとThe Graphの世界に飛び込んでみましょう。

The Graphとは?

ブロックチェーンは分散型データベースですが、通常の場合とは対照的に、このデータベースにはクエリ言語がありません。 データを取得するためのソリューションは、手間がかかるか、あるいはまったく不可能です。 The Graphは、ブロックチェーンのデータをインデックス化し、クエリを実行するための分散型プロトコルです。 お察しの通り、クエリ言語としてGraphQLを使用しています。

The Graph

何かを理解するには例を見るのが一番です。そこで、私たちのGameContractの例でThe Graphを使ってみましょう。

サブグラフの作成方法

データをインデックス化する方法の定義は、サブグラフと呼ばれます。 それには3つのコンポーネントが必要です:

  1. マニフェスト (subgraph.yaml)
  2. スキーマ (schema.graphql)
  3. マッピング (mapping.ts)

マニフェスト (subgraph.yaml)

マニフェストは設定ファイルで、以下を定義します:

  • どのスマートコントラクトをインデックス化するか (アドレス、ネットワーク、ABIなど)
  • どのイベントをリッスンするか
  • 関数呼び出しやブロックなど、他にリッスンするもの
  • 呼び出されるマッピング関数 (下記のmapping.tsを参照)

ここでは複数のコントラクトとハンドラを定義できます。 典型的なセットアップでは、Hardhatプロジェクト内に独自のレポジトリを持つサブグラフフォルダを置きます。 そうすれば、ABIを簡単に参照できます。

利便性のために、Mustacheのようなテンプレートツールを使ってもよいでしょう。 次に、subgraph.template.yamlを作成し、最新のデプロイに基づいてアドレスを挿入します。 より高度なセットアップ例については、例えばAaveサブグラフリポジトリopens in a new tabを参照してください。

完全なドキュメントはこちらopens in a new tabでご覧いただけます。

1specVersion: 0.0.1
2description: イーサリアム上でのベット
3repository: - GitHubリンク -
4schema:
5 file: ./schema.graphql
6dataSources:
7 - kind: ethereum/contract
8 name: GameContract
9 network: mainnet
10 source:
11 address: '0x2E6454...cf77eC'
12 abi: GameContract
13 startBlock: 6175244
14 mapping:
15 kind: ethereum/events
16 apiVersion: 0.0.1
17 language: wasm/assemblyscript
18 entities:
19 - GameContract
20 abis:
21 - name: GameContract
22 file: ../build/contracts/GameContract.json
23 eventHandlers:
24 - event: PlacedBet(address,uint256,bool)
25 handler: handleNewBet
26 file: ./src/mapping.ts
すべて表示

スキーマ (schema.graphql)

スキーマはGraphQLのデータ定義です。 これにより、どのエンティティが存在し、その型は何かを定義できます。 The Graphでサポートされている型は次の通りです

  • バイト
  • ID
  • String
  • Boolean
  • Int
  • BigInt
  • BigDecimal

エンティティを型として使用して、リレーションシップを定義することもできます。 この例では、プレイヤーからベットへの1対多のリレーションシップを定義します。 ! は値が空にできないことを意味します。 完全なドキュメントはこちらopens in a new tabでご覧いただけます。

1type Bet @entity {
2 id: ID!
3 player: Player!
4 playerHasWon: Boolean!
5 time: Int!
6}
7
8type Player @entity {
9 id: ID!
10 totalPlayedCount: Int
11 hasWonCount: Int
12 hasLostCount: Int
13 bets: [Bet]!
14}
すべて表示

マッピング (mapping.ts)

The Graphのマッピングファイルは、受信したイベントをエンティティに変換する関数を定義します。 これはTypescriptのサブセットであるAssemblyScriptで書かれています。 これは、マッピングの実行をより効率的でポータブルにするために、WASM (WebAssembly) にコンパイルできることを意味します。

subgraph.yamlファイルで名付けられた各関数を定義する必要があります。この例では、handleNewBetの1つだけが必要です。 まず、送信者のアドレスをIDとしてPlayerエンティティをロードしようとします。 それが存在しない場合は、新しいエンティティを作成し、初期値を入力します。

次に、新しいBetエンティティを作成します。 このIDは event.transaction.hash.toHex() + "-" + event.logIndex.toString() となり、常に一意の値を保証します。 スマートコントラクトを介して1つのトランザクションで誰かがplaceBet関数を複数回呼び出す可能性があるため、ハッシュのみでは不十分です。

最後に、すべてのデータでPlayerエンティティを更新できます。 配列に直接プッシュすることはできませんが、ここに示すように更新する必要があります。 ベットを参照するためにIDを使用します。 そして、エンティティを保存するためには最後に .save() が必要です。

完全なドキュメントはこちらopens in a new tabでご覧いただけます。 マッピングファイルにログ出力を追加することもできます。こちらopens in a new tabを参照してください。

1import { Bet, Player } from "../generated/schema"
2import { PlacedBet } from "../generated/GameContract/GameContract"
3
4export function handleNewBet(event: PlacedBet): void {
5 let player = Player.load(event.transaction.from.toHex())
6
7 if (player == null) {
8 // まだ存在しない場合は作成
9 player = new Player(event.transaction.from.toHex())
10 player.bets = new Array<string>(0)
11 player.totalPlayedCount = 0
12 player.hasWonCount = 0
13 player.hasLostCount = 0
14 }
15
16 let bet = new Bet(
17 event.transaction.hash.toHex() + "-" + event.logIndex.toString()
18 )
19 bet.player = player.id
20 bet.playerHasWon = event.params.hasWon
21 bet.time = event.block.timestamp
22 bet.save()
23
24 player.totalPlayedCount++
25 if (event.params.hasWon) {
26 player.hasWonCount++
27 } else {
28 player.hasLostCount++
29 }
30
31 // このように配列を更新
32 let bets = player.bets
33 bets.push(bet.id)
34 player.bets = bets
35
36 player.save()
37}
すべて表示

フロントエンドでの使用

Apollo Boostのようなものを使用すると、React dapp (またはApollo-Vue) にThe Graphを簡単に統合できます。 特にReactフックとApolloを使用する場合、データの取得はコンポーネントに単一のGraphQLクエリを記述するのと同じくらい簡単です。 典型的な設定は次のようになります。

1// See all subgraphs: https://thegraph.com/explorer/
2const client = new ApolloClient({
3 uri: "{{ subgraphUrl }}",
4})
5
6ReactDOM.render(
7 <ApolloProvider client={client}>
8 <App />
9 </ApolloProvider>,
10 document.getElementById("root")
11)
すべて表示

そして今、例えばこのようなクエリを書くことができます。 これにより、以下が取得されます

  • 現在のユーザーが勝った回数
  • 現在のユーザーが負けた回数
  • 彼の過去のすべてのベットのタイムスタンプのリスト

すべてGraphQLサーバーへの単一のリクエストで完了します。

1const myGraphQlQuery = gql`
2 players(where: { id: $currentUser }) {
3 totalPlayedCount
4 hasWonCount
5 hasLostCount
6 bets {
7 time
8 }
9 }
10`
11
12const { loading, error, data } = useQuery(myGraphQlQuery)
13
14React.useEffect(() => {
15 if (!loading && !error && data) {
16 console.log({ data })
17 }
18}, [loading, error, data])
すべて表示

魔法

しかし、パズルの最後のピース、つまりサーバーが欠けています。 自分で実行するか、ホストされたサービスを使用することができます。

The Graphサーバー

Graph Explorer: ホストされたサービス

最も簡単な方法は、ホストされたサービスを使用することです。 サブグラフをデプロイするには、こちらopens in a new tabの指示に従ってください。 多くのプロジェクトでは、実際にエクスプローラーopens in a new tabで既存のサブグラフを見つけることができます。

The Graph Explorer

独自のノードの実行

あるいは、独自のノードを実行することもできます。 ドキュメントはこちらopens in a new tab。 これを行う理由の1つは、ホストされたサービスでサポートされていないネットワークを使用する場合かもしれません。 現在サポートされているネットワークはこちらopens in a new tabで確認できます。

分散型の未来

GraphQLは、新たに着信するイベントのストリームもサポートしています。 これらは、現在オープンベータ版であるSubstreamsopens in a new tab を介してグラフ上でサポートされています。

2021年opens in a new tab、The Graphは分散型インデックスネットワークへの移行を開始しました。 この分散型インデックスネットワークのアーキテクチャについては、こちらopens in a new tabで詳しく読むことができます。

2つの重要な側面は次のとおりです。

  1. ユーザーはクエリに対してインデクサーに支払います。
  2. インデクサーはグラフトークン (GRT) をステークします。

最終更新: 2025年6月24日

このチュートリアルは役に立ちましたか?