إنشاء مخططات في الوقت الفعلي باستخدام GraphQL و Postgres

نشرت: 2022-03-10
ملخص سريع ↬ لا توجد طريقة أفضل لفهم البيانات من تصورها باستخدام المخططات والرسوم البيانية. لدى مجتمع JS بعض المشاريع الرائعة مفتوحة المصدر التي تجعل تصور البيانات أسهل ، ومع ذلك ، لم يكن هناك حل سريع لبناء الخلفيات في الوقت الفعلي التي يمكنها دعم هذه المخططات وجعلها في الوقت الفعلي. باستخدام GraphQL (التي تحتوي على مواصفات محددة جيدًا للاشتراكات في الوقت الفعلي) ، يمكننا تشغيل الواجهة الخلفية في الوقت الفعلي في غضون ثوانٍ واستخدامها لتشغيل الرسوم البيانية في الوقت الفعلي.

تشكل الرسوم البيانية جزءًا لا يتجزأ من أي صناعة تتعامل مع البيانات. تعد المخططات مفيدة في مجال التصويت والاقتراع ، كما أنها رائعة أيضًا في مساعدتنا على فهم السلوكيات والخصائص المختلفة للمستخدمين والعملاء الذين نعمل معهم بشكل أفضل.

لماذا الرسوم البيانية في الوقت الحقيقي مهمة جدا؟ حسنًا ، إنها مفيدة في الحالات التي يتم فيها إنتاج بيانات جديدة بشكل مستمر ؛ على سبيل المثال ، عند استخدام سلاسل الوقت المباشر لتصور أسعار الأسهم يعد استخدامًا رائعًا لمخططات الوقت الفعلي. في هذا البرنامج التعليمي ، سأشرح كيفية إنشاء مخططات في الوقت الفعلي باستخدام تقنيات مفتوحة المصدر مناسبة لهذه المهمة تحديدًا.

ملاحظة : يتطلب هذا البرنامج التعليمي معرفة أساسية بـ React و GraphQL.

كومة

  1. PostgreSQL
    الهدف الأساسي من استخدام الرسوم البيانية هو تصور كميات "ضخمة" من البيانات. لذلك ، نحتاج إلى قاعدة بيانات تتعامل مع البيانات الكبيرة بكفاءة وتوفر واجهة برمجة تطبيقات سهلة الاستخدام لإعادة هيكلتها. تسمح لنا قواعد بيانات SQL بعمل آراء تجرد وتجمع البيانات لنا. سنستخدم Postgres وهي قاعدة بيانات تم اختبارها بمرور الوقت وذات كفاءة عالية. كما أن لديها امتدادات رائعة مفتوحة المصدر مثل Timescale و PostGIS والتي تسمح لنا ببناء مخططات قائمة على الموقع الجغرافي ومتسلسلة زمنية على التوالي. سنستخدم مقياس الوقت لبناء مخطط السلسلة الزمنية الخاص بنا.
  2. محرك GraphQL
    يدور هذا المنشور حول إنشاء مخططات في الوقت الفعلي ، وتأتي GraphQL بمواصفات محددة جيدًا للاشتراكات في الوقت الفعلي. Hasura GraphQL Engine هو خادم GraphQL مفتوح المصدر يأخذ اتصال Postgres ويسمح لك بالاستعلام عن بيانات Postgres عبر GraphQL في الوقت الفعلي. يأتي أيضًا مع طبقة التحكم في الوصول التي تساعدك على تقييد بياناتك بناءً على قواعد التحكم في الوصول المخصصة.
  3. رسم بياني
    ChartJS هي مكتبة مفتوحة المصدر شائعة وتتم صيانتها جيدًا لبناء الرسوم البيانية باستخدام JavaScript. سنستخدم chart.js جنبًا إلى جنب مع ReactJS abstraction react-chartjs-2 . حول سبب استخدام React ، يرجع ذلك إلى أن React تمكّن المطورين من استخدام واجهة برمجة تطبيقات بديهية تعتمد على الأحداث. أيضًا ، يعد تدفق البيانات أحادي الاتجاه في React مثاليًا لإنشاء مخططات تعتمد على البيانات.
