جلب البيانات

الآن بعد أن قمت بإنشاء وتعبئة قاعدة البيانات الخاصة بك، دعونا نناقش الطرق المختلفة التي يمكنك من خلالها جلب البيانات لتطبيقك، وبناء صفحة نظرة عامة على لوحة التحكم.

اختيار طريقة جلب البيانات

طبقة API

واجهات برمجة التطبيقات (APIs) هي طبقة وسيطة بين كود التطبيق الخاص بك وقاعدة البيانات. هناك بعض الحالات التي قد تستخدم فيها واجهة برمجة التطبيقات:

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

في Next.js، يمكنك إنشاء نقاط نهاية واجهة برمجة التطبيقات باستخدام معالجات المسارات (Route Handlers).

استعلامات قاعدة البيانات

عند إنشاء تطبيق كامل المكدس (full-stack)، ستحتاج أيضًا إلى كتابة منطق للتفاعل مع قاعدة البيانات الخاصة بك. بالنسبة لقواعد البيانات العلائقية مثل Postgres، يمكنك القيام بذلك باستخدام SQL أو مع ORM.

هناك بعض الحالات التي يجب فيها كتابة استعلامات قاعدة البيانات:

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

دعونا نتعلم المزيد عن مكونات خادم React.

استخدام مكونات الخادم لجلب البيانات

بشكل افتراضي، تستخدم تطبيقات Next.js مكونات خادم React. جلب البيانات باستخدام مكونات الخادم هو نهج جديد نسبيًا وهناك بعض الفوائد لاستخدامها:

  • تدعم مكونات الخادم وعود JavaScript (Promises)، مما يوفر حلاً للمهام غير المتزامنة مثل جلب البيانات بشكل أصلي. يمكنك استخدام بناء الجملة async/await دون الحاجة إلى useEffect أو useState أو مكتبات جلب بيانات أخرى.
  • تعمل مكونات الخادم على الخادم، لذا يمكنك الاحتفاظ بعمليات جلب البيانات المنهكة والمنطق على الخادم، وإرسال النتيجة فقط إلى العميل.
  • نظرًا لأن مكونات الخادم تعمل على الخادم، يمكنك الاستعلام مباشرة من قاعدة البيانات دون طبقة واجهة برمجة التطبيقات إضافية. هذا يوفر عليك كتابة وصيانة كود إضافي.

استخدام SQL

لتطبيق لوحة التحكم الخاص بك، ستكتب استعلامات قاعدة البيانات باستخدام مكتبة postgres.js و SQL. هناك عدة أسباب لاستخدام SQL:

  • SQL هو المعيار الصناعي لاستعلام قواعد البيانات العلائقية (مثلًا، تقوم ORMs بإنشاء SQL تحت الغطاء).
  • الفهم الأساسي لـ SQL يمكن أن يساعدك في فهم أساسيات قواعد البيانات العلائقية، مما يسمح لك بتطبيق معرفتك على أدوات أخرى.
  • SQL متعدد الاستخدامات، مما يسمح لك بجلب ومعالجة بيانات محددة.
  • توفر مكتبة postgres.js حماية ضد حقن SQL (SQL injections).

لا تقلق إذا لم تكن قد استخدمت SQL من قبل - لقد قمنا بتوفير الاستعلامات لك.

انتقل إلى /app/lib/data.ts. هنا سترى أننا نستخدم postgres. تتيح لك وظيفة sql الاستعلام من قاعدة البيانات الخاصة بك:

/app/lib/data.ts
import postgres from 'postgres';
 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 
// ...

يمكنك استدعاء sql في أي مكان على الخادم، مثل مكون الخادم. ولكن للسماح لك بالتنقل بين المكونات بسهولة أكبر، احتفظنا بجميع استعلامات البيانات في ملف data.ts، ويمكنك استيرادها إلى المكونات.

ملاحظة: إذا كنت قد استخدمت مزود قاعدة البيانات الخاص بك في الفصل 6، فستحتاج إلى تحديث استعلامات قاعدة البيانات للعمل مع مزودك. يمكنك العثور على الاستعلامات في /app/lib/data.ts.

جلب البيانات لصفحة نظرة عامة على لوحة التحكم

