Получение данных
Теперь, когда вы создали и заполнили базу данных, давайте обсудим различные способы получения данных для вашего приложения и создадим страницу обзора панели управления.
Выбор способа получения данных
API слой
API — это промежуточный слой между кодом вашего приложения и базой данных. Есть несколько случаев, когда вам может понадобиться API:
- Если вы используете сторонние сервисы, предоставляющие API.
- Если вы получаете данные на клиенте, вам нужен API слой, работающий на сервере, чтобы избежать раскрытия секретов базы данных клиенту.
В Next.js вы можете создавать конечные точки API с помощью Route Handlers.
Запросы к базе данных
При создании полноценного приложения вам также потребуется написать логику для взаимодействия с базой данных. Для реляционных баз данных, таких как Postgres, вы можете делать это с помощью SQL или ORM.
Есть несколько случаев, когда вам нужно писать запросы к базе данных:
- При создании конечных точек API вам нужно написать логику для взаимодействия с базой данных.
- Если вы используете серверные компоненты React (получение данных на сервере), вы можете пропустить API слой и запрашивать данные напрямую из базы данных, не рискуя раскрыть секреты базы данных клиенту.
Давайте узнаем больше о серверных компонентах React.
Использование серверных компонентов для получения данных
По умолчанию приложения Next.js используют серверные компоненты React. Получение данных с помощью серверных компонентов — относительно новый подход, и у него есть несколько преимуществ:
- Серверные компоненты поддерживают JavaScript Promises, предоставляя нативное решение для асинхронных задач, таких как получение данных. Вы можете использовать синтаксис
async/await
без необходимости вuseEffect
,useState
или других библиотеках для получения данных. - Серверные компоненты выполняются на сервере, поэтому вы можете оставить дорогостоящие запросы данных и логику на сервере, отправляя клиенту только результат.
- Поскольку серверные компоненты выполняются на сервере, вы можете запрашивать данные из базы данных напрямую без дополнительного API слоя. Это избавляет вас от написания и поддержки дополнительного кода.
Использование SQL
Для вашего приложения панели управления вы будете писать запросы к базе данных с помощью библиотеки postgres.js и SQL. Вот несколько причин, почему мы будем использовать SQL:
- SQL — это отраслевой стандарт для запросов к реляционным базам данных (например, ORM генерируют SQL под капотом).
- Базовое понимание SQL поможет вам понять основы реляционных баз данных, что позволит применять эти знания к другим инструментам.
- SQL универсален и позволяет получать и манипулировать конкретными данными.
- Библиотека
postgres.js
обеспечивает защиту от SQL-инъекций.
Не волнуйтесь, если вы раньше не использовали SQL — мы предоставили вам запросы.
Перейдите в /app/lib/data.ts
. Здесь вы увидите, что мы используем postgres
. Функция sql
позволяет выполнять запросы к вашей базе данных:
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
, вставьте следующий код и уделите время его изучению:
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`}>
Dashboard
</h1>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */}
{/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */}
{/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */}
{/* <Card
title="Total Customers"
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>
);
}
Код выше намеренно закомментирован. Теперь мы начнем разбирать каждую часть.
page
— это асинхронный серверный компонент. Это позволяет вам использоватьawait
для получения данных.- Также есть 3 компонента, которые получают данные:
<Card>
,<RevenueChart>
и<LatestInvoices>
. Они пока закомментированы и не реализованы.
Получение данных для <RevenueChart/>
Чтобы получить данные для компонента <RevenueChart/>
, импортируйте функцию fetchRevenue
из data.ts
и вызовите ее внутри вашего компонента:
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();
// ...
}
Далее сделайте следующее:
- Раскомментируйте компонент
<RevenueChart/>
. - Перейдите в файл компонента (
/app/ui/dashboard/revenue-chart.tsx
) и раскомментируйте код внутри него. - Проверьте
localhost:3000
, и вы должны увидеть график, использующий данныеrevenue
.

Продолжим импортировать больше данных и отображать их на панели управления.
Получение данных для <LatestInvoices/>
Для компонента <LatestInvoices />
нам нужно получить последние 5 счетов, отсортированных по дате.
Вы могли бы получить все счета и отсортировать их с помощью JavaScript. Это не проблема, пока данных мало, но по мере роста вашего приложения это может значительно увеличить объем передаваемых данных и JavaScript, необходимого для их сортировки.
Вместо сортировки последних счетов в памяти вы можете использовать SQL-запрос для получения только последних 5 счетов. Например, вот SQL-запрос из вашего файла 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
:
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:
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
Функция, которую вам нужно импортировать, называется fetchCardData
. Вам нужно будет деструктурировать значения, возвращаемые функцией.
Подсказка:
- Проверьте компоненты карточек, чтобы увидеть, какие данные им нужны.
- Проверьте файл
data.ts
, чтобы увидеть, что возвращает функция.
Когда вы будете готовы, разверните переключатель ниже для финального кода:
Отлично! Теперь вы получили все данные для страницы обзора панели управления. Ваша страница должна выглядеть так:

Однако... есть две вещи, о которых вам нужно знать:
- Запросы данных непреднамеренно блокируют друг друга, создавая водопад запросов.
- По умолчанию Next.js предварительно рендерит маршруты для повышения производительности, это называется статическим рендерингом. Поэтому, если ваши данные изменятся, это не отразится на вашей панели управления.
Давайте обсудим первый пункт в этой главе, а затем подробно рассмотрим второй пункт в следующей главе.
Что такое водопады запросов?
"Водопад" относится к последовательности сетевых запросов, которые зависят от завершения предыдущих запросов. В случае получения данных каждый запрос может начаться только после того, как предыдущий запрос вернет данные.

Например, нам нужно дождаться выполнения fetchRevenue()
, прежде чем fetchLatestInvoices()
сможет начать работу, и так далее.
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // ждем завершения fetchRevenue()
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData(); // ждем завершения fetchLatestInvoices()
Этот шаблон не обязательно плох. Могут быть случаи, когда вам нужны водопады, потому что вы хотите, чтобы условие было выполнено перед следующим запросом. Например, вы можете сначала получить ID и информацию о профиле пользователя. Получив ID, вы можете затем получить список его друзей. В этом случае каждый запрос зависит от данных, возвращенных предыдущим запросом.
Однако такое поведение может быть непреднамеренным и влиять на производительность.
Параллельное получение данных
Распространенный способ избежать водопадов — инициировать все запросы данных одновременно — параллельно.
В JavaScript вы можете использовать функции Promise.all()
или Promise.allSettled()
для одновременного запуска всех промисов. Например, в data.ts
мы используем Promise.all()
в функции fetchCardData()
:
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: что произойдет, если один запрос данных будет медленнее всех остальных? Давайте узнаем больше в следующей главе.