المزيد بعد القفز! أكمل القراءة أدناه ↓

متطلبات

في هذا البرنامج التعليمي ، ستحتاج إلى ما يلي على نظامك:

  1. دوكر سي
    Docker هو برنامج يتيح لك تخزين تطبيقاتك في حاويات. صورة عامل الإرساء عبارة عن حزمة مستقلة تحتوي على برامج جنبًا إلى جنب مع تبعياتها ونظام تشغيل بسيط. يمكن تشغيل صور عامل الإرساء هذه تقنيًا في أي جهاز مثبت عليه عامل إرساء. سوف تحتاج عامل ميناء لهذا البرنامج التعليمي.
    • اقرأ المزيد عن Docker
    • تثبيت Docker
  2. npm: npm هي الحزمة المُدارة لـ JavaScript.

تجريبي

سنقوم ببناء الرسم البياني التالي للسلسلة الزمنية الحية التي توضح درجة الحرارة القصوى لموقع ما في فترات زمنية مدتها 5 ثوانٍ خلال العشرين دقيقة الماضية من اللحظة الحالية.

عرض GIF لمخطط الوقت الفعلي
عرض GIF لمخطط الوقت الفعلي

إعداد الواجهة الخلفية

تشغيل الخدمات

تتكون الواجهة الخلفية من قاعدة بيانات Postgres ، وامتداد مقياسها الزمني ، ومحرك Hasura GraphQL. دعنا نحصل على قاعدة البيانات وخادم GraphQL الخاص بنا قيد التشغيل عن طريق تشغيل صور عامل الإرساء المعني. قم بإنشاء ملف يسمى docker-compose.yaml والصق هذا المحتوى فيه.

ملاحظة : يعد docker-compose أداة مساعدة لتشغيل العديد من صور عامل الإرساء بشكل إعلاني.

 version: '2' services: timescale: image: timescale/timescaledb:latest-pg10 restart: always environment: POSTGRES_PASSWORD: postgrespassword volumes: - db_data:/var/lib/postgresql/data graphql-engine: image: hasura/graphql-engine:v1.0.0-alpha38 ports: - "8080:8080" depends_on: - "timescale" restart: always environment: HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@timescale:5432/postgres HASURA_GRAPHQL_ACCESS_KEY: mylongsecretkey command: - graphql-engine - serve - --enable-console volumes: db_data:

يحتوي هذا docker-compose.yaml على المواصفات لخدمتين:

  1. timescale
    هذه هي قاعدة بيانات Postgres الخاصة بنا مع تثبيت ملحق Timescale. تم تكوينه ليعمل في المنفذ 5432.
  2. graphql-engine
    هذا هو مثال Hasura GraphQL Engine ، أي خادم GraphQL الذي يشير إلى قاعدة البيانات ويعطي واجهات برمجة تطبيقات GraphQL عليها. تم تكوينه للتشغيل عند المنفذ 8080 ، وتم تعيين المنفذ 8080 على المنفذ 8080 للجهاز الذي تعمل عليه حاوية عامل الإرساء. هذا يعني أنه يمكنك الوصول إلى خادم GraphQL هذا من خلال localhost:8080 من الجهاز.

لنقم بتشغيل حاويات عامل الإرساء هذه عن طريق تشغيل الأمر التالي أينما وضعت docker-compose.yaml .

 docker-compose up -d

يقوم هذا الأمر بسحب صور عامل الإرساء من السحابة وتشغيلها بالترتيب المحدد. قد يستغرق الأمر بضع ثوانٍ بناءً على سرعة الإنترنت لديك. بمجرد اكتماله ، يمكنك الوصول إلى وحدة التحكم في GraphQL Engine على https://localhost:8080/console .