الآن بعد أن فهمت طرقًا مختلفة لجلب البيانات، دعونا نجلب البيانات لصفحة نظرة عامة على لوحة التحكم. انتقل إلى /app/dashboard/page.tsx، والصق الكود التالي، واقض بعض الوقت في استكشافه:

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
 
export default async function Page() {
  return (
    <main>
      <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        لوحة التحكم
      </h1>
      <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        {/* <Card title="المحصلة" value={totalPaidInvoices} type="collected" /> */}
        {/* <Card title="قيد الانتظار" value={totalPendingInvoices} type="pending" /> */}
        {/* <Card title="إجمالي الفواتير" value={numberOfInvoices} type="invoices" /> */}
        {/* <Card
          title="إجمالي العملاء"
          value={numberOfCustomers}
          type="customers"
        /> */}
      </div>
      <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
        {/* <RevenueChart revenue={revenue}  /> */}
        {/* <LatestInvoices latestInvoices={latestInvoices} /> */}
      </div>
    </main>
  );
}

الكود أعلاه معلق عمدًا. سنبدأ الآن في فحص كل جزء.

  • الصفحة هي مكون خادم غير متزامن (async). هذا يسمح لك باستخدام await لجلب البيانات.
  • هناك أيضًا 3 مكونات تستقبل البيانات: <Card> و <RevenueChart> و <LatestInvoices>. وهي معلقة حاليًا ولم يتم تنفيذها بعد.

جلب البيانات لمكون <RevenueChart/>

لجلب البيانات لمكون <RevenueChart/>، استورد وظيفة fetchRevenue من data.ts واستدعها داخل مكونك:

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  // ...
}

بعد ذلك، لنقم بما يلي:

  1. إلغاء تعليق مكون <RevenueChart/>.
  2. الانتقال إلى ملف المكون (/app/ui/dashboard/revenue-chart.tsx) وإلغاء تعليق الكود بداخله.
  3. تحقق من localhost:3000 ويجب أن ترى مخططًا يستخدم بيانات revenue.
مخطط الإيرادات يظهر إجمالي الإيرادات لآخر 12 شهرًا

دعونا نواصل استيراد المزيد من البيانات وعرضها على لوحة التحكم.

جلب البيانات لمكون <LatestInvoices/>

بالنسبة لمكون <LatestInvoices />، نحتاج إلى الحصول على آخر 5 فواتير، مرتبة حسب التاريخ.

يمكنك جلب جميع الفواتير وفرزها باستخدام JavaScript. هذه ليست مشكلة لأن بياناتنا صغيرة، ولكن مع نمو تطبيقك، يمكن أن يزيد ذلك بشكل كبير من كمية البيانات المنقولة في كل طلب و JavaScript المطلوب لفرزها.

بدلاً من فرز أحدث الفواتير في الذاكرة، يمكنك استخدام استعلام SQL لجلب آخر 5 فواتير فقط. على سبيل المثال، هذا هو استعلام SQL من ملف data.ts الخاص بك:

/app/lib/data.ts
// جلب آخر 5 فواتير، مرتبة حسب التاريخ
const data = await sql<LatestInvoiceRaw[]>`
  SELECT invoices.amount, customers.name, customers.image_url, customers.email
  FROM invoices
  JOIN customers ON invoices.customer_id = customers.id
  ORDER BY invoices.date DESC
  LIMIT 5`;

في صفحتك، استورد وظيفة fetchLatestInvoices:

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  // ...
}

ثم، ألغ تعليق مكون <LatestInvoices />. ستحتاج أيضًا إلى إلغاء تعليق الكود ذي الصلة في مكون <LatestInvoices /> نفسه، الموجود في /app/ui/dashboard/latest-invoices.

إذا قمت بزيارة localhost الخاص بك، يجب أن ترى أن آخر 5 فواتير فقط تم إرجاعها من قاعدة البيانات. نأمل أن تبدأ في رؤية مزايا الاستعلام مباشرة من قاعدة البيانات!

مكون أحدث الفواتير بجانب مخطط الإيرادات

تمرين: جلب البيانات لمكونات <Card>

