The Graph: Usprawnianie zapytań o dane Web3
Tym razem przyjrzymy się bliżej The Graph, który w zeszłym roku stał się zasadniczo częścią standardowego stosu do tworzenia dapek. Zobaczmy najpierw, jak zrobilibyśmy rzeczy w tradycyjny sposób...
Bez The Graph...
Przejdźmy więc do prostego przykładu w celach ilustracyjnych. Wszyscy lubimy gry, więc wyobraźmy sobie prostą grę, w której użytkownicy obstawiają zakłady:
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 nie powiódł się");14 totalGamesPlayerWon++;15 } else {16 totalGamesPlayerLost++;17 }1819 emit BetPlaced(msg.sender, msg.value, hasWon);20 }21}Pokaż wszystkoPowiedzmy, że w naszej dapce chcemy wyświetlać całkowitą liczbę zakładów, łączną liczbę przegranych/wygranych gier, a także aktualizować ją za każdym razem, gdy ktoś ponownie zagra. Podejście byłoby następujące:
- Pobierz
totalGamesPlayerWon. - Pobierz
totalGamesPlayerLost. - Subskrybuj zdarzenia
BetPlaced.
Możemy nasłuchiwać zdarzenia w Web3 (opens in a new tab), jak pokazano po prawej stronie, ale wymaga to obsługi sporej liczby przypadków.
1GameContract.events.BetPlaced({2 fromBlock: 03}, function(error, event) { console.log(event); })4.on('data', function(event) {5 // zdarzenie zostało wywołane6})7.on('changed', function(event) {8 // zdarzenie zostało ponownie usunięte9})10.on('error', function(error, receipt) {11 // transakcja odrzucona12});Pokaż wszystkoW naszym prostym przykładzie jest to nadal w miarę w porządku. Ale powiedzmy, że chcemy teraz wyświetlać kwoty przegranych/wygranych zakładów tylko dla aktualnego gracza. Cóż, nie mamy szczęścia, lepiej wdrożyć nowy kontrakt, który przechowuje te wartości i je pobiera. A teraz wyobraź sobie znacznie bardziej skomplikowany inteligentny kontrakt i dapkę, sprawy mogą się bardzo szybko skomplikować.
Widać, że nie jest to optymalne:
- Nie działa dla już wdrożonych kontraktów.
- Dodatkowe koszty gazu za przechowywanie tych wartości.
- Wymaga kolejnego wywołania w celu pobrania danych dla węzła Ethereum.
Spójrzmy teraz na lepsze rozwiązanie.
Pozwól, że przedstawię Ci GraphQL
Najpierw porozmawiajmy o GraphQL, pierwotnie zaprojektowanym i zaimplementowanym przez Facebooka. Być może znasz tradycyjny model API REST. A teraz wyobraź sobie, że zamiast tego możesz napisać zapytanie dokładnie o te dane, które chcesz:
Te dwa obrazy w dużej mierze oddają istotę GraphQL. Za pomocą zapytania po prawej stronie możemy dokładnie zdefiniować, jakich danych chcemy, dzięki czemu otrzymujemy wszystko w jednym żądaniu i nic ponad to, czego potrzebujemy. Serwer GraphQL obsługuje pobieranie wszystkich wymaganych danych, dzięki czemu jest niezwykle łatwy w użyciu dla klienta frontendowego. To jest dobre wyjaśnienie (opens in a new tab), jak dokładnie serwer obsługuje zapytanie, jeśli jesteś zainteresowany.
Mając tę wiedzę, przejdźmy w końcu do przestrzeni blockchain i The Graph.
Czym jest The Graph?
Blockchain to zdecentralizowana baza danych, ale w przeciwieństwie do tego, co zwykle ma miejsce, nie mamy języka zapytań dla tej bazy danych. Rozwiązania do pobierania danych są uciążliwe lub całkowicie niemożliwe. The Graph to zdecentralizowany protokół do indeksowania i wykonywania zapytań o dane blockchain. I jak można się domyślić, używa GraphQL jako języka zapytań.
Przykłady są zawsze najlepszym sposobem na zrozumienie czegoś, więc użyjmy The Graph w naszym przykładzie GameContract.
Jak stworzyć podgraf
Definicja sposobu indeksowania danych nazywana jest podgrafem. Wymaga trzech komponentów:
- Manifest (
subgraph.yaml) - Schemat (
schema.graphql) - Mapowanie (
mapping.ts)
Manifest (subgraph.yaml)
Manifest jest naszym plikiem konfiguracyjnym i definiuje:
- które inteligentne kontrakty indeksować (adres, sieć, ABI...)
- jakich zdarzeń nasłuchiwać
- inne elementy do nasłuchiwania, takie jak wywołania funkcji lub bloki
- wywoływane funkcje mapowania (patrz
mapping.tsponiżej)
Można tu zdefiniować wiele kontraktów i handlerów. Typowa konfiguracja to folder podgrafu w projekcie Hardhat z własnym repozytorium. Wtedy można łatwo odwołać się do ABI.
Dla wygody można również użyć narzędzia do szablonów, takiego jak mustache. Następnie tworzy się subgraph.template.yaml i wstawia adresy na podstawie najnowszych wdrożeń. Bardziej zaawansowany przykład konfiguracji można znaleźć na przykład w repozytorium podgrafu Aave (opens in a new tab).
Pełną dokumentację można zobaczyć tutaj (opens in a new tab).
1specVersion: 0.0.12description: Obstawianie zakładów na Ethereum3repository: - GitHub link -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.tsPokaż wszystkoSchemat (schema.graphql)
Schemat to definicja danych GraphQL. Pozwala on zdefiniować, jakie encje istnieją i jakie są ich typy. Obsługiwane typy z The Graph to
- Bajty
- ID
- String
- Boolean
- Int
- BigInt
- BigDecimal
Można również używać encji jako typów do definiowania relacji. W naszym przykładzie definiujemy relację jeden-do-wielu od gracza do zakładów. Znak ! oznacza, że wartość nie może być pusta. Pełną dokumentację można zobaczyć tutaj (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}Pokaż wszystkoMapowanie (mapping.ts)
Plik mapowania w The Graph definiuje nasze funkcje, które przekształcają przychodzące zdarzenia w encje. Jest napisany w AssemblyScript, podzbiorze Typescript. Oznacza to, że może być skompilowany do WASM (WebAssembly) w celu wydajniejszego i bardziej przenośnego wykonywania mapowania.
Należy zdefiniować każdą funkcję nazwaną w pliku subgraph.yaml, więc w naszym przypadku potrzebujemy tylko jednej: handleNewBet. Najpierw próbujemy załadować encję Player z adresu nadawcy jako id. Jeśli nie istnieje, tworzymy nową encję i wypełniamy ją wartościami początkowymi.
Następnie tworzymy nową encję Bet. Identyfikator dla tego będzie event.transaction.hash.toHex() + "-" + event.logIndex.toString(), co zapewnia unikalną wartość. Użycie samego haszu nie wystarczy, ponieważ ktoś może wywołać funkcję placeBet kilka razy w jednej transakcji za pośrednictwem inteligentnego kontraktu.
Na koniec możemy zaktualizować encję Player, podając wszystkie dane. Tablice nie mogą być bezpośrednio zasilane (push), ale muszą być aktualizowane w pokazany tutaj sposób. Używamy identyfikatora do odniesienia się do zakładu. I .save() jest wymagane na końcu do zapisania encji.
Pełną dokumentację można zobaczyć tutaj: https://thegraph.com/docs/en/developing/creating-a-subgraph/#writing-mappings (opens in a new tab). Można również dodać dane wyjściowe rejestrowania do pliku mapowania, patrz tutaj (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 // utwórz, jeśli jeszcze nie istnieje9 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 // zaktualizuj tablicę w ten sposób32 let bets = player.bets33 bets.push(bet.id)34 player.bets = bets3536 player.save()37}Pokaż wszystkoUżycie na frontendzie
Używając czegoś takiego jak Apollo Boost, można łatwo zintegrować The Graph w swojej dapce React (lub Apollo-Vue). Szczególnie w przypadku korzystania z hooków React i Apollo, pobieranie danych jest tak proste, jak napisanie pojedynczego zapytania GraphQL w komponencie. Typowa konfiguracja może wyglądać następująco:
1// Zobacz wszystkie podgrafy: 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)Pokaż wszystkoA teraz możemy napisać na przykład takie zapytanie. Spowoduje to pobranie
- ile razy bieżący użytkownik wygrał
- ile razy bieżący użytkownik przegrał
- listy sygnatur czasowych ze wszystkimi jego poprzednimi zakładami
Wszystko w jednym żądaniu do serwera 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])Pokaż wszystkoAle brakuje nam ostatniego elementu układanki, którym jest serwer. Można go uruchomić samodzielnie lub skorzystać z usługi hostowanej.
Serwer The Graph
Graph Explorer: Usługa hostowana
Najprostszym sposobem jest skorzystanie z usługi hostowanej. Postępuj zgodnie z instrukcjami tutaj (opens in a new tab), aby wdrożyć podgraf. Dla wielu projektów można znaleźć istniejące podgrafy w eksploratorze (opens in a new tab).
Uruchamianie własnego węzła
Alternatywnie można uruchomić własny węzeł. Dokumentacja tutaj (opens in a new tab). Jednym z powodów może być korzystanie z sieci, która nie jest obsługiwana przez usługę hostowaną. Obecnie obsługiwane sieci można znaleźć tutaj (opens in a new tab).
Zdecentralizowana przyszłość
GraphQL obsługuje również strumienie dla nowo przychodzących zdarzeń. Są one obsługiwane w The Graph za pomocą Substreams (opens in a new tab), które są obecnie w otwartej wersji beta.
W 2021 (opens in a new tab) The Graph rozpoczął przejście na zdecentralizowaną sieć indeksującą. Więcej o architekturze tej zdecentralizowanej sieci indeksującej można przeczytać tutaj (opens in a new tab).
Dwa kluczowe aspekty to:
- Użytkownicy płacą indekserom za zapytania.
- Indekserzy stakują tokeny Graph (GRT).
Strona ostatnio zaktualizowana: 26 lutego 2026