وحدة تحكم المحرك Hasura GraphQL
وحدة تحكم محرك Hasura GraphQL (معاينة كبيرة)

إنشاء قاعدة البيانات

بعد ذلك ، دعنا ننشئ جدولًا يسمى درجة الحرارة يخزن قيم درجات الحرارة في أوقات مختلفة. انتقل إلى علامة التبويب البيانات في وحدة التحكم وانتقل إلى قسم SQL . قم بإنشاء جدول temperature الخاص بنا عن طريق تشغيل كتلة SQL هذه:

 CREATE TABLE temperature ( temperature numeric not null, location text not null, recorded_at timestamptz not null default now() );

يؤدي هذا إلى إنشاء جدول Postgres بسيط في قاعدة البيانات. لكننا نرغب في الاستفادة من تقسيم الفاصل الزمني لملحق مقياس الوقت. للقيام بذلك ، يجب علينا تحويل هذا الجدول إلى قابل للضغط على مقياس الوقت عن طريق تشغيل أمر SQL:

 SELECT create_hypertable('temperature', 'recorded_at');

يقوم هذا الأمر بإنشاء قابل للضغط العالي يتم تقسيمه حسب الوقت في الحقل recorded_at .

الآن ، بما أن هذا الجدول قد تم إنشاؤه ، يمكننا مباشرة إنشاء استعلامات GraphQL عليه. يمكنك تجربتها بالنقر فوق علامة التبويب GraphiQL في الأعلى. حاول صنع طفرة أولاً:

 mutation { insert_temperature ( objects: [{ temperature: 13.4 location: "London" }] ) { returning { recorded_at temperature } } } أشياء: [{ mutation { insert_temperature ( objects: [{ temperature: 13.4 location: "London" }] ) { returning { recorded_at temperature } } }

تُدرج طفرة GraphQL أعلاه صفًا في جدول temperature . حاول الآن إجراء استعلام GraphQL للتحقق من إدراج البيانات.

ثم حاول إجراء استعلام:

 query { temperature { recorded_at temperature location } }

أتمنى أن تعمل :)

الآن ، تتمثل المهمة التي بين أيدينا في إنشاء مخطط مباشر للسلسلة الزمنية يعرض درجة الحرارة القصوى لموقع ما في فترات زمنية مدتها 5 ثوانٍ على مدار العشرين دقيقة الماضية من اللحظة الحالية. لنقم بإنشاء عرض يعطينا هذه البيانات بالضبط.

 CREATE VIEW last_20_min_temp AS ( SELECT time_bucket('5 seconds', recorded_at) AS five_sec_interval, location, MAX(temperature) AS max_temp FROM temperature WHERE recorded_at > NOW() - interval '20 minutes' GROUP BY five_sec_interval, location ORDER BY five_sec_interval ASC );

يجمع هذا العرض البيانات من جدول temperature في نوافذ مدتها 5 ثوانٍ مع درجة الحرارة القصوى ( max_temp) . يتم التجميع الثانوي باستخدام حقل location . كل هذه البيانات فقط من العشرين دقيقة الماضية من اللحظة الحالية.

هذا هو. تم إعداد الواجهة الخلفية لدينا. دعونا الآن نبني مخططًا لطيفًا في الوقت الفعلي.

نهاية المقدمة

مرحبًا اشتراكات GraphQL

اشتراكات GraphQL هي في الأساس استعلامات GraphQL "مباشرة". تعمل عبر WebSockets ولديها نفس بنية الاستجابة تمامًا مثل استعلامات GraphQL. ارجع إلى https://localhost:8080/console وحاول إنشاء اشتراك GraphQL في العرض الذي أنشأناه.

 subscription { last_20_min_temp( order_by: { five_sec_interval: asc } where: { location: { _eq: "London" } } ) { five_sec_interval location max_temp } }

يشترك هذا الاشتراك في البيانات الموجودة في العرض حيث يكون الموقع London ويتم ترتيبها بترتيب تصاعدي five_second_intervals .

بطبيعة الحال ، ستكون الاستجابة من طريقة العرض مصفوفة فارغة لأننا لم ندرج أي شيء في قاعدة البيانات خلال العشرين دقيقة الماضية. (قد ترى الإدخال الذي أدخلناه في وقت ما إذا وصلت إلى هذا القسم في غضون عشرين دقيقة.)

 { "data": { "last_20_min_temp": [] } }

استمر في تشغيل هذا الاشتراك ، افتح علامة تبويب أخرى وحاول إدخال قيمة أخرى في جدول temperatures باستخدام نفس الطفرة التي أجريناها سابقًا. بعد الإدراج ، إذا عدت إلى علامة التبويب حيث كان الاشتراك قيد التشغيل ، فسترى الاستجابة وقد تم تحديثها تلقائيًا. هذا هو السحر الحقيقي الذي يوفره محرك GraphQL. دعنا نستخدم هذا الاشتراك لتشغيل مخططنا في الوقت الفعلي.

الشروع في العمل مع Create-React-App

دعنا نبدأ بسرعة في بدء تشغيل تطبيق React باستخدام إنشاء تطبيق رد فعل. قم بتشغيل الأمر:

 npx create-react-app time-series-chart

سيؤدي هذا إلى إنشاء مشروع بداية فارغ. cd فيه وقم بتثبيت مكتبات GraphQL والمخططات. أيضًا ، قم بتثبيت لحظة لتحويل الطوابع الزمنية إلى تنسيق يمكن للبشر قراءته.

 cd time-series-chart npm install --save apollo-boost apollo-link-ws subscriptions-transport-ws graphql react-apollo chart.js react-chartjs-2 moment

أخيرًا ، قم بتشغيل التطبيق باستخدام npm start وسيتم فتح تطبيق React الأساسي على https://localhost:3000 .

تطبيق إنشاء-تفاعل-خام
تطبيق Raw creat-رد فعل (معاينة كبيرة)

إعداد عميل Apollo لخدمة GraphQL من جانب العميل

يعد عميل Apollo حاليًا أفضل عميل GraphQL يعمل مع أي خادم متوافق مع GraphQL. يعد Relay modern جيدًا أيضًا ولكن يجب أن يدعم الخادم مواصفات الترحيل للاستفادة من جميع مزايا Relay الحديثة. سنستخدم عميل Apollo مع GraphQL من جانب العميل لهذا البرنامج التعليمي. دعونا نقوم بالإعداد لتوفير عميل Apollo للتطبيق.

أنا لا أدخل في التفاصيل الدقيقة لهذا الإعداد لأن مقتطفات التعليمات البرمجية التالية مأخوذة مباشرة من المستندات. توجه إلى src/index.js في دليل تطبيق React وقم بإنشاء مثيل لعميل Apollo وأضف مقتطف الشفرة هذا أعلى ReactDOM.render .

 import { WebSocketLink } from 'apollo-link-ws'; import { ApolloClient } from 'apollo-client'; import { ApolloProvider } from 'react-apollo'; import { InMemoryCache } from 'apollo-cache-inmemory'; // Create a WebSocket link: const link = new WebSocketLink({ uri: `ws://localhost:8080/v1alpha1/graphql`, options: { reconnect: true, connectionParams: { headers: { "x-hasura-admin-secret: "mylongsecretkey" } } } }) const cache = new InMemoryCache(); const client = new ApolloClient({ link, cache });

أخيرًا ، قم بلف App داخل ApolloProvider حتى نتمكن من استخدام عميل Apollo في مكونات الأطفال. يجب أن يبدو App.js أخيرًا بالشكل:

 import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import { WebSocketLink } from 'apollo-link-ws'; import { ApolloClient } from 'apollo-client'; import { ApolloProvider } from 'react-apollo'; import { InMemoryCache } from 'apollo-cache-inmemory'; // Create a WebSocket link: const link = new WebSocketLink({ uri: `ws://localhost:8080/v1alpha1/graphql`, options: { reconnect: true, connectionParams: { headers: { "x-hasura-admin-secret: "mylongsecretkey" } } } }) const cache = new InMemoryCache(); const client = new ApolloClient({ link, cache }); ReactDOM.render( ( <ApolloProvider client={client}> <App /> </ApolloProvider> ), document.getElementById('root') );

تم إعداد عميل Apollo. يمكننا الآن بسهولة استخدام GraphQL في الوقت الفعلي من تطبيقنا. توجه إلى src/App.js

بناء المخطط

يوفر ChartJS واجهة برمجة تطبيقات أنيقة جدًا لإنشاء المخططات. سنقوم ببناء مخطط خطي ؛ لذلك يتوقع المخطط الخطي بيانات النموذج:

 { "labels": ["label1", "label2", "label3", "label4"], "datasets": [{ "label": "Sample dataset", "data": [45, 23, 56, 55], "pointBackgroundColor": ["red", "brown", "green", "yellow"], "borderColor": "brown", "fill": false }], }

إذا تم استخدام مجموعة البيانات أعلاه لعرض مخطط خطي ، فسيبدو مثل هذا:

عينة مخطط خطي
عينة مخطط خطي (معاينة كبيرة)

دعونا نحاول بناء نموذج الرسم البياني هذا أولاً. قم باستيراد Line من react-chartjs-2 يمر على الكائن أعلاه كعنصر بيانات. ستبدو طريقة العرض بالشكل التالي:

 render() { const data = { "labels": ["label1", "label2", "label3", "label4"], "datasets": [{ "label": "Sample dataset", "data": [45, 23, 56, 55], "pointBackgroundColor": ["red", "brown", "green", "yellow"], "borderColor": "brown", "fill": false }], } return ( <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '20px'}} > <Line data={data} /> </div> ); }

