02-10, ORMs
1. Problema de Engenharia
ORMs prometem produtividade trocando "SQL puro" por queries tipadas em código. O preço, quando você não sabe o que está fazendo: queries N+1, índices ignorados, migrations frágeis, abstrações sobre features do banco que vazam mal, time perdendo dias entendendo SQL gerado. ORMs são ferramenta. Quem domina entende exatamente o SQL emitido por cada chamada.
Este módulo compara as opções TS modernas, Drizzle, Prisma, Kysely, TypeORM, com raw SQL no fundo da pirâmide. Você sai sabendo escolher pra cada caso, ler o SQL gerado, prever o plano de execução, e identificar quando ORM vira ankle weight.
2. Teoria Hard
2.1 Espectro: raw SQL → query builder → ORM
Raw SQL com driver (pg, mysql2): você escreve SQL strings, manda. Tipagem precisa de cuidado.
Query builder (Kysely, knex): você compõe queries com API chainable que gera SQL. Type-safe quando bem feito.
Lightweight ORM (Drizzle): definições de schema viram tipos; queries são estruturas tipadas que ainda parecem SQL. Mantém tradução explícita.
Heavyweight ORM (Prisma, TypeORM, Sequelize, MikroORM): abstração mais alta. Schema central, query API próprio, lifecycle hooks, relations carregadas declarativamente.
Trade-off: quanto mais alto, mais ergonomia inicial, mais perda de controle e visibilidade.
2.2 Drizzle
Lançado 2022. Schema-first em TS . SQL-like API:
import { pgTable, uuid, text, integer } from 'drizzle-orm/pg-core';
export const orders = pgTable('orders', {
id: uuid('id').primaryKey().defaultRandom(),
tenantId: uuid('tenant_id').notNull(),
total: integer('total').notNull(),
});
const result = await db
.select({ id: orders.id, total: orders.total })
.from(orders)
.where(eq(orders.tenantId, tenant))
.orderBy(desc(orders.createdAt))
.limit(20);
ts Copy
Pontos:
SQL gerado é previsível: você praticamente escreve query.
Suporte a Postgres, MySQL, SQLite, e várias variantes (Neon, Cloudflare D1, Bun SQLite, etc.).
Drizzle Kit : migrations a partir de diff do schema TS.
Relations API (opcional) pra carregar associated rows.
Edge-friendly (HTTP drivers).
Quando vence: você quer tipagem, controle do SQL, ergonomia razoável, sem mágica.
2.3 Prisma
Lançado 2019. ORM com schema próprio em DSL .prisma:
model Order {
id String @id @default(cuid())
tenantId String
total Int
events OrderEvent[]
}
prisma Copy
Code-gen produz @prisma/client com tipos. Queries:
const orders = await prisma.order.findMany({
where: { tenantId },
include: { events: true },
orderBy: { createdAt: 'desc' },
take: 20,
});
ts Copy
Pontos:
DX excelente, autocomplete forte.
Prisma Studio : GUI pra DB.
Migrations declarativas com diff.
Queries N+1: include faz JOINs ou múltiplas queries dependendo do caso.
Engine separado (Rust binary) historicamente, em 2025 lançaram @prisma/client JavaScript-only (sem engine separado, mais leve, melhor pra edge).
Sem composição arbitrária de query. Para casos complexos, fallback é raw SQL via prisma.$queryRaw.
Quando vence: time grande precisa DX uniforme, queries não são especialmente complexas, valoriza tooling visual e migrations declarativas.
Críticas: SQL gerado às vezes é subótimo (queries adicionais quando JOIN seria suficiente). Performance em workloads pesados merece teste.
2.4 Kysely
Query builder puro, type-safe. Não é ORM:
const orders = await db
.selectFrom('orders')
.where('tenant_id', '=', tenant)
.orderBy('created_at', 'desc')
.select(['id', 'total'])
.limit(20)
.execute();
ts Copy
Pontos:
Tipos derivados de schema (você define interface ou usa kysely-codegen).
SQL emitido é exatamente o que você escreve.
Sem migrations próprias (use Atlas, sql migration files, ou Kysely migrator).
Plugin model.
Quando vence: você quer type safety mas controle total do SQL, sem schema DSL.
2.5 TypeORM e Sequelize
ORMs antigos. Active Record ou Data Mapper patterns. Decorators-heavy:
@Entity()
class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
tenantId: string;
@ManyToOne(() => Customer)
customer: Customer;
}
ts Copy
Pontos:
Maduros mas com baggage. Repos têm muitos issues abertos.
Inspirações Java/.NET (Hibernate). Pesados em decorators.
Em projetos novos TS, raramente é primeira escolha em 2026.
2.6 N+1 problem
Caso clássico:
const orders = await db.order.findMany();
for (const order of orders) {
const events = await db.orderEvent.findMany({ where: { orderId: order.id } });
}
ts Copy
Para N pedidos, 1 + N queries. Em prod, mata DB.
Solução:
ORM com include/select que JOIN ou faz IN (...) em segunda query.
Manualmente: 1 query pra orders, 1 pra events com WHERE order_id IN (...), junta em código.
DataLoader pattern (especialmente em GraphQL): batch + cache de chaves dentro de uma request.
Detectar: log SQL emitido, observar queries por request. Em Drizzle/Prisma você consegue habilitar log SQL.
2.7 Transactions
Toda lib decente expõe API de transação:
// Drizzle
await db.transaction(async (tx) => {
await tx.update(orders).set({ status: 'paid' }).where(eq(orders.id, id));
await tx.insert(orderEvents).values({ orderId: id, eventType: 'paid' });
});
// Prisma
await prisma.$transaction(async (tx) => { ... });
ts Copy
Cuidado:
Funções dentro da transação devem usar tx, não o client global. Senão queries vão fora da txn.
Long-running txn segura locks → contenção.
Transação distribuída (Postgres + Redis + outro serviço) não existe nativamente ; use outbox/saga.
2.8 Migrations
Drizzle Kit : drizzle-kit generate cria SQL a partir de diff. Você revisa antes de aplicar.
Prisma Migrate : similar, em SQL com nome humano.
Atlas (Ariga): tool agnóstico, suporta múltiplos ORMs/DBs, gera diff e suporte HCL.
sqitch , Flyway , Liquibase : SQL versionado tradicional.
Práticas:
Migrations são forward-only em prod. Rollback geralmente é nova migration que reverte.
Migrations devem ser transacionais (Postgres), todo SQL em single txn pra atomicidade.
Mudanças destrutivas (DROP COLUMN, RENAME) requerem dual-write ou janelas de manutenção. Vimos em 02-09.
Em deployment, migration roda antes do código novo subir, e código antigo deve continuar funcionando com schema novo (princípio expand-contract).
2.9 Connection pooling em código
ORM não substitui pool. Você ainda precisa:
pg.Pool em Node, ou pooler externo (PgBouncer/Supavisor) na frente.
Configurar max connections corretamente: cores * 2 + spindle_count é heurística antiga. Em prática, comece com 10-20 por instância e meça.
Em serverless: pooler externo é não-negociável. Drizzle e Prisma têm drivers HTTP-based pra Neon, Supabase, etc., que fazem isso bem.
2.10 Raw SQL escape hatch
Toda lib decente expõe:
Drizzle: sql template tag.
Prisma: prisma.$queryRaw e $executeRaw.
Kysely: sql template + dynamic builder.
Sempre use template tagged , nunca concat string com input do user, SQL injection.
// SEGURO
await db.execute(sql`SELECT * FROM orders WHERE id = ${userInput}`);
// VULNERÁVEL
await db.execute(`SELECT * FROM orders WHERE id = '${userInput}'`);
ts Copy
Select só o que precisa . ORMs default selecionam todas colunas. Em tables wide com TOAST columns, isso queima.
Avoid eager loading agressivo . include: { everything: true } carrega árvore inteira; freq desnecessário.
Cache de queries quentes : em camada de aplicação (LRU em memória) ou Redis.
Read replicas : alguns ORMs suportam roteamento read/write; ou faça manualmente.
Cursor-based pagination : pra listas longas, melhor que offset/limit em offsets grandes.
2.12 Type-level SQL: Drizzle relations vs raw join
Drizzle tem 2 estilos:
Query API "core" (SQL-like com JOIN explícito).
Query API "relational" (db.query.orders.findMany({ with: { events: true } })), declarativo.
Relational gera múltiplas queries internamente (1 pra orders, 1 pra events com IN). Em vez de JOIN. Trade-off: simpler payload, mais round-trips. Pra árvores rasas, ok. Pra árvores profundas, pondere.
2.13 Multi-database
Ler de Postgres + cache em Redis: 2 stores, 2 acessos.
Read da réplica + write no primary: lib de routing ou wrappers.
Sharded write: app divide tenant_id → shard. ORM precisa suportar (Drizzle suporta múltiplas dbs; Prisma menos elegante).
2.14 Test e seed
Test contra Postgres real (Docker, ou Testcontainers em CI). Não mocke o DB pra integration tests.
Reset por txn rollback : começa txn no setUp, dá rollback no tearDown. Rápido.
Schema temp : cada teste tem schema/database próprio. Mais lento mas isolado.
Seed scripts em SQL ou via lib (@faker-js/faker).
2.15 Quando ORM atrapalha
Queries analíticas pesadas (window functions, CTEs complexas, materialized views) → SQL puro.
Mass insert/update otimizado → COPY ou INSERT ... SELECT.
Geo queries (PostGIS) → SQL extension-aware.
Triggers, stored procedures, custom types → SQL/migrations diretas.
ORM pra CRUD comum, raw pro resto. Composição é a regra.
3. Threshold de Maestria
Você precisa, sem consultar:
Comparar Drizzle, Prisma, Kysely, TypeORM em 4 dimensões: tipagem, controle do SQL, migrations, edge-readiness.
Detectar N+1 num código de exemplo e propor 2 soluções.
Explicar quando include/eager load gera JOIN vs query secundária e qual prefere por quê.
Justificar por que pool de conexões importa apesar do ORM.
Escrever transação correta com Drizzle ou Prisma e explicar erro comum (usar client global em vez do tx).
Distinguir migrations declarativas (diff) vs imperativas (SQL versionado).
Explicar expand-contract pra migration sem downtime.
Defender quando trocar ORM por raw SQL.
Identificar SQL injection num código com template não-tagged.
Estratégia pra integration tests com DB real.
4. Desafio de Engenharia
Reescrever Logística API sobre ORM com tipagem forte, mantendo o schema do 02-09.
Especificação
Stack :
Node 22 + Fastify (do 02-08).
Drizzle ORM como principal, com Drizzle Kit pra migrations.
Postgres 16 (mesmo schema do 02-09, agora gerado por Drizzle).
Schema :
Reproduzir o schema do 02-09 em schema.ts.
Migrations geradas via drizzle-kit generate.
Comparar SQL gerado com o que você escreveu no 02-09 manualmente.
Endpoints com Drizzle :
Mesmos endpoints do 02-08, agora todas queries via Drizzle.
Pelo menos 2 endpoints usam JOIN explícito.
Pelo menos 1 endpoint usa relational API (with).
Pelo menos 1 endpoint cai pra sql template (caso real onde Drizzle API não fica natural).
N+1 demonstration :
Implemente intencionalmente 1 endpoint com N+1 (ex: lista pedidos + para cada um, busca eventos com query separada).
Use o logger SQL pra mostrar isso em log.
Refatore pra eliminar.
Transactions :
Endpoint POST /orders/:id/events deve atualizar orders.status E inserir em order_events na mesma transação.
Demonstre rollback intencional (joga erro entre os 2 statements).
Migration de schema realista :
Adicione coluna priority int NOT NULL DEFAULT 0.
Adicione coluna assigned_courier_id (FK nullable).
Renomeie order_events.event_type pra order_events.kind usando expand-contract: 2 migrations + ajuste no código entre elas.
Comparação Prisma :
Em pasta separada, defina o mesmo schema em Prisma.
Implemente 2 endpoints (lista pedidos com eventos; cria pedido em transação) em ambos.
Capture SQL emitido em ambos (Drizzle logger, Prisma log).
README documenta diferenças no SQL gerado e DX.
Restrições
Sem $queryRaw/sql arbitrário pra contornar dificuldade, só onde realmente faz sentido (e justifique).
Sem skip de validação de input (Zod/TypeBox antes do ORM).
Threshold
README documenta:
Decisão Drizzle vs Prisma com base no projeto.
SQL emitido em 5 queries representativas (copy do log).
Caso N+1 reproduzido + fix.
Comparação migration declarativa (Drizzle) vs imperativa (SQL puro do 02-09).
Estratégia de integration tests (DB Docker, txn rollback ou DB per test).
Stretch
Custom Postgres extension (pgvector) integrado: campo embedding em produto/order com query de similaridade via sql template.
Read replica routing: leituras vão pra réplica simulada; writes pro primary.
Migration zero-downtime real: deploy script que aplica migration → roda app antigo → sobe app novo (script orquestrado em bash, prova em local).
5. Extensões e Conexões
Liga com 02-09 (Postgres): você só usa ORM bem se entende o que ele gera.
Liga com 02-08 (frameworks): integration via plugins, lifecycle, error mapping.
Liga com 02-11 (Redis): cache de queries pesadas; invalidate ao escrever.
Liga com 02-13 (auth): row-level security policies em Postgres + ORM.
Liga com 03-01 (testes): integration test patterns, Testcontainers.
Liga com 03-04 (CI/CD): migration step antes de deploy.
Liga com 03-10 (perf): EXPLAIN sobre SQL gerado.
Liga com 04-03 (event-driven): outbox pattern: ORM escreve em domain table + outbox table na mesma txn.
6. Referências
Drizzle docs (orm.drizzle.team ).
Prisma docs (prisma.io/docs ).
Kysely docs (kysely.dev ).
"Database Internals" : Alex Petrov.
"SQL Antipatterns" : Bill Karwin (clássico de o que não fazer).
Markus Winand, "SQL Performance Explained" .
Atlas docs (atlasgo.io ), migration tooling.
Theo's video reviews sobre Drizzle/Prisma trade-offs.