ここ数年、私は技術者が知的財産の価値を高めることに注力し続けることができるフレームワーク、製品、サービスを見つけようと努めてきました。これは私にとって、ユニークな学習機会に満ちた素晴らしい旅であり続けています。
エンジニアとしての私は最近、以前話した既存のコンセプトに二次的なメリットを見出せる状況があるのではないかと考えました。言い換えれば、以前認識した元の親ソリューションと同じレベルの影響を持つ別のメリットを特定できるでしょうか?
この記事では、GraphQL をさらに深く掘り下げて、何がわかるかを調べたいと思いました。
「 REST を休むべき時」という記事では、RESTful サービスよりも GraphQL の方が適している現実的なシナリオについて説明しました。Apollo Server を使用して GraphQL API を構築およびデプロイする方法を詳しく説明しました。
このフォローアップ記事では、リアルタイムのデータ取得のためのサブスクリプションについて説明しながら、GraphQL の知識を深める予定です。また、サブスクリプションを利用するための WebSocket サービスも構築します。
前回の記事では、架空のビジネスの顧客が次のデータ コレクションを維持する Customer 360 の使用例を中心に説明しました。
GraphQL を使用する大きな利点は、単一の GraphQL リクエストで顧客のトークン (一意の ID) に必要なすべてのデータを取得できることです。
type Query { addresses: [Address] address(customer_token: String): Address contacts: [Contact] contact(customer_token: String): Contact customers: [Customer] customer(token: String): Customer credits: [Credit] credit(customer_token: String): Credit }
RESTful アプローチを使用して顧客の単一 (360) ビューを取得するには、複数のリクエストと応答を結合する必要がありました。GraphQL を使用すると、パフォーマンスがはるかに優れたソリューションが得られます。
人生のあらゆる面でレベルアップするには、新しい目標を達成する必要があります。ここでの私の目標は、次のことを意味します。
GraphQL 内でクエリやミューテーションよりもサブスクリプションを使用するという考え方は、次の条件が満たされる場合に適した方法です。
GraphQL 内でサブスクリプションを実装するのは簡単ではないため、これは重要です。基盤となるサーバーを更新するだけでなく、使用するアプリケーションの再設計も必要になります。
幸いなことに、Customer 360 の例で追求しているユースケースは、サブスクリプションに最適です。また、これらのサブスクリプションを活用するために WebSocket アプローチを実装する予定です。
これまで同様、今後もApolloを使い続けます。
まず、Apollo GraphQL サーバーでサブスクリプションをサポートするために必要なライブラリをインストールする必要があります。
npm install ws npm install graphql-ws @graphql-tools/schema npm install graphql-subscriptions
これらのアイテムをインストールしたら、元のリポジトリのindex.ts
更新して、次のようにtypedefs
定数を拡張することに重点を置きました。
type Subscription { creditUpdated: Credit }
また、新しいPubSub
インスタンスを格納する定数を確立し、後で使用するサンプル サブスクリプションを作成しました。
const pubsub = new PubSub(); pubsub.publish('CREDIT_BALANCE_UPDATED', { creditUpdated: { } });
既存のリゾルバをクリーンアップし、この新しいユースケース用に新しいSubscription
を追加しました。
const resolvers = { Query: { addresses: () => addresses, address: (parent, args) => { const customer_token = args.customer_token; return addresses.find(address => address.customer_token === customer_token); }, contacts: () => contacts, contact: (parent, args) => { const customer_token = args.customer_token; return contacts.find(contact => contact.customer_token === customer_token); }, customers: () => customers, customer: (parent, args) => { const token = args.token; return customers.find(customer => customer.token === token); }, credits: () => credits, credit: (parent, args) => { const customer_token = args.customer_token; return credits.find(credit => credit.customer_token === customer_token); } }, Subscription: { creditUpdated: { subscribe: () => pubsub.asyncIterator(['CREDIT_BALANCE_UPDATED']), } } };
次に、サーバー構成をリファクタリングし、サブスクリプション設計を導入しました。
const app = express(); const httpServer = createServer(app); const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' }); const schema = makeExecutableSchema({ typeDefs, resolvers }); const serverCleanup = useServer({ schema }, wsServer); const server = new ApolloServer({ schema, plugins: [ ApolloServerPluginDrainHttpServer({ httpServer }), { async serverWillStart() { return { async drainServer() { serverCleanup.dispose(); } }; } } ], }); await server.start(); app.use('/graphql', cors(), express.json(), expressMiddleware(server, { context: async () => ({ pubsub }) })); const PORT = Number.parseInt(process.env.PORT) || 4000; httpServer.listen(PORT, () => { console.log(`Server is now running on http://localhost:${PORT}/graphql`); console.log(`Subscription is now running on ws://localhost:${PORT}/graphql`); });
顧客主導の更新をシミュレートするために、サービスの実行中に 5 秒ごとにクレジット残高を 50 ドル増やす次のメソッドを作成しました。残高がクレジット限度額の 10,000 ドルに達する (または超過する) と、残高を 2,500 ドルにリセットして、残高の支払いが行われていることをシミュレートします。
function incrementCreditBalance() { if (credits[0].balance >= credits[0].credit_limit) { credits[0].balance = 0.00; console.log(`Credit balance reset to ${credits[0].balance}`); } else { credits[0].balance += 50.00; console.log(`Credit balance updated to ${credits[0].balance}`); } pubsub.publish('CREDIT_BALANCE_UPDATED', { creditUpdated: credits[0] }); setTimeout(incrementCreditBalance, 5000); } incrementCreditBalance();
完全なindex.ts
ファイルはここにあります。
サービスの準備ができたら、サービスをデプロイして操作できるようにします。前回 Heroku がうまく機能したので (私にとっては使いやすいので)、このアプローチを採用しましょう。
まず、次の Heroku CLI コマンドを実行する必要がありました。
$ heroku login $ heroku create jvc-graphql-server-sub Creating ⬢ jvc-graphql-server-sub... done https://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/ | https://git.heroku.com/jvc-graphql-server-sub.git
このコマンドは、Heroku がリモートとして使用するリポジトリも自動的に追加しました。
$ git remote heroku origin
前回の記事で述べたように、Apollo Server は実稼働環境では Apollo Explorer を無効にします。Apollo Explorer を必要に応じて利用できるようにするために、 NODE_ENV
環境変数を開発に設定する必要がありました。次の CLI コマンドを使用して設定しました。
$ heroku config:set NODE_ENV=development Setting NODE_ENV and restarting ⬢ jvc-graphql-server-sub... done, v3 NODE_ENV: development
コードを Heroku にデプロイする準備ができました。
$ git commit --allow-empty -m 'Deploy to Heroku' $ git push heroku
Heroku ダッシュボードをざっと見ると、Apollo Server が問題なく動作していることが分かりました。
設定セクションで、このサービス インスタンスの Heroku アプリ URL を見つけました。
https://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/
注意:この記事が公開される頃には、このリンクは利用できなくなります。
とりあえず、この URL に GraphQL を追加して Apollo Server Studio を起動することができました。これにより、サブスクリプションが期待どおりに動作していることを確認できました。
画面の右側にあるサブスクリプションの応答に注意してください。
WebSocket サポートと Heroku の機能を活用して、作成したサブスクリプションを使用する実装を作成できます。
私の場合は、次の内容の index.js ファイルを作成しました。基本的に、これによって WebSocket クライアントが作成され、クライアントが実行中であることを検証するために使用できるダミーの HTTP サービスも確立されました。
import { createClient } from "graphql-ws"; import { WebSocket } from "ws"; import http from "http"; // Create a dummy HTTP server to bind to Heroku's $PORT const PORT = process.env.PORT || 3000; http.createServer((req, res) => res.end('Server is running')).listen(PORT, () => { console.log(`HTTP server running on port ${PORT}`); }); const host_url = process.env.GRAPHQL_SUBSCRIPTION_HOST || 'ws://localhost:4000/graphql'; const client = createClient({ url: host_url, webSocketImpl: WebSocket }); const query = `subscription { creditUpdated { token customer_token credit_limit balance credit_score } }`; function handleCreditUpdated(data) { console.log('Received credit update:', data); } // Subscribe to the creditUpdated subscription client.subscribe( { query, }, { next: (data) => handleCreditUpdated(data.data.creditUpdated), error: (err) => console.error('Subscription error:', err), complete: () => console.log('Subscription complete'), } );
完全なindex.js
ファイルは ここにあります。
このシンプルな Node.js アプリケーションを Heroku にデプロイすることもできます。その場合は、 GRAPHQL_SUBSCRIPTION_HOST
環境変数を、先ほど使用した Heroku アプリの URL に設定してください。
また、Heroku にアプリの起動方法を指示するために、次のProcfile
作成しました。
web: node src/index.js
次に、新しい Heroku アプリを作成しました。
$ heroku create jvc-websocket-example Creating ⬢ jvc-websocket-example... done https://jvc-websocket-example-62824c0b1df4.herokuapp.com/ | https://git.heroku.com/jvc-websocket-example.git
次に、実行中の GraphQL サーバーを指すようにGRAPHQL_SUBSCRIPTION_HOST
環境変数を設定します。
$ heroku --app jvc-websocket-example \ config:set \ GRAPHQL_SUBSCRIPTION_HOST=ws://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/graphql
この時点で、コードを Heroku にデプロイする準備が整いました。
$ git commit --allow-empty -m 'Deploy to Heroku' $ git push heroku
WebSocket クライアントが起動すると、Heroku ダッシュボードでそのステータスを確認できます。
jvc-websocket-example
インスタンスの Heroku ダッシュボード内のログを表示すると、 jvc-graphql-server-sub
サービスのbalance
プロパティが複数回更新されたことがわかります。私のデモでは、残高がゼロに減ったユースケースをキャプチャして、支払いが行われたことをシミュレートすることもできました。
ターミナルでは、CLI コマンド heroku logs を使用して同じログにアクセスできます。
2024-08-28T12:14:48.463846+00:00 app[web.1]: Received credit update: { 2024-08-28T12:14:48.463874+00:00 app[web.1]: token: 'credit-token-1', 2024-08-28T12:14:48.463875+00:00 app[web.1]: customer_token: 'customer-token-1', 2024-08-28T12:14:48.463875+00:00 app[web.1]: credit_limit: 10000, 2024-08-28T12:14:48.463875+00:00 app[web.1]: balance: 9950, 2024-08-28T12:14:48.463876+00:00 app[web.1]: credit_score: 750 2024-08-28T12:14:48.463876+00:00 app[web.1]: }
サブスクリプション実装が実行されている GraphQL サービスがあるだけでなく、それらの更新を利用する WebSocket クライアントも存在するようになりました。
読者の皆さんは、私の個人的なミッションステートメントを覚えているかもしれません。これは、あらゆる IT プロフェッショナルに当てはまると思います。
「知的財産の価値を高める機能や特性を提供することに時間を集中してください。他のすべてにはフレームワーク、製品、サービスを活用してください。」 — J. ベスター
この GraphQL サブスクリプションの詳細な調査では、同じく Heroku で実行されている別のサービス (WebSocket を使用する Node.js ベースのアプリケーション) を使用して、Heroku で実行されている Apollo Server からの更新を正常に消費しました。軽量サブスクリプションを活用することで、変更されていないデータに対するクエリの送信を回避し、クレジット残高の更新が発生したときにそれを受信するようにサブスクライブするだけです。
はじめに、以前書いたトピックの中で、追加の価値原則を探すことについて触れました。GraphQL サブスクリプションは、私が考えていたものの優れた例です。これは、ソース データに対してクエリを実行する必要なく、消費者が更新をすぐに受け取ることができるためです。Customer 360 データの消費者は、更新が発生したときにライブで更新できることを知り、非常に喜ぶでしょう。
Heroku は、CLI と標準の Git コマンドを使用してソリューションを迅速にプロトタイプ化できるプラットフォームを提供することで、私のミッション ステートメントを順守し続けているもう 1 つの例です。これにより、サブスクリプションのユース ケースを簡単に紹介できるだけでなく、WebSocket を使用してコンシューマーを実装することもできます。
この記事のソースコードに興味がある場合は、GitLab のリポジトリを確認してください。
この取り組みによって、GraphQL スキルをレベルアップできたと自信を持って言えます。この旅は私にとって新しく、挑戦的なものでした...そしてとても楽しかったです!
次は認証について詳しく取り上げる予定です。GraphQL と Apollo Server をレベルアップするまた別の機会になれば幸いです。お楽しみに!
本当に素晴らしい一日をお過ごしください!