بعد ذلك ، سنقوم بالاشتراك في البيانات الموجودة في وجهة نظرنا وإدخالها في المخطط الخطي. لكن كيف نقوم بعمل الاشتراكات على العميل؟

تعمل مكونات Apollo <Subscription> باستخدام نمط عرض الدعامة حيث يتم عرض العناصر الفرعية للمكون مع سياق بيانات الاشتراك.

 <Subscription subscription={gql`subscription { parent { child } }`} /> { ({data, error, loading}) => { if (error) return <Error error={error} />; if (loading) return <Loading />; return <RenderData data={data} />; } } </Subscription>

دعونا نستخدم أحد مكونات Subscription للاشتراك في وجهة نظرنا ثم تحويل بيانات الاشتراك إلى الهيكل الذي تتوقعه ChartJS. يبدو منطق التحويل كما يلي:

 let chartJSData = { labels: [], datasets: [{ label: "Max temperature every five seconds", data: [], pointBackgroundColor: [], borderColor: 'brown', fill: false }] }; data.last_20_min_temp.forEach((item) => { const humanReadableTime = moment(item.five_sec_interval).format('LTS'); chartJSData.labels.push(humanReadableTime); chartJSData.datasets[0].data.push(item.max_temp); chartJSData.datasets[0].pointBackgroundColor.push('brown'); })

