Удивился, что про стольпопулярный продукт не было статьи на Хабре, срочно это исправляю. Drizzle ORM — это #2 самая желаемая ORM по опросам, и она даже вошла в top 50 JavaScript Rising Stars 2024, заняв 27 место.
Что это такое?
Drizzle — это современная TypeScript/JavaScript ORM, которая работает со всеми основными реляционными БД (PostgreSQL, MySQL, SQLite и др.).
Её главные особенности: малый вес (~7.4kb), отсутствие внешних зависимостей и умение работать в различных средах (Node, serverless среды, браузер). Особенно тут стоит обратить внимание на serverless, что довольно нетипично для классических ORM'ок типа Sequelize/TypeORM.
Продолжая список особенностей, я бы выделил следующее:
Строгая типизация схем и запросов, под каждый тип БД вы используете конкретные обертки специфичные для данной БД.
Лёгкость в использовании, императивный подход (без ООП-оберток).
Удобный CLI (Drizzle Kit) для миграций/сидирования БД
Есть инструмент Drizzle Studio для работы с БД через графический интерфейс (дернуть тоже можно из CLI, особенно удобно для всяких БД типа Neon/Turso).
Как установить
Здесь всё просто:
npm run --save drizzle-orm
Для pnpm
/yarn
естественно всё аналогично. Собственно, для конкретной БД ещё потребуется драйвер: pg
/sqllite3
/ещё что-нибудь.
Примеры использования
1. Объявляем/создаем схему (Postgres):
import { pgTable, varchar, serial, primaryKey } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
firstName: varchar(),
});
2. Инициализация подключения
import { drizzle } from 'drizzle-orm/node-postgres';
const dbUrl = process.env.DATABASE_URL;
const db = drizzle(dbUrl);
// можно поменять правила наименования колонок через доп. параметр:
const db = drizzle({ connection: dbUrl, casing: 'snake_case' })
3. Делаем простой Select-запрос
const result = await db.select().from(users);
// SELECT "id", "first_name" FROM users;
4. Продвинутые примеры
Для переиспользования структур между разными таблицами, можно пользоваться обычными JS-примитивами, такими как spread-оператором:
// columns.helpers.ts
const timestamps = {
updated_at: timestamp(),
created_at: timestamp().defaultNow().notNull(),
deleted_at: timestamp(),
}
// users.sql.ts
export const users = pgTable('users', {
id: integer(),
...timestamps
})
// posts.sql.ts
export const posts = pgTable('posts', {
id: integer(),
...timestamps
})
В завершении можно привести пример полноценной схемы, с дефолтными значениями, ссылками на колонки в других таблицах, индексами и прочим:
import { AnyPgColumn } from "drizzle-orm/pg-core";
import { pgEnum, pgTable as table } from "drizzle-orm/pg-core";
import * as t from "drizzle-orm/pg-core";
export const rolesEnum = pgEnum("roles", ["guest", "user", "admin"]);
export const users = table(
"users",
{
id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
firstName: t.varchar("first_name", { length: 256 }),
lastName: t.varchar("last_name", { length: 256 }),
email: t.varchar().notNull(),
invitee: t.integer().references((): AnyPgColumn => users.id),
role: rolesEnum().default("guest"),
},
(table) => [
t.uniqueIndex("email_idx").on(table.email)
]
);
export const posts = table(
"posts",
{
id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
slug: t.varchar().$default(() => generateUniqueString(16)),
title: t.varchar({ length: 256 }),
ownerId: t.integer("owner_id").references(() => users.id),
},
(table) => [
t.uniqueIndex("slug_idx").on(table.slug),
t.index("title_idx").on(table.title),
]
);
export const comments = table("comments", {
id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
text: t.varchar({ length: 256 }),
postId: t.integer("post_id").references(() => posts.id),
ownerId: t.integer("owner_id").references(() => users.id),
});
Запросы
Базовый SQL-конструктор очень напоминает решения наподобие Knex.js или TypeORM QueryBuilder:
// получить пост с ID 10 с комментариями к нему:
await db
.select()
.from(posts)
.leftJoin(comments, eq(posts.id, comments.post_id))
.where(eq(posts.id, 10))
Или более продвинутый пример, похожий на реализацию поиска с фильтрами, которые выбирает юзер:
async function getProductsBy({
name,
category,
maxPrice,
}: {
name?: string;
category?: string;
maxPrice?: string;
}) {
const filters: SQL[] = [];
if (name) filters.push(ilike(products.name, name));
if (category) filters.push(eq(products.category, category));
if (maxPrice) filters.push(lte(products.price, maxPrice));
return db
.select()
.from(products)
.where(and(...filters));
}
Дополнительная фишка, которой я не встречал в других библиотеках это возможность заранее объявить подзапрос, и использовать его как аргумент источника данных (грубо говоря — как замену таблице):
const subquery = db
.select()
.from(internalStaff)
.leftJoin(customUser, eq(internalStaff.userId, customUser.id))
.as('internal_staff');
const mainQuery = await db
.select()
.from(ticket)
.leftJoin(subquery, eq(subquery.internal_staff.userId, ticket.staffId));
CRUD-операции:
// Insert
await db.insert(users).values({ name: "Маша" });
// Select
const result = await db.select().from(users);
// Update
await db.update(users).set({ name: "Вася" }).where(users.name.eq("Маша"));
// Delete
await db.delete(users).where(users.name.eq("Вася"));
Заключение
Это действительно легковесная и крутая обертка над ключевыми операциями с БД. Я очень много работал с Sequelize/TypeORM, и мне трудно было поверить, что я смогу получить удовольствие от объявления структур данных не используя классы/декораторы, и всё прочее.
Но я смог — Drizzle действительно дает крутой набор инструментов, в котором можно как легко выполнить все классические задачи, так и такие экзотичные штуки как обертки для union-запросов, типизированный шаблонизатор sql``
, и прочее.
Спасибо за внимание!
P.S. Я веду телеграм-канал, где регулярно пишу про новые технологии, разработку, все ключевые вещи в мире ИИ/агентов, да и про технологический бизнес в целом. А ещё я часто даю там довольно глубокую аналитику по громким новостям. В общем, велком!
Akina
Ну-ка, ну-ка... вот точно показанный код ORM отсылает на сервер ИМЕННО ПОКАЗАННЫЙ текст запроса? То есть ORM сперва запросит с сервера БД структуру таблицы или представления, на основе полученного результата сформирует список полей в запросе, и только потом выполнит собственно затребованный запрос? Проверяли?
Или всё же ORM отправит на сервер запрос
?
MaxLevs
Вы правы в том, что пример запроса выглядит странно. Однако запрос структуры тут ни при чем. Данных должно быть достаточно, так как
users
уже хранит описание таблицы.Но вот что точно смущает, так это то, что в запросе поля взяты как будто наобум. Обычно ожидается увидеть одно из трех:
Запрос всех "своих" полей без ссылочных сущностей
Запрос вообще всех полей со ссылками
Запрос через * (хотя я такое нн встречал в выхлопе ORM, но не удивился бы такое увидеть)
А вот в актуальном примере какая-то лажа.
Akina
Тогда вообще ничего не понял. Вот тот начальный код, где объявляется схема - это что, просто тупо хардкодится ожидаемая схема структуры хранения данных? И потом код с ней работает, вообще ничего не проверяя, авось никто структуру не поменял? И что будет, если в структуру были внесены изменения, а в код по какой-то причине ещё нет - если повезёт, то вылетим по ошибке, а если нет, то "Здравствуй, армагеддон"? Не-е-е, такой хоккей нам не нужен.
MaxLevs
А как вам запрос структуры поможет, если эту структуру абы как поменяли, например, до запуска приложения? В любом случае на этапе запроса получим ошибку, что схема не соответствует ожидаемой.
Вы извлечённые из базы данные потом как‑то должны обработать. И если эта обработка — не просто «получи и передай куда‑то ещё как‑есть», тогда вы ожидаете, что данные будут содержать определённые поля и хранить данные определённого типа, чтобы понимать, как это обрабатывать.
Для этого у вас должны быть гарантии, что вы извлекли то, что ожидали извлечь, и ничего неожиданного (например, данные из других полей) не получили. Для получения этих гарантий требуется выбрать, что будет источником истины: приложение или база — что‑то одно.
В данном случае источник истинны — код, в котором мы сами описываем ту схему данных, с которое это приложение умеет работать. И да, вы правильно заметили, что структура базы может поменяться, и надо как‑то обеспечить поддержку этих изменений в приложении. Для этого есть свои инструменты.
Например, можно использовать миграции для автоматического обновления структуры базы в соответствие с изменениями в описании структуры данных из кода(так как в этом примере именно он является источником истины), а саму базу не ломать.
Ещё можно использовать версионирование моделей данных, чтобы обеспечить обратную совместимость, а запросом структуры тогда уже решать, какую версию моделей брать. Но появление новой структуры всё равно требует изменений кода.
Описанные выше подходы были про Code First. Есть ещё DB first, когда мы извлекаем из базы описание структуры и, например, генерируем описание схемы и модели в коде.
Однако тут также требуется на этапе разработки после изменения схемы базы вызвать соответствующие инструменты. Так что это как будто ещё более нестабильно.
Как видим, код всё равно менять придётся. Так что я не совсем понимаю, что именно вы вкладывали в сценарий "кто-то поменял структуру"?
Akina
Понятно, вы - адепт подхода "вся логика на клиенте". Тогда мы точно не договоримся. Вопросы снимаются.
ElKornacio Автор
Каким образом "логика на клиенте" связана с тем, что источник информации о структуре БД - бекенд? Это не просто нормальная практика, это фактически доминирующий подход в современных ORM - когда структуру требуется поменять в реальной БД - делают миграцию, но главный источник этой структуры - всегда бекенд.
Причем здесь клиент я не понимаю.
savostin
Подразумевается, что базу создает разработчик бэкенда, а не какой-то там DBA. Т.е. схема пишется в Typescript, создаются миграции и накатываются в базу. И не дай бог кто-то там в базе что-то поправит.
ElKornacio Автор
Да, именно показанный текст запроса. Таблица
users
объявлена выше по тексту - в ней 2 колонки,id
иfirstName
. Разные ORM поступают по разному - какие-то действительно делают `select *`, а какие-то перечисляют колонки. Drizzle - перечисляет.