Next.js и PostgreSQL: серверные запросы через Drizzle
Оглавление курса · Скачать все состояния проекта.
Учебная задача: Описать постоянную схему заявки и отдельную подготовку миграций.
Входное состояние: Получение данных вынесено в серверный слой. Выходное состояние: Список и карточки читают договорную PostgreSQL-таблицу после её самостоятельной подготовки. Продолжаем «Смену» — учебную диспетчерскую заявок мастерской. Нужны основы JavaScript, TypeScript, React и HTTP. Используем App Router, Next 16.4.0, React 19.3.0 и Node.js не ниже 20.9.0. Основные версии закреплены; диапазоны остальных пакетов ещё не разрешены установкой и lockfile отсутствует. Полный каталог lesson-13 в архиве соответствует окончанию этой главы. Выбирайте один снимок целиком: перенос отдельных файлов из будущих этапов меняет его зависимости и правила данных.
Исходники подготовлены редакционным чтением. Пакеты, сервер, сборка, миграции, примеры и внешнее размещение не выполнялись. Ответы страницы и изменения данных ниже являются ожидаемыми, example_execution: not_run.
Массив в исходном модуле не хранит обращения между выпусками, а память процесса не объединяет несколько серверных экземпляров. Для следующего этапа «Смены» выберем PostgreSQL и Drizzle. База хранит строки, схема описывает их поля и ограничения, серверный слой выполняет запросы. Подключение не выполнялось, поэтому важно прочитать не только код запроса, но и условия подготовки таблицы в отдельной локальной среде.
Этот снимок ещё предшествует подключению входа. Он предназначен только для вымышленных публичных данных на собственной локальной базе. Не помещайте сюда рабочие обращения клиентов. Следующий этап требует личности, а затем добавляет ограничение владельцем. Такой порядок позволяет изучить схему отдельно, не выдавая промежуточное состояние за готовую систему персонального обслуживания.
Схема и идентификатор
Таблица tickets содержит автоматически создаваемый целочисленный id, заголовок до 160 символов, текст, статус, строковый ownerId и время создания. Статус описан PostgreSQL-перечислением open и closed. Значение по умолчанию — открытая заявка. Идентификатор выделяет база через identity, поэтому создание не должно вычислять «длина массива плюс один» и сталкиваться с повторением после удаления.
ownerId пока хранит текстовый идентификатор, который позже совпадёт с личностью Clerk. Здесь не существует внешнего ключа к локальной таблице пользователей, поскольку такой таблицы в схеме нет. Это осмысленная граница: удаление учётной записи во внешнем сервисе не запускает автоматически каскадное удаление наших заявок. Для реальной системы понадобился бы процесс согласования жизненного цикла личности и данных.
Timestamp содержит часовую зону и серверное значение по умолчанию. Время создания не означает время последнего изменения и не является версией записи. Мы не добавляем выдуманную историю действий. Если позже понадобится контроль конкурентного редактирования, потребуется дополнительный договор. Схема должна отражать именно сохраняемые величины, а не названия, которые хотелось бы видеть в интерфейсе.
Клиент соединения
Модуль db/client.ts имеет server-only и читает DATABASE_URL. Если строка не задана, он сообщает об отсутствующей настройке, а не подключается к случайной базе. Создаётся Pool node-postgres с ограничением в пять соединений. Drizzle получает этот клиент и использует его для запросов. В режиме разработки ссылка на pool сохраняется глобально, чтобы повторная загрузка модулей не создавала новые пулы без необходимости.
Глобальная ссылка относится к конкретному серверному процессу. Внешнее масштабирование создаст несколько экземпляров, каждый со своим ограничением соединений. Поэтому число 5 не является общим пределом для всей платформы. При выборе хостинга и сервера базы потребуется согласовать возможное число экземпляров и доступные подключения. Локальное удобство не доказывает устойчивость к любой нагрузке.
Строка подключения остаётся серверной переменной. Не добавляйте NEXT_PUBLIC_ ради того, чтобы библиотека её нашла: это изменит границу доступности и может отправить секрет в браузерный код. Публичному клиенту нужна форма или серверный ответ, а не прямые полномочия базы. В архиве поле переменной пустое и не содержит рабочих реквизитов.
История схемы не появляется от импорта
drizzle.config.ts задаёт PostgreSQL, путь схемы и каталог будущих миграций. Он сам не загружает .env; переменную надо передать окружению команд отдельно. Читатель сначала генерирует SQL миграций, читает результат и затем применяет его к выбранной базе. В этой работе ни генерация, ни применение не выполнялись. Поэтому в снимке нет выданной за проверенную истории SQL.
Файл db/README.md описывает этот порядок и отдельный учебный INSERT. Это инструкция для самостоятельного прохождения, а не журнал выполненных действий. Значение demo-author не совпадёт автоматически с будущим userId. После входа собственные записи появятся через форму или осмысленное назначение. Нельзя решить проблему владельца случайным созданием пользователя с похожим числом.
Запросы списка и карточки
Слой данных читает максимум 200 строк, сортируя id по убыванию. Ограничение защищает учебный интерфейс от неограниченного набора, но не является полной серверной пагинацией. Карточка получает положительный безопасный числовой id и ищет одну строку. И каноническая страница, и модальный маршрут переведены на эту функцию, чтобы источник совпадал независимо от способа открытия.
Перед появлением рабочей базы временные открытые комментарии HTTP удалены из последующих снимков. Публичный /api/demo и каталог сохраняют только исходные фикстуры. Они не начинают незаметно раскрывать PostgreSQL-данные. Такая явная смена источника важна: старый учебный обработчик не должен случайно стать вторым открытым входом к персональной системе.
Для самостоятельного разбора различите ошибку отсутствия переменной, отсутствующую таблицу и пустую корректную таблицу. У них разные причины и действия. Затем проследите источник данных обеих карточек. Ожидаемые строки появятся только после самостоятельной подготовки базы; код схемы сам по себе не доказывает её наличие и не даёт подтверждения исполнения.
Проверьте смысл границ
Генерация миграций и применение SQL должны относиться к одной выбранной схеме и среде. Если конфигурация инструмента получила одну строку подключения, а сервер другую, таблица может появиться в чужой базе, а приложение продолжит сообщать об отсутствии. Поэтому db/README отдельно указывает передачу окружения обеим сторонам.
Пустая таблица является нормальным исходным состоянием после подготовки схемы. Для её заполнения не требуется ослаблять будущие права всех пользователей. Учебный INSERT принадлежит этому локальному этапу; настоящие личные записи появятся из формы с серверным владельцем. Не смешивайте административное наполнение и обычное полномочие сотрудника.
Полные файлы этого шага
Следующие файлы показаны полностью. Остальные согласованные компоненты и настройки находятся в том же каталоге архива; сохраняйте их вместе при самостоятельном прохождении.
db/schema.ts
import { pgTable, pgEnum, integer, varchar, text, timestamp } from "drizzle-orm/pg-core";
export const ticketStatus = pgEnum("ticket_status", ["open", "closed"]);
export const tickets = pgTable("tickets", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
title: varchar("title", { length: 160 }).notNull(),
body: text("body").notNull(),
status: ticketStatus("status").notNull().default("open"),
ownerId: varchar("owner_id", { length: 128 }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
db/client.ts
import "server-only";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
const connectionString = process.env.DATABASE_URL;
if (!connectionString) throw new Error("Настройте DATABASE_URL для этого снимка");
const globalPool = globalThis as unknown as { smenaPool?: Pool };
export const pool = globalPool.smenaPool ?? new Pool({ connectionString, max: 5 });
if (process.env.NODE_ENV !== "production") globalPool.smenaPool = pool;
export const db = drizzle({ client: pool });
drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({ schema: "./db/schema.ts", out: "./drizzle", dialect: "postgresql", dbCredentials: { url: process.env.DATABASE_URL || "" } });
lib/data.ts
import "server-only";
import { desc, eq } from "drizzle-orm";
import { db } from "@/db/client";
import { tickets } from "@/db/schema";
import type { Ticket } from "@/lib/tickets";
export async function fetchTickets(): Promise<Ticket[]> { return db.select().from(tickets).orderBy(desc(tickets.id)).limit(200); }
export async function fetchNotice(): Promise<string> { return "Локальный учебный этап до подключения входа. Не используйте реальные персональные данные."; }
export async function findTicket(id: number): Promise<Ticket | undefined> { const [row] = await db.select().from(tickets).where(eq(tickets.id, id)); return row; }
db/README.md
# Подготовка базы читателем
Сначала выберите отдельную локальную PostgreSQL-базу и задайте DATABASE_URL в окружении команд и приложения. Конфигурация Drizzle не загружает .env самостоятельно. При самостоятельном прохождении читатель генерирует миграции командой db:generate, читает SQL и применяет db:migrate. Здесь миграции не сгенерированы и не применены. Данные настоящих клиентов в учебные состояния 01–13 не помещают. В 14 появляется обязательный вход, в 15 — серверное ограничение владельцем и ролью.
Пример SQL для локального состояния 13 после создания схемы: INSERT INTO tickets (title, body, owner_id) VALUES ('Настроить рабочее место', 'Подготовить принтер для приёмной.', 'demo-author'); Этот пример не выполнялся. После подключения Clerk демо-автор не совпадает с реальным userId; собственные записи создают через форму или назначают владельца осмысленно.
Документация и исходники
Договор этой главы раскрыт в официальной документации. Сопоставьте её API с выбранной версией и с объяснённым источником каждого значения. Архив «Смены» содержит двадцать полных состояний и условия самостоятельной подготовки. Ожидаемый результат оценивается по текущей главе: поздние проверки владельца, база и внешние ключи не являются неявно выполненными предпосылками раннего учебного состояния.