ملاحظة : يمكنك أيضًا استخدام مكتبة Graphq2chartjs مفتوحة المصدر لتحويل البيانات من استجابة GraphQL إلى نموذج يتوقعه ChartJS.

بعد استخدام هذا داخل مكون الاشتراك ، يبدو App.js لدينا كما يلي:

 import React, { Component } from 'react'; import { Line } from 'react-chartjs-2'; import { Subscription } from 'react-apollo'; import gql from 'graphql-tag'; import moment from 'moment'; const TWENTY_MIN_TEMP_SUBSCRIPTION= gql' subscription { last_20_min_temp( order_by: { five_sec_interval: asc } where: { location: { _eq: "London" } } ) { five_sec_interval location max_temp } } ' class App extends Component { render() { return ( <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '20px'}} > <Subscription subscription={TWENTY_MIN_TEMP_SUBSCRIPTION}> { ({data, error, loading}) => { if (error) { console.error(error); return "Error"; } if (loading) { return "Loading"; } let chartJSData = { labels: [], datasets: [{ label: "Max temperature every five seconds", data: [], pointBackgroundColor: [], borderColor: 'brown', fill: false }] }; data.last_20_min_temp.forEach((item) => { const humanReadableTime = moment(item.five_sec_interval).format('LTS'); chartJSData.labels.push(humanReadableTime); chartJSData.datasets[0].data.push(item.max_temp); chartJSData.datasets[0].pointBackgroundColor.push('brown'); }) return ( <Line data={chartJSData} options={{ animation: {duration: 0}, scales: { yAxes: [{ticks: { min: 5, max: 20 }}]} }} /> ); } } </Subscription> </div> ); } } export default App;

