javascript

Drizzle ORM — современная типизированная ORM для реляционных БД в JS/TS

  • среда, 12 февраля 2025 г. в 00:00:06
https://habr.com/ru/articles/881242/

Удивился, что про столь популярный продукт не было статьи на Хабре, срочно это исправляю. 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.

Продолжая список особенностей, я бы выделил следующее:

  1. Строгая типизация схем и запросов, под каждый тип БД вы используете конкретные обертки специфичные для данной БД.

  2. Лёгкость в использовании, императивный подход (без ООП-оберток).

  3. Удобный CLI (Drizzle Kit) для миграций/сидирования БД

  4. Есть инструмент 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. Я веду Телеграм канал, где регулярно пишу про новые технологии, разработку, все ключевые вещи в мире ИИ/агентов, да и про технологический бизнес в целом. А ещё я часто даю там довольно глубокую аналитику по громким новостям. В общем, велком!