The Graph: Web3データクエリ問題の解決
今回は、The Graphを詳しく見ていきます。これは昨年、dappsを開発するための標準スタックの一部となりました。 まずは、従来のやり方から見ていきましょう...
The Graphを使用しない場合...
それでは、説明のために簡単な例を見ていきましょう。 私たちは皆ゲームが好きなので、ユーザーがベットするシンプルなゲームを想像してみましょう:
1pragma solidity 0.7.1;23contract Game {4 uint256 totalGamesPlayerWon = 0;5 uint256 totalGamesPlayerLost = 0;6 event BetPlaced(address player, uint256 value, bool hasWon);78 function placeBet() external payable {9 bool hasWon = evaluateBetForPlayer(msg.sender);1011 if (hasWon) {12 (bool success, ) = msg.sender.call{ value: msg.value * 2 }('');13 require(success, "送金に失敗しました");14 totalGamesPlayerWon++;15 } else {16 totalGamesPlayerLost++;17 }1819 emit BetPlaced(msg.sender, msg.value, hasWon);20 }21}すべて表示さて、私たちのdappで合計ベット数、勝敗の合計ゲーム数を表示し、誰かが再度プレイするたびにそれを更新したいとしましょう。 アプローチは次のようになります:
totalGamesPlayerWonを取得します。totalGamesPlayerLostを取得します。BetPlacedイベントをサブスクライブします。
右に示すようにWeb3のイベントopens in a new tabをリッスンできますが、かなりの数のケースを処理する必要があります。
1GameContract.events.BetPlaced({2 fromBlock: 03}, 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モデルには馴染みがあるかもしれません。 代わりに、欲しいデータを正確に取得するためのクエリを書けると想像してみてください。
この2つの画像は、GraphQLの本質をよく捉えています。 右側のクエリでは、欲しいデータを正確に定義できます。そのため、1回のリクエストで必要なものだけをすべて取得できます。 GraphQLサーバーは必要なすべてのデータの取得を処理するため、フロントエンドの利用者側にとっては非常に使いやすくなっています。 ご興味があれば、こちらの分かりやすい説明opens in a new tabで、サーバーがクエリをどのように処理するかを正確に知ることができます。
この知識をもとに、いよいよブロックチェーンとThe Graphの世界に飛び込んでみましょう。
The Graphとは?
ブロックチェーンは分散型データベースですが、通常の場合とは対照的に、このデータベースにはクエリ言語がありません。 データを取得するためのソリューションは、手間がかかるか、あるいはまったく不可能です。 The Graphは、ブロックチェーンのデータをインデックス化し、クエリを実行するための分散型プロトコルです。 お察しの通り、クエリ言語としてGraphQLを使用しています。
何かを理解するには例を見るのが一番です。そこで、私たちのGameContractの例でThe Graphを使ってみましょう。
サブグラフの作成方法
データをインデックス化する方法の定義は、サブグラフと呼ばれます。 それには3つのコンポーネントが必要です:
- マニフェスト (
subgraph.yaml) - スキーマ (
schema.graphql) - マッピング (
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.12description: イーサリアム上でのベット3repository: - GitHubリンク -4schema:5 file: ./schema.graphql6dataSources:7 - kind: ethereum/contract8 name: GameContract9 network: mainnet10 source:11 address: '0x2E6454...cf77eC'12 abi: GameContract13 startBlock: 617524414 mapping:15 kind: ethereum/events16 apiVersion: 0.0.117 language: wasm/assemblyscript18 entities:19 - GameContract20 abis:21 - name: GameContract22 file: ../build/contracts/GameContract.json23 eventHandlers:24 - event: PlacedBet(address,uint256,bool)25 handler: handleNewBet26 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}78type Player @entity {9 id: ID!10 totalPlayedCount: Int11 hasWonCount: Int12 hasLostCount: Int13 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"34export function handleNewBet(event: PlacedBet): void {5 let player = Player.load(event.transaction.from.toHex())67 if (player == null) {8 // まだ存在しない場合は作成9 player = new Player(event.transaction.from.toHex())10 player.bets = new Array<string>(0)11 player.totalPlayedCount = 012 player.hasWonCount = 013 player.hasLostCount = 014 }1516 let bet = new Bet(17 event.transaction.hash.toHex() + "-" + event.logIndex.toString()18 )19 bet.player = player.id20 bet.playerHasWon = event.params.hasWon21 bet.time = event.block.timestamp22 bet.save()2324 player.totalPlayedCount++25 if (event.params.hasWon) {26 player.hasWonCount++27 } else {28 player.hasLostCount++29 }3031 // このように配列を更新32 let bets = player.bets33 bets.push(bet.id)34 player.bets = bets3536 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})56ReactDOM.render(7 <ApolloProvider client={client}>8 <App />9 </ApolloProvider>,10 document.getElementById("root")11)すべて表示そして今、例えばこのようなクエリを書くことができます。 これにより、以下が取得されます
- 現在のユーザーが勝った回数
- 現在のユーザーが負けた回数
- 彼の過去のすべてのベットのタイムスタンプのリスト
すべてGraphQLサーバーへの単一のリクエストで完了します。
1const myGraphQlQuery = gql`2 players(where: { id: $currentUser }) {3 totalPlayedCount4 hasWonCount5 hasLostCount6 bets {7 time8 }9 }10`1112const { loading, error, data } = useQuery(myGraphQlQuery)1314React.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で既存のサブグラフを見つけることができます。
独自のノードの実行
あるいは、独自のノードを実行することもできます。 ドキュメントはこちら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つの重要な側面は次のとおりです。
- ユーザーはクエリに対してインデクサーに支払います。
- インデクサーはグラフトークン (GRT) をステークします。
最終更新: 2025年6月24日






