Реализация паттерна доступа к данным при работе с Drizzle

Введение

Погружаться в бэкенд-разработку с нуля  —  все равно что начинать с чистого листа. Только дело в том, что этот лист заполнен данными, и то, как вы с ними обращаетесь, может создать или разрушить проект.

Вы наверняка слышали выражение “данные  —  всему голова”. Оно как нельзя лучше подходит для разработки бэкенд-систем. Большинство из нас интуитивно понимают это, но часто не имеют подходящих инструментов для воплощения своего понимания в жизнь.

Поговорим о том, почему подход data-first является секретным оружием успешных бэкенд-проектов и как заставить его работать на вас, даже если вы новичок в разработке.

Недавно я спросил на X:

Начиная работать над бэкендом, что вы предпочитаете выполнять сначала: бизнес-логику или моделирование базы данных?

И был рад увидеть почти во всех комментариях один ответ: сначала нужно моделировать данные. На мой взгляд, это правильный ответ. Я задал этот вопрос, потому что использую паттерн, который мне очень помог. Речь идет о схематическом моделировании данных, которое можно реализовать с помощью нескольких TS-инструментов.

Drizzle для моделирования данных

Надеюсь, вы решили сначала заняться моделированием базы данных, поскольку заботитесь о производительности моделей. Лично я предпочитаю в первую очередь думать о паттернах доступа к данным, придерживаясь мнения, выраженного в большинстве комментариев: “Бэкенд может измениться, но данные и база данных останутся надолго”.

Поэтому рекомендую предпринять следующие шаги.

1. Определите, какие данные будете хранить и как будете обращаться к ним большую часть времени.

2. Определите точную схему, типы данных, свойства и т. д.

3. Реализуйте надлежащие стратегии индексирования и ограничений.

4. Сразу же попытайтесь написать запросы, чтобы понять, возможно ли это, насколько это сложно и т. д.

Drizzle значительно упрощает этот процесс, поэтому именно его мы применяем во всех проектах. Это позволяет быстро выполнять итерации и минимизировать ошибки. Кроме того, у нас уже есть схема, определенная на TypeScript, и некоторые запросы уже подготовлены в логике. Получается, что мы решили сразу несколько задач без каких-либо компромиссов. Можно просто “внедрить” схему и сразу приступить к тестированию.


Итак, как планировать бэкенд-разработку? Предположим, что у вас уже есть дизайн пользовательского интерфейса и все требования к нему, уже продумана архитектура инфраструктуры и нужна база данных SQL для основных бэкенд-функций.

Вот какие шаги я бы предпринял.

1. Создайте файл *.ts и начните моделировать схему с помощью системы Drizzle. Она похожа на SQL-запросы DDL-подмножеств: позволяет определять типы данных, ограничения, свойства и многое другое.

2. Если это важно для приложения, можно легко спланировать стратегии индексации и ограничений.

3. Определите отношения (любым удобным для вас способом, будь то реальные ограничения, использование “relations()” или комментарии).

4. В этом же файле можно создать мок-соединение с базой данных и визуализировать, как будут выглядеть запросы, основанные на необходимых вам схемах доступа к данным. Это поможет понять, подходит ли текущая схема. Если нет, то можно внести изменения в файл, и типы будут быстро выведены автоматически.


Все это позволяет оперативно выполнять моделирование данных в одном месте, избавляя от необходимости использовать множество инструментов, заметок и цифровых досок.

Кроме того, вам не придется изучать синтаксис собственных схем, что иногда бывает непросто. Вы получаете контроль над точными типами данных, отношениями и ограничениями, как и в случае с необработанными DDL-операторами в SQL. Кроме того, нет необходимости выполнять шаг генерации при каждом изменении, что упрощает процесс.

Пример

Допустим, ваша задача  —  построить API бэкенда для управления простой лентой, включающей пользователей, посты и ответы на эти посты.

Для начала необходимо создать .ts-файл, задокументировать все необходимые паттерны доступа, сымитировать функции базы данных, смоделировать подходящую вам схему и попытаться написать запрос, который, по вашему мнению, будет наиболее распространенным.

import { drizzle } from 'drizzle-orm/node-postgres';
import { index, integer, pgTable, text } from 'drizzle-orm/pg-core';

const users = pgTable('users', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
});

const posts = pgTable('post', {
id: integer('id').primaryKey(),
content: text('name').notNull(),
ownerId: integer('owner_id').references(() => users.id),
});

// Сымитируем базу данных
const db = drizzle({} as any);

// Нам нужно получить все посты от создателя постов
function getPostsWithOwner(userId: number) {

}
// Нужно открыть конкретный пост со всеми ответами
function getPostWithReplies(postId: number) {

}

Теперь можно приступить к созданию прототипов запросов, которые, по вашему мнению, будут использоваться для проверки того, какую стратегию работы с индексами/ограничениями следует выбрать.

import { eq } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/node-postgres';
import { index, integer, pgTable, text } from 'drizzle-orm/pg-core';

const users = pgTable('users', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
});

const posts = pgTable('post', {
id: integer('id').primaryKey(),
content: text('name').notNull(),
ownerId: integer('owner_id').references(() => users.id),
}, (t) => ({
// будем фильтровать по ownerId, проведем индексацию
ownerSearch: index('owner_search_idx').on(t.ownerId),
}));

// Сымитируем базу данных
const db = drizzle({} as any);

function getPostsWithOwner(userId: number) {
db.select().from(posts)
.leftJoin(users, eq(users.id, posts.ownerId))
.where(eq(posts.ownerId, userId));
}

function getPostWithReplies(postId: number) {
db.select().from(posts)
// Ответы на пост все еще не определены
.leftJoin(postReplies, eq(posts.id, postReplies.parentPost))
.where(eq(posts.id, postId));
}

Добавив индекс для столбца owner_id, вы наверняка заметили, что отсутствует таблица postReplies. Добавьте ее, чтобы обеспечить работу всех запросов. 

const postReplies = pgTable('post_replies', {
id: integer('id').primaryKey(),
content: text('name').notNull(),
// Нам нужен 1 "родительский" пост для всех ответов
parentPost: integer('parent_post').references(() => posts.id),
});

Итак, основные паттерны доступа к данным определены, схема, индексы и запросы проверены. Все было определено в одном файле, и все подсказки типов были изменены немедленно благодаря TypeScript.

Теперь можно использовать команду drizzle-kit push и внедрить схему в базу данных, что позволит протестировать ее на реальных данных.

Для получения дополнительной информации вы можете ознакомиться с Drizzle ORM и Drizzle Kit.

Заключение

Хотя поначалу схема Drizzle может показаться запутанной, непонятной и, возможно, “не такой хорошей, как собственный DSL”, после погружения в реальное моделирование данных и создание прототипов схем вы оцените истинную силу TypeScript-разработки на основе Drizzle. Такой подход не только позволяет использовать все возможности SQL в бэкенде, но и предлагает высокопродуктивный способ проектирования любых бэкендов и систем баз данных.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Andrii Sherman: The Data-Access-Pattern first approach with Drizzle

Предыдущая статьяПринципы SOLID в Kotlin
Следующая статьяYAML против JSON: какой формат эффективнее для работы с LLM?