الآن حان دورك لجلب البيانات لمكونات <Card>. ستعرض البطاقات البيانات التالية:

  • إجمالي مبلغ الفواتير المحصلة.
  • إجمالي مبلغ الفواتير قيد الانتظار.
  • إجمالي عدد الفواتير.
  • إجمالي عدد العملاء.

مرة أخرى، قد تميل إلى جلب جميع الفواتير والعملاء، واستخدام JavaScript لمعالجة البيانات. على سبيل المثال، يمكنك استخدام Array.length للحصول على إجمالي عدد الفواتير والعملاء:

const totalInvoices = allInvoices.length;
const totalCustomers = allCustomers.length;

ولكن باستخدام SQL، يمكنك جلب البيانات التي تحتاجها فقط. إنه أطول قليلاً من استخدام Array.length، ولكنه يعني نقل بيانات أقل أثناء الطلب. هذا هو البديل باستخدام SQL:

/app/lib/data.ts
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;

الوظيفة التي ستحتاج إلى استيرادها تسمى fetchCardData. ستحتاج إلى تفكيك القيم التي تم إرجاعها من الوظيفة.

تلميح:

  • تحقق من مكونات البطاقات لمعرفة البيانات التي تحتاجها.
  • تحقق من ملف data.ts لمعرفة ما تقوم الوظيفة بإرجاعه.

بمجرد أن تصبح جاهزًا، قم بتوسيع التبديل أدناه للحصول على الكود النهائي:

رائع! لقد قمت الآن بجلب جميع البيانات لصفحة نظرة عامة على لوحة التحكم. يجب أن تبدو صفحتك هكذا:

صفحة لوحة التحكم مع جميع البيانات التي تم جلبها

ومع ذلك... هناك شيئان تحتاج إلى أن تكون على علم بهما:

  1. طلبات البيانات تعيق بعضها البعض عن غير قصد، مما يخلق شلال طلبات (request waterfall).
  2. بشكل افتراضي، يقوم Next.js بالتصيير المسبق (prerenders) للمسارات لتحسين الأداء، وهذا يسمى التصيير الثابت (Static Rendering). لذا إذا تغيرت بياناتك، فلن تنعكس في لوحة التحكم الخاصة بك.

دعونا نناقش النقطة الأولى في هذا الفصل، ثم ننظر بالتفصيل إلى النقطة الثانية في الفصل التالي.

ما هي شلالات الطلبات؟

يشير "الشلال" إلى سلسلة من طلبات الشبكة التي تعتمد على اكتمال الطلبات السابقة. في حالة جلب البيانات، لا يمكن أن يبدأ كل طلب إلا بعد أن يعيد الطلب السابق البيانات.

مخطط يظهر الوقت مع جلب البيانات التسلسلي وجلب البيانات المتوازي

على سبيل المثال، نحتاج إلى انتظار تنفيذ fetchRevenue() قبل أن يتمكن fetchLatestInvoices() من البدء في التشغيل، وهكذا.

/app/dashboard/page.tsx
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // انتظر انتهاء fetchRevenue()
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // انتظر انتهاء fetchLatestInvoices()

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

ومع ذلك، يمكن أن يكون هذا السلوك غير مقصود ويؤثر على الأداء.

جلب البيانات المتوازي

طريقة شائعة لتجنب الشلالات هي بدء جميع طلبات البيانات في نفس الوقت - بشكل متوازي.

في JavaScript، يمكنك استخدام وظائف Promise.all() أو Promise.allSettled() لبدء جميع الوعود في نفس الوقت. على سبيل المثال، في data.ts، نستخدم Promise.all() في وظيفة fetchCardData():

/app/lib/data.ts
export async function fetchCardData() {
  try {
    const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
    const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
    const invoiceStatusPromise = sql`SELECT
         SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
         SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
         FROM invoices`;
 
    const data = await Promise.all([
      invoiceCountPromise,
      customerCountPromise,
      invoiceStatusPromise,
    ]);
    // ...
  }
}

باستخدام هذا النمط، يمكنك:

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

ومع ذلك، هناك عيب واحد للاعتماد فقط على نمط JavaScript هذا: ماذا يحدث إذا كان أحد طلبات البيانات أبطأ من البقية؟ دعونا نتعرف على المزيد في الفصل التالي.