Drizzle ORM — современная типизированная ORM для реляционных БД в JS/TS
- среда, 12 февраля 2025 г. в 00:00:06
Удивился, что про столь популярный продукт не было статьи на Хабре, срочно это исправляю. 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
/ещё что-нибудь.
import { pgTable, varchar, serial, primaryKey } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
firstName: varchar(),
});
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' })
const result = await db.select().from(users);
// SELECT "id", "first_name" FROM users;
Для переиспользования структур между разными таблицами, можно пользоваться обычными 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. Я веду Телеграм канал, где регулярно пишу про новые технологии, разработку, все ключевые вещи в мире ИИ/агентов, да и про технологический бизнес в целом. А ещё я часто даю там довольно глубокую аналитику по громким новостям. В общем, велком!