سيكون لديك مخطط يعمل بالكامل في الوقت الفعلي جاهزًا على https://localhost:3000 . ومع ذلك ، سيكون فارغًا ، لذلك دعونا نملأ بعض عينات البيانات حتى نتمكن بالفعل من رؤية بعض السحر يحدث.

ملاحظة : لقد أضفت بعض الخيارات الأخرى إلى المخطط الخطي لأنني لا أحب تلك الرسوم المتحركة الرائعة في ChartJS. تبدو السلسلة الزمنية لطيفة عندما تكون بسيطة ، ومع ذلك ، يمكنك إزالة دعم الخيارات إذا أردت.

إدخال بيانات العينة

لنكتب نصًا يملأ قاعدة البيانات الخاصة بنا ببيانات وهمية. قم بإنشاء دليل منفصل (خارج هذا التطبيق) وأنشئ ملفًا يسمى script.js بالمحتوى التالي ،

 const fetch = require('node-fetch'); setInterval( () => { const randomTemp = (Math.random() * 5) + 10; fetch( `https://localhost:8080/v1alpha1/graphql`, { method: 'POST', body: JSON.stringify({ query: ` mutation ($temp: numeric) { insert_temperature ( objects: [{ temperature: $temp location: "London" }] ) { returning { recorded_at temperature } } } `, variables: { temp: randomTemp } }) } ).then((resp) => resp.json().then((respObj) => console.log(JSON.stringify(respObj, null, 2)))); }, 2000 ); أشياء: [{ const fetch = require('node-fetch'); setInterval( () => { const randomTemp = (Math.random() * 5) + 10; fetch( `https://localhost:8080/v1alpha1/graphql`, { method: 'POST', body: JSON.stringify({ query: ` mutation ($temp: numeric) { insert_temperature ( objects: [{ temperature: $temp location: "London" }] ) { returning { recorded_at temperature } } } `, variables: { temp: randomTemp } }) } ).then((resp) => resp.json().then((respObj) => console.log(JSON.stringify(respObj, null, 2)))); }, 2000 );

الآن قم بتشغيل هذين الأمرين:

 npm install --save node-fetch node script.js

يمكنك العودة إلى https://localhost:3000 ومشاهدة تحديث المخطط.

الانتهاء

يمكنك إنشاء معظم الرسوم البيانية في الوقت الفعلي باستخدام الأفكار التي ناقشناها أعلاه. الخوارزمية هي:

  1. نشر محرك GraphQL باستخدام Postgres ؛
  2. إنشاء الجداول حيث ترغب في تخزين البيانات ؛
  3. اشترك في هذه الجداول من تطبيق React الخاص بك ؛
  4. تقديم الرسم البياني.

يمكنك العثور على الكود المصدري هنا.