Drizzle ORM в TypeScript

Если вы разработчик, который уже знаком с SQL, вы знаете, насколько неприятным может быть переход на ORM, требующий изучения совершенно нового языка запросов.

И если вы никогда раньше не слышали об ORM, то это, по сути, библиотека поверх базы данных. Обычно он обрабатывает как подключение к базе данных , так и все ваши взаимодействия с базой данных.

Проблема в том, что большинство ORM предоставляют уровни абстракции между вами и SQL, который вы пытаетесь написать. Это может быть противоречивая тема, но с некоторыми ORM мне хочется просто написать SQL напрямую, вместо того, чтобы тратить время на понимание конкретного синтаксиса библиотек. Возьмем это, например:

db.emailTable.findMany({
  where: {
    email: {
      endsWith: 'customer.com',
    },
  },
})

Чтение этого блока кода не является проблемой — вполне понятно, что он делает. Однако написать это — совсем другая история. Если бы мы захотели вместо этого написать это на SQL, мы бы получили что-то похожее на это:

SELECT * 
  FROM emails
  WHERE email like '%customer.com'

и попытка перейти от этого запроса к приведенному выше фрагменту кода может оказаться сложной задачей.

Применение Drizzle

Вот тут-то и приходит на помощь Drizzle — их философия проста: «если вы знаете SQL, вы знаете Drizzle ORM». Тот же самый запрос выше в Drizzle выглядит следующим образом:

db.select()
  .from(emailTable)
  .where(like(emailTable.email, "%customer.com"))
  .all()

и это действительно интересно! Вы получаете как синтаксис, который невероятно похож на синтаксис написания на чистом SQL, так и безопасность типов, а также автозаполнение, которые помогут вам выполнить запросы.

Но прежде чем мы забежим вперед, давайте рассмотрим еще несколько примеров, в том числе то, как мы можем в первую очередь определить нашу схему.

Настройка Drizzle

В вашем приложении вам необходимо установить Drizzle. Мы собираемся использовать SQLite, но есть также драйверы для Postgres и MySQL.

npm i drizzle-orm better-sqlite3
npm i -D drizzle-kit

Создание схемы

Давайте теперь создадим нашу первую схему в новой папке db/schema.ts:

import {integer, sqliteTable, text} from 'drizzle-orm/sqlite-core';

export const postsTable = sqliteTable('post', {
    id: integer('id').primaryKey(),
    username: text('username').notNull(),
    text: text('text').notNull(),
    createdAt: integer('created_at', {mode: 'timestamp'}).notNull().defaultNow(),
})

Это определяет вызываемую таблицу post, которая имеет идентификатор, имя пользователя, текст и метку времени. Если бы у вас был отдельный столбец пользователей, вместо этого вы могли бы использовать внешний ключ:

userId: text('user_id').references(() => users.id), // inline foreign key

Оттуда мы можем сказать, где находится база данных. Для SQLite мы можем просто использовать файл:

import {drizzle} from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';

// ...

const sqlite = new Database('sqlite.db');
export const db = drizzle(sqlite);

Последнее, что нам нужно сделать, это перенести базу данных, чтобы она соответствовала нашей схеме. Мы уже установили, drizzle-kit который будет генерировать для нас миграции:

$ npm exec drizzle-kit generate:sqlite --out migrations --schema db/schema.ts

1 tables
comments 4 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ migrations/0000_aspiring_whiplash.sql 🚀

Это просто генерирует файл миграции , но не настраивает для нас базу данных. Мы можем либо применить этот файл SQL самостоятельно, либо сделать это внутри приложения (обратно в файле schema.ts):

import {migrate} from "drizzle-orm/better-sqlite3/migrator";
// ...
migrate(db, {migrationsFolder: './migrations'});

И теперь, когда мы запустим наше приложение, база данных будет соответствовать определенной нами схеме. Давайте начнем его запрашивать.

Получение данных

Мы уже видели один пример получения данных, но теперь давайте добавим еще несколько вариантов. Вот пример запроса, который мы можем выполнить:

SELECT * 
    FROM post 
    ORDER BY created_at DESC
    LIMIT 10
    OFFSET 40

а с Drizzle это выглядит так:

export function fetchPosts(pageSize: number, pageNumber: number) {
    return db.select()
             .from(postsTable)
             .orderBy(desc(postsTable.createdAt))
             .limit(pageSize)
             .offset(pageSize * pageNumber)
             .all()
}

Это довольно легко читать, но что более важно, это было просто писать. Вы автоматически завершаете свой путь через методы, которые вы уже понимаете ( from, orderBy, limit, offset), потому что это всего лишь SQL.

Небольшое отступление: какой тип возвращать?

Drizzle позволяет InferModel вам получать явные типы для запроса и вставки данных.

export type Post = InferModel<typeof postsTable>
export type InsertPost = InferModel<typeof postsTable, 'insert'>

Чем они отличаются? InsertPost, например, не требует ввода данных, created_atпоскольку для него задан набор по умолчанию.

Теперь мы можем вернуться и обновить нашу функцию следующим типом:

export function fetchPosts(pageSize: number, pageNumber: number): Post[] {

Вставка данных

Здесь особо нечего сказать, кроме того, что это снова похоже на SQL-запрос:

export function createPost(insertPost: InsertPost): Post {
    return db.insert(postsTable)
             .values(insertPost)
             .returning()
             .get()
}

Настоящий тест ORM… Присоединяется

Совершенно очевидно, что Drizzle проделал отличную работу по согласованию синтаксиса SQL для выбора/вставки, но как насчет соединений? На мой взгляд, настоящая проверка ORM — это то, как он обрабатывает соединения, учитывая, насколько сложным может быть запрос.

Если мы немного перепишем нашу схему, чтобы имя пользователя было в отдельной user таблице:

export const usersTable = sqliteTable('user', {
    id: integer('id').primaryKey(),
    username: text('username').notNull(),
    // other fields that we don't care about
})

export const postsTable = sqliteTable('post', {
    id: integer('id').primaryKey(),
    userId: text('user_id').references(() => usersTable.id),
    text: text('text').notNull(),
    createdAt: integer('created_at', {mode: 'timestamp'}).notNull().defaultNow(),
})

Тогда мы можем написать такое соединение:

db.select({
  id: postsTable.id,
  username: usersTable.username,
  text: postsTable.text,
  createdAt: postsTable.text
})
  .from(postsTable)
  .innerJoin(usersTable, eq(postsTable.userId, usersTable.id))
  .all()

Вот что меня продало.

Я уже использовал ORM, когда чтение документации о том, как работают соединения, требует от вас прочитать длинные описания того, как работают отношения между таблицами. В Drizzle этот синтаксис кажется естественным и больше поддерживает мое желание просто безопасно писать SQL, а не заставлять меня изучать что-то новое.

Заключение

Drizzle — это новая захватывающая ORM Typescript, которая действительно доработала свой синтаксис. Если вы знаете SQL, вы можете невероятно быстро начать писать запросы.

Помимо простого выбора и вставки, Drizzle даже идеально подходит для синтаксиса соединений(join) и агрегаций, который трудно найти в пространстве ORM.