Почему для музыкального сервиса с пользовательскими файлами я выбрал Drizzle ORM
Рассказываю, чем Drizzle помогает проекту с тысячами пользовательских треков, как она упрощает хранение метаданных и контроль жизненного цикла файлов
Мой новый музыкальный сервис генерирует и хранит тонны пользовательских данных: исходные mp3, платные wav, метаданные по срокам жизни и потоковым правам доступа. Нам нужен ORM, который не скрывает SQL, но при этом даёт безопасные типы и быстрые миграции. Именно поэтому я выбрал Drizzle ORM.
Контекст: какие задачи нужно закрыть
- Пользователь загружает или генерирует
mp3, а потом может докупить конверсию вwav. - Для бесплатных аккаунтов файлы живут 7 дней, дальше их нужно очищать без ручного вмешательства.
- Мы стримим треки по
Range-запросам и хотим понимать, какой формат хранится и где (локальный диск, сетевое хранилище, S3 в будущем). - В миграциях важно жёстко контролировать enum'ы и внешние ключи, иначе появятся «битые» файлы.
Почему не Prisma и не TypeORM
| Критерий | TypeORM | Prisma | Drizzle |
|---|---|---|---|
Контроль SQL (ENUM, ON DELETE CASCADE) | Ограничен декораторами | Нужны raw-запросы | Полноценные билдеры + raw |
| Runtime overhead | Декораторы/рефлексия | Генератор + клиент | Ноль рантайма, только compile-time |
| Структурирование схемы | Один монолитный файл | Одна модель целиком | Можно делить на модули (users.ts, tracks.ts) |
| Миграции | TypeORM CLI, медленно | prisma migrate, сложно кастомизировать | drizzle-kit, plain SQL/TS |
Для проекта, где схема растёт каждую неделю, важны простота миграций и типобезопасные SQL-конструкции без «магии». Здесь Drizzle выигрывает.
Схема под треки и их файлы
Drizzle даёт выразительный DSL, поэтому таблицы выглядят так же прозрачно, как и чистый SQL:
export const trackLifecycleEnum = pgEnum('track_lifecycle_state', ['active', 'expired', 'deleted']);
export const assetFormatEnum = pgEnum('track_asset_format', ['mp3', 'wav']);
export const tracks = pgTable('tracks', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id),
title: varchar('title', { length: 255 }).notNull(),
lifecycleState: trackLifecycleEnum('lifecycle_state').default('active').notNull(),
expiresAt: timestamp('expires_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const trackAssets = pgTable('track_assets', {
id: serial('id').primaryKey(),
trackId: integer('track_id').references(() => tracks.id, { onDelete: 'cascade' }).notNull(),
format: assetFormatEnum('format').notNull(),
isPremium: boolean('is_premium').default(false).notNull(),
storagePath: text('storage_path').notNull(),
storageProvider: varchar('storage_provider', { length: 64 }).notNull(),
expiresAt: timestamp('expires_at'),
lifecycleState: trackLifecycleEnum('lifecycle_state').default('active').notNull(),
});
Здесь сразу видно, какие состояния допускает система, а каскадное удаление гарантирует, что при очистке истёкшего трека мы не оставим «висящих» файлов.
Управление жизненным циклом и платной конверсией
- Enum'ы и
expiresAtпозволяют хранить правила на уровне базы: если файл платный (isPremium = true), мы просто не ставим дату истечения. - Можно добавить индексы (
index('track_assets_lifecycle_idx').on(table.lifecycleState, table.expiresAt)), чтобы воркеру очистки было достаточно одного запроса — никакой сложной ORM-логики. storageProviderфиксирует, где лежит файл. Как только мы подключим S3, достаточно добавить новое значение в конфиг, а схема останется прежней.
Typed-запросы для потоковой отдачи
Когда пользователь жмёт «Play», нам нужно быстро найти актуальный ассет и подготовить поток:
const asset = await db
.select()
.from(trackAssets)
.where(and(eq(trackAssets.trackId, trackId), eq(trackAssets.format, requestedFormat)))
.limit(1);
В ответе я получаю строго типизированный объект с путём и провайдером. Дальше StorageService решает, читать файл с локального диска или отдавать ссылку на сетевой NAS. Никаких any, никакой угадай-структуры.
Миграции без боли
drizzle-kit генерирует миграции на основании схемы, но всегда оставляет итоговые файлы читабельными. Для нас важно:
- Сгенерировать enum'ы и таблицы.
- Протянуть индексы под cron-задачи.
- Катить миграции через CI/CD без сюрпризов.
Drizzle делает это быстро, а в репозитории лежит чистый SQL/TypeScript, который понятно ревьюить. Если завтра мы решим хранить ещё и flac, я меняю один enum и перекатываю миграцию.
Итог
Drizzle ORM оказалась идеальной серединой между «ORM, который скрывает всё», и «ручным SQL». Для музыкального сервиса это значит:
- Типобезопасная и модульная схема, куда легко добавлять новые форматы.
- Чёткий контроль над жизненным циклом файлов и политиками хранения.
- Прозрачные миграции и отсутствие лишнего рантайм-кода.
В результате мы быстрее внедряем новые возможности (платный wav, автоудаление через 7 дней, стриминг с Range) и уверены, что данные пользователей ведут себя предсказуемо.