The Graph:解決 Web3 資料查詢的問題
這次,我們將深入探討 The Graph,它在去年已然成為開發去中心化應用程式 (dapp) 的標準技術堆疊之一。 讓我們先來看看傳統的做法......
不使用 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, "Transfer failed");14 totalGamesPlayerWon++;15 } else {16 totalGamesPlayerLost++;17 }1819 emit BetPlaced(msg.sender, msg.value, hasWon);20 }21}顯示全部現在假設在我們的去中心化應用程式中,我們想要顯示總下注次數、輸贏的總場次,並且在有玩家再次遊玩時更新這些資訊。 做法會是:
- 擷取
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});顯示全部就我們這個簡單的範例而言,這樣做還算可以。 但假設我們現在只想顯示當前玩家輸/贏的下注總額。 這樣的話我們就沒輒了,你最好部署一份新合約來儲存並擷取那些數值。 現在想像一下更複雜的智能合約和去中心化應用程式,情況很快就會變得一團亂。
你可以看到為何這不是最佳做法:
- 對已經部署的合約不管用。
- 儲存這些數值會產生額外的 gas 費用。
- 需要另一次呼叫才能從以太坊節點擷取資料。
現在讓我們來看看一個更好的解決方案。
為您介紹 GraphQL
首先我們來談談 GraphQL,它最初是由 Facebook 設計和實作的。 您可能熟悉傳統的 REST API 模型。 現在,想像一下,您可以編寫一個查詢,精準地取得您想要的資料:
這兩張圖幾乎掌握了 GraphQL 的精髓。 透過右方的查詢,我們可以精確定義我們想要的資料,因此我們可以在一次請求中得到所有東西,而且不多不少,正好是我們需要的。 GraphQL 伺服器會處理所有必要資料的擷取,因此對於前端取用方來說,使用上非常簡單。 如果您有興趣,可以在這裡 (opens in a new tab)找到關於伺服器如何處理查詢的詳細說明。
了解了這些知識之後,讓我們終於可以進入區塊鏈領域和 The Graph 的世界了。
什麼是 The Graph?
區塊鏈是一種去中心化資料庫,但與通常情況不同的是,我們沒有用於此資料庫的查詢語言。 檢索資料的解決方案既痛苦又或者完全不可能。 The Graph 是一種用於索引和查詢區塊鏈資料的去中心化協定。 您可能已經猜到了,它使用 GraphQL 作為查詢語言。
範例永遠是理解事物的最好方法,所以讓我們在 GameContract 範例中使用 The Graph。
如何建立子圖
關於如何索引資料的定義被稱為「子圖」。 它需要三個元件:
- 資訊清單 (
subgraph.yaml) - 結構 (
schema.graphql) - 映射 (
mapping.ts)
資訊清單 (subgraph.yaml)
資訊清單是我們的設定檔,它定義了:
- 要索引哪些智能合約(位址、網路、ABI......)
- 要監聽哪些事件
- 其他要監聽的東西,例如函式呼叫或區塊
- 被呼叫的映射函式(請見下方的
mapping.ts)
您可以在這裡定義多個合約和處理常式。 一個典型的設定,會在 Hardhat 專案中,有一個 subgraph 資料夾,並有自己的儲存庫。 然後您就可以輕易地引用 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
- 字串
- 布林值
- 整數
- 大整數
- 大十進位數
您也可以使用實體作為型別來定義關聯。 在我們的範例中,我們定義了從玩家到下注的一對多關聯。 驚嘆號 (!) 代表該值不可為空。 完整的說明文件可以在這裡 (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 中的映射檔案定義了我們的函式,用以將傳入的事件轉換為實體。 它以 AssemblyScript(Typescript 的一個子集)撰寫。 這代表它可以被編譯成 WASM (WebAssembly),讓映射的執行更有效率且具可攜性。
您將需要定義 subgraph.yaml 檔案中命名的每個函式,所以在我們的例子中,我們只需要一個:handleNewBet。 我們首先嘗試從傳送者位址載入 Player 實體作為 ID。 如果它不存在,我們就建立一個新的實體,並填入初始值。
然後我們建立一個新的 Bet 實體。 這個實體的 ID 會是 event.transaction.hash.toHex() + "-" + event.logIndex.toString(),以確保其值永遠是唯一的。 只使用哈希是不夠的,因為有人可能會透過一份智能合約,在單一筆交易中多次呼叫 placeBet 函式。
最後,我們可以用所有資料來更新 Player 實體。 陣列無法直接推送,而需要如此處所示來更新。 我們使用 ID 來引用該筆下注。 最後需要使用 .save() 來儲存實體。
完整的說明文件可以在這裡查看:https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappings。 (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 的工具,您可以輕易地將 The Graph 整合進您的 React 去中心化應用程式(或 Apollo-Vue)。 特別是使用 React hooks 和 Apollo 時,擷取資料就跟在您的元件中寫一個 GraphQL 查詢一樣簡單。 一個典型的設定可能像這樣:
1// 查看所有子圖: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)的指示來部署子圖。 對於許多專案,您其實可以在 explorer (opens in a new tab) 中找到現有的子圖。
執行你自己的節點
或者,您也可以執行您自己的節點。 文件在這裡 (opens in a new tab)。 這樣做的一個原因可能是,您使用的網路不受託管服務支援。 目前支援的網路可在此處找到 (opens in a new tab)。
去中心化的未來
GraphQL 也支援用於新傳入事件的串流。 The Graph 透過 Substreams (opens in a new tab) 支援這些功能,目前正在公開測試中。
在 2021 年 (opens in a new tab),The Graph 開始轉型為去中心化索引網路。 您可以在此處 (opens in a new tab)閱讀更多關於此去中心化索引網路的架構。
兩個關鍵面向是:
- 使用者為查詢向索引者付費。
- 索引者質押 Graph 代幣 (GRT)。
頁面最後更新時間: 2026年2月26日






