Teu progresso
0 / 83 módulos0%
Estágio 02 · 02-10
BloqueadoORMs 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.
pg, mysql2): você escreve SQL strings, manda. Tipagem precisa de cuidado.Kysely, knex): você compõe queries com API chainable que gera SQL. Type-safe quando bem feito.Drizzle): definições de schema viram tipos; queries são estruturas tipadas que ainda parecem SQL. Mantém tradução explícita.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.
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);
Pontos:
Quando vence: você quer tipagem, controle do SQL, ergonomia razoável, sem mágica.
Lançado 2019. ORM com schema próprio em DSL .prisma:
model Order {
id String @id @default(cuid())
tenantId String
total Int
events OrderEvent[]
}
Code-gen produz @prisma/client com tipos. Queries:
const orders = await prisma.order.findMany({
where: { tenantId },
include: { events: true },
orderBy: { createdAt: 'desc' },
take: 20,
});
Pontos:
include faz JOINs ou múltiplas queries dependendo do caso.@prisma/client JavaScript-only (sem engine separado, mais leve, melhor pra edge).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.
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();
Pontos:
kysely-codegen).Quando vence: você quer type safety mas controle total do SQL, sem schema DSL.
ORMs antigos. Active Record ou Data Mapper patterns. Decorators-heavy:
@Entity()
class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
tenantId: string;
@ManyToOne(() => Customer)
customer: Customer;
}
Pontos:
Caso clássico:
const orders = await db.order.findMany();
for (const order of orders) {
const events = await db.orderEvent.findMany({ where: { orderId: order.id } });
}
Para N pedidos, 1 + N queries. Em prod, mata DB.
Solução:
include/select que JOIN ou faz IN (...) em segunda query.WHERE order_id IN (...), junta em código.Detectar: log SQL emitido, observar queries por request. Em Drizzle/Prisma você consegue habilitar log SQL.
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) => { ... });
Cuidado:
tx, não o client global. Senão queries vão fora da txn.drizzle-kit generate cria SQL a partir de diff. Você revisa antes de aplicar.Práticas:
ORM não substitui pool. Você ainda precisa:
pg.Pool em Node, ou pooler externo (PgBouncer/Supavisor) na frente.max connections corretamente: cores * 2 + spindle_count é heurística antiga. Em prática, comece com 10-20 por instância e meça.Toda lib decente expõe:
sql template tag.prisma.$queryRaw e $executeRaw.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}'`);
include: { everything: true } carrega árvore inteira; freq desnecessário.Drizzle tem 2 estilos:
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.
@faker-js/faker).COPY ou INSERT ... SELECT.ORM pra CRUD comum, raw pro resto. Composição é a regra.
N+1 query é o bug mais comum em apps com ORM. 1 query buscando 50 orders + 50 queries buscando courier de cada = 51 queries em vez de 1 JOIN. ORMs preguiçosos (eager loading default off) escondem custo. Em scale, vira 800ms p99 que era pra ser 8ms. Esta seção entrega: detecção (logger, Sentry, datadog APM), prevenção (eager via include/with), pattern DataLoader pra GraphQL, e query analysis production-ready com Drizzle/Prisma.
Anatomia — exemplo Logística:
// BAD — Drizzle/Prisma sem eager loading
const orders = await db.query.orders.findMany({
where: eq(orders.tenantId, tenantId),
limit: 50,
});
for (const order of orders) {
const courier = await db.query.couriers.findFirst({
where: eq(couriers.id, order.courierId),
});
// render
}
// 1 + 50 = 51 queries
Fix 1: eager via include/with (Drizzle):
const orders = await db.query.orders.findMany({
where: eq(orders.tenantId, tenantId),
limit: 50,
with: {
courier: true, // JOIN automático
items: { with: { product: true } }, // nested
},
});
// 1 query (Drizzle gera LEFT JOIN ou subquery dependendo do driver)
Prisma equivalente:
const orders = await prisma.order.findMany({
where: { tenantId },
take: 50,
include: {
courier: true,
items: { include: { product: true } },
},
});
Fix 2: SELECT IN batch quando JOIN é problemático. JOIN de 1 → N com 100 items vira cartesian product gigante. Fetch separado é mais eficiente:
const orders = await db.query.orders.findMany({ where: ... });
const courierIds = [...new Set(orders.map(o => o.courierId))];
const couriers = await db.query.couriers.findMany({
where: inArray(couriers.id, courierIds),
});
const courierMap = new Map(couriers.map(c => [c.id, c]));
const result = orders.map(o => ({ ...o, courier: courierMap.get(o.courierId) }));
// 2 queries em vez de 51
Fix 3: DataLoader pattern (GraphQL ou REST com nested):
import DataLoader from 'dataloader';
const courierLoader = new DataLoader<string, Courier>(async (ids) => {
const couriers = await db.query.couriers.findMany({
where: inArray(couriers.id, [...ids]),
});
const map = new Map(couriers.map(c => [c.id, c]));
return ids.map(id => map.get(id) ?? new Error(`Not found: ${id}`));
});
// Em GraphQL resolver
const Order = {
courier: (parent: Order) => courierLoader.load(parent.courierId),
};
// 50 .load() em mesma tick → 1 query batched
DataLoader rules:
Map + return ids.map(...) garante.Detection: logger + APM:
// Drizzle com logger
import { drizzle } from 'drizzle-orm/postgres-js';
import { sql } from 'postgres';
const client = sql(connectionString, {
onnotice: () => {},
});
const db = drizzle(client, {
logger: {
logQuery(query, params) {
const duration = (process.hrtime.bigint() / 1_000_000n);
log.debug({ query, params, duration }, 'sql');
},
},
});
// Detector simples — alert quando > 10 queries por request
import { AsyncLocalStorage } from 'async_hooks';
const requestQueries = new AsyncLocalStorage<{ count: number; queries: string[] }>();
db.$on('query', (e) => {
const ctx = requestQueries.getStore();
if (ctx) {
ctx.count++;
ctx.queries.push(e.query);
if (ctx.count > 30) {
log.warn({ count: ctx.count, queries: ctx.queries.slice(-5) }, 'possible N+1');
}
}
});
APM observability — Datadog/New Relic/Sentry:
dd-trace auto instruments Prisma/TypeORM/Sequelize. Span tree mostra "SELECT orders" + 50 child spans "SELECT courier WHERE id = X" — visualmente óbvio.@sentry/node + Sentry.Integrations.Postgres(). Em transaction view, expand SQL ops; N+1 vira colunas verticais idênticas.db.queries_per_request.p95 > 20.Query analysis ferramentas:
pg_stat_statements (Postgres): top queries por total_exec_time. N+1 aparece como 1 query repetida 1000x com small mean_exec_time mas alto total.drizzle-kit introspect + EXPLAIN ANALYZE: pega plano de query gerada pelo ORM, vê se é o esperado.Eager loading anti-patterns:
include/with: { items: true, courier: true, payments: true, lojista: true, history: true } quando query só usa items. Carrega 5x mais data, more JOINs, perf cai.include: { items: true, payments: true } com 50 items × 10 payments = 500 rows result. Postgres handles, mas client deserializa lentamente. Use 2 queries separadas.class Order { get courier() { return db.couriers.findFirst({ id: this.courierId }); } } — getter lazy chamado em loop = N+1 silencioso.maxBatchSize: 10k orders chamam .load → 1 query com 10k IN clause; Postgres explode parser.Best practice — query budget per endpoint:
db.queries_per_request per route como SLO.Logística stack pragmático:
// Drizzle + DataLoader + EXPLAIN sniffer
import { drizzle } from 'drizzle-orm/postgres-js';
import DataLoader from 'dataloader';
import { performance } from 'perf_hooks';
const SLOW_QUERY_MS = 100;
const db = drizzle(client, {
logger: {
logQuery(query, params) {
const start = performance.now();
// ...
},
},
});
// Per-request DataLoaders
export function createLoaders(ctx: RequestContext) {
return {
courier: new DataLoader<string, Courier>(async (ids) => {
const rows = await db.query.couriers.findMany({
where: inArray(couriers.id, [...ids]),
});
const map = new Map(rows.map(r => [r.id, r]));
return ids.map(id => map.get(id) ?? null);
}, { maxBatchSize: 1000 }),
product: new DataLoader<string, Product>(...),
};
}
// Em route handler
app.get('/orders', async (req, reply) => {
const loaders = createLoaders(req.ctx);
// ... use loaders
});
Cruza com 02-10 §2.11 (performance considerations base), 02-09 §2.7 (índices que o JOIN precisa), 02-09 §2.9 (EXPLAIN forensic em queries geradas), 04-05 §2.x (GraphQL DataLoader é canônico), 03-07 §2.x (APM detecta N+1 visualmente).
SaaS B2B prod outage tem custo direto: revenue perdida + SLA penalty + churn risk. "Maintenance window" morreu em 2026 — base de usuários global 24/7, expectativa 99.95%+ uptime. Schema migration é #2 cause of self-inflicted incidents (após deploy bugs). Esta seção entrega lifecycle completo expand-migrate-contract, cheat sheet de lock impact por operação Postgres, código copy-paste pra Drizzle 0.30+/Postgres 16, e timeline real Logística.
Three-step lifecycle (cada release deployable independente; rollback safe a qualquer momento):
Postgres lock impact cheat sheet:
| Operation | Lock | Safe? |
|---|---|---|
ADD COLUMN ... NULL | AccessExclusive instant | OK Postgres 11+ |
ADD COLUMN ... NOT NULL DEFAULT 'X' | AccessExclusive (rewrites) Postgres < 11 | Pre-PG11 perigoso; PG11+ fast |
ALTER COLUMN TYPE (compatible) | AccessExclusive instant | OK |
ALTER COLUMN TYPE (rewrites) | AccessExclusive long | Não — use new column |
DROP COLUMN | AccessExclusive instant (logical) | OK |
RENAME COLUMN | AccessExclusive instant | App ainda lê old name → 500s |
CREATE INDEX CONCURRENTLY | ShareUpdateExclusive | OK Long but non-blocking |
CREATE INDEX (blocking) | Share | Não — bloqueia writes |
ADD CONSTRAINT ... NOT VALID + VALIDATE CONSTRAINT | Two phases | OK Non-blocking |
ADD CONSTRAINT (validate) | AccessExclusive long | Não |
Pattern concreto — rename column (orders.payout_cents → orders.payout_value):
-- Phase 1 (Release N) — Expand
ALTER TABLE orders ADD COLUMN payout_value INTEGER;
-- App passa a escrever em AMBAS columns; lê de payout_cents (primary).
-- Backfill via background job (chunked, fora de migration):
UPDATE orders SET payout_value = payout_cents
WHERE id > $1 AND id <= $2 AND payout_value IS NULL;
-- Phase 2 (Release N+1) — Migrate
-- App flip reads pra payout_value primary; payout_cents fallback de safety.
-- NOT NULL via NOT VALID + VALIDATE (não-bloqueante):
ALTER TABLE orders ADD CONSTRAINT payout_value_not_null
CHECK (payout_value IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT payout_value_not_null;
-- Phase 3 (Release N+2) — Contract
ALTER TABLE orders DROP COLUMN payout_cents;
ALTER TABLE orders ALTER COLUMN payout_value SET NOT NULL;
ALTER TABLE orders DROP CONSTRAINT payout_value_not_null; -- redundante
Adicionar NOT NULL safe:
-- Naive (BLOQUEIA writes durante full table scan):
ALTER TABLE orders ALTER COLUMN status SET NOT NULL; -- NUNCA em prod com volume
-- Safe pattern:
ALTER TABLE orders ADD CONSTRAINT status_not_null
CHECK (status IS NOT NULL) NOT VALID; -- instant
ALTER TABLE orders VALIDATE CONSTRAINT status_not_null;
-- ShareUpdateExclusive only; reads/writes concorrentes OK
Adicionar foreign key safe:
ALTER TABLE orders ADD CONSTRAINT orders_courier_fk
FOREIGN KEY (courier_id) REFERENCES couriers(id) NOT VALID; -- instant
ALTER TABLE orders VALIDATE CONSTRAINT orders_courier_fk; -- non-blocking
Drizzle 0.30+ migrations integration:
// drizzle/0042_expand_payout.sql (Phase 1 — schema only)
ALTER TABLE orders ADD COLUMN payout_value INTEGER;
// drizzle/0044_payout_validate.sql (Phase 2 — constraint two-step)
ALTER TABLE orders ADD CONSTRAINT payout_value_check
CHECK (payout_value IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT payout_value_check;
// drizzle/0045_payout_contract.sql (Phase 3)
ALTER TABLE orders DROP COLUMN payout_cents;
Regra: migration files são schema-only. Backfill data move pra job separado (BullMQ / Inngest / Temporal); migration que tenta UPDATE em 100M rows estoura timeout do Drizzle migrator + bloqueia replication.
Backfill chunking pattern (BullMQ ou Inngest):
const CHUNK = 1000;
let lastId = 0;
while (true) {
const result = await db.execute(sql`
UPDATE orders SET payout_value = payout_cents
WHERE id > ${lastId} AND id <= ${lastId + CHUNK}
AND payout_value IS NULL
RETURNING id;
`);
if (result.rowCount === 0) break;
lastId += CHUNK;
// Throttle: monitora replication lag; pause se > 5s
const lag = await getReplicationLagSeconds();
await sleep(lag > 5 ? 5000 : 100);
}
Chunk size 1000-10000 rows balança latência vs throughput. Sempre com WHERE payout_value IS NULL (idempotente — job pode reiniciar). RETURNING id confirma rowCount sem extra round-trip.
Migration tooling 2026:
Logística applied — split orders.address em pickup_address + dropoff_address:
address ainda primary read.address; promote NOT NULL nas novas columns via VALIDATE.Anti-patterns observados:
ALTER COLUMN TYPE em column com millions of rows (rewrites table; lock 30min+).ADD COLUMN NOT NULL DEFAULT 'X' em Postgres < 11 (rewrites table inteira).RENAME COLUMN em release ativo (app code lê old name → 500s imediato).NOT VALID (long lock durante full validation table scan).CONCURRENTLY (writes blocked por minutos em large table).Cruza com 02-09 (Postgres lock semantics + autovacuum), 03-04 (CI/CD deployment strategy + migration gates), 04-04 (resilience, rollback strategy), 04-13 (lakehouse, schema evolution philosophy), 03-15 (incident response, migration-related incidents).
Em 2026 o tabuleiro mudou. Drizzle 0.36 (Q1 2026, relations v2 estável) virou default em greenfield TS — bundle ~7kb no edge, sem runtime engine, SQL-like API que não esconde nada. Prisma 6.x (Q4 2025) aposentou o Rust query engine, migrou pra TS query compiler nativo, apostou em Prisma Postgres (GA Q1 2025, Postgres serverless via unikernel) + Prisma Accelerate (connection pool global). Kysely 0.27 consolidou-se como "Drizzle without ORM-ness": query builder puro, codegen via kysely-codegen, escape hatch ideal pra CTEs e queries complexas.
Não é "qual é melhor". É qual encaixa em qual workload, considerando edge runtime, prepared statements, profundidade de type-inference e estratégia de connection pooling.
| Use-case | Default | Justificativa |
|---|---|---|
| Greenfield Next.js + RSC + Vercel/Cloudflare | Drizzle 0.36 + Neon HTTP | Bundle minúsculo no edge, zero overhead, RSC-native |
| Monorepo SaaS multi-tenant maduro com schema rico | Prisma 6 + Accelerate | Migrate maduro, RLS via $extends, Postgres GA |
| Heavy analytics, CTEs, window functions, JSON ops | Kysely 0.27 | Query builder fiel ao SQL, sem opinião relacional |
| Edge function CRUD simples + Hyperdrive | Drizzle ou Kysely | Prisma 6 ainda ~50kb, custo de cold start |
| Equipe vinda de Rails/Django, schema-driven | Prisma 6 | DSL declarativo, melhor onboarding |
| Lib publicada em npm (cliente final escolhe DB) | Kysely | Driver-agnostic, sem schema centralizado |
Stack Logística: Drizzle como default + Kysely como escape hatch pra queries com CTEs recursivas e janelamento (relatórios de SLA, percentis).
Relations v2 (Q1 2026) corrigiu o pecado original: joins agora geram SQL único com JSON aggregation, sem N+1 implícito. Type-narrowing preserva campos selecionados. RSC compat porque não há runtime engine — só funções puras.
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import { pgTable, uuid, text, integer, timestamp, relations } from 'drizzle-orm/pg-core';
import { eq, sql } from 'drizzle-orm';
export const tenants = pgTable('tenants', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
});
export const orders = pgTable('orders', {
id: uuid('id').primaryKey().defaultRandom(),
tenantId: uuid('tenant_id').notNull().references(() => tenants.id),
total: integer('total').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
// Relations v2 — nomeadas, type-safe, geram JOIN+JSON_AGG em query única
export const tenantsRelations = relations(tenants, ({ many }) => ({
orders: many(orders),
}));
export const ordersRelations = relations(orders, ({ one }) => ({
tenant: one(tenants, { fields: [orders.tenantId], references: [tenants.id] }),
}));
const sql_client = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql_client, { schema: { tenants, orders, tenantsRelations, ordersRelations } });
// RSC, edge runtime, zero TCP handshake, HTTP query
export async function getTenantWithOrders(tenantId: string) {
return db.query.tenants.findFirst({
where: eq(tenants.id, tenantId),
with: {
orders: {
columns: { id: true, total: true }, // type-narrow real
orderBy: (o, { desc }) => [desc(o.createdAt)],
limit: 50,
},
},
});
// SQL: SELECT t.*, json_agg(...) FROM tenants t LEFT JOIN LATERAL (...) o
}
// Prepared statement, evita re-parse no Postgres em hot path
const orderByIdStmt = db
.select()
.from(orders)
.where(eq(orders.id, sql.placeholder('id')))
.prepare('order_by_id');
await orderByIdStmt.execute({ id: 'uuid' });
Prisma 6 matou o Rust binary engine. O novo TS query compiler roda no mesmo processo Node/Bun, eliminando IPC overhead (~3-5ms saved por query). omit finalmente permite excluir campos default-selected. $extends virou ponto canônico pra RLS, soft delete, audit.
import { PrismaClient } from '@prisma/client';
import { withAccelerate } from '@prisma/extension-accelerate';
const prisma = new PrismaClient({
omit: { user: { passwordHash: true } }, // global default
}).$extends(withAccelerate()) // connection pool global, edge-compatible
.$extends({
query: {
$allModels: {
async $allOperations({ model, operation, args, query }) {
const tenantId = getTenantFromContext(); // RLS via app
if (operation === 'findMany' || operation === 'findFirst') {
args.where = { ...args.where, tenantId };
}
return query(args);
},
},
},
});
// Type-narrow real com omit + select
const order = await prisma.order.findUniqueOrThrow({
where: { id },
omit: { internalNotes: true },
include: { items: { select: { sku: true, qty: true } } },
});
// order.items[0].sku é typed; .price não compila
Prisma Postgres (GA Q1 2025): Postgres serverless via unikernel, scale-to-zero, billing por query. Trade-off: cold start ~100-300ms, lock-in no provider. Use pra projetos novos sem ops; evite pra workload latency-sensitive sustentado.
import { Kysely, PostgresDialect, sql } from 'kysely';
import { Pool } from 'pg';
import type { DB } from './db-types'; // gerado por kysely-codegen
export const db = new Kysely<DB>({
dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) }),
});
// CTE recursiva + window function — onde Drizzle/Prisma sofrem
const result = await db
.with('order_ranked', (qb) =>
qb.selectFrom('orders')
.select([
'id',
'tenant_id',
'total',
sql<number>`row_number() over (partition by tenant_id order by total desc)`.as('rank'),
])
.where('created_at', '>', new Date(Date.now() - 30 * 86_400_000)),
)
.selectFrom('order_ranked')
.selectAll()
.where('rank', '<=', 10)
.execute();
// Prepared statement explícito, reutilizável
const compiled = db.selectFrom('orders').selectAll().where('id', '=', sql.placeholder('id')).compile();
// compiled.sql + compiled.parameters → driver
// Raw SQL com type-safety por anotação
const stats = await sql<{ tenant: string; p95: number }>`
SELECT tenant_id AS tenant, percentile_cont(0.95) WITHIN GROUP (ORDER BY total) AS p95
FROM orders WHERE created_at > now() - interval '7 days' GROUP BY tenant_id
`.execute(db);
// Transaction explícita
await db.transaction().setIsolationLevel('repeatable read').execute(async (trx) => {
const order = await trx.selectFrom('orders').selectAll().where('id', '=', id).forUpdate().executeTakeFirstOrThrow();
await trx.updateTable('orders').set({ total: order.total + delta }).where('id', '=', id).execute();
});
| ORM | Neon HTTP | Cloudflare Hyperdrive | PlanetScale serverless | Bundle edge |
|---|---|---|---|---|
| Drizzle 0.36 | nativo | sim (TCP via Hyperdrive pool) | nativo | ~7kb |
| Prisma 6 | via Accelerate | via Accelerate | limitado | ~50kb (compiler TS) |
| Kysely 0.27 | adapter manual | sim | adapter | ~12kb |
Hyperdrive (Cloudflare GA 2024) faz TCP pooling no edge da Cloudflare — driver TCP normal (pg) funciona, sem cold start TCP handshake por request. Ainda assim, pra latência mínima absoluta no edge, HTTP-based (Neon HTTP, PlanetScale fetch) ganha — request vira fetch() único, sem manter conexão.
pgbouncer em transaction mode (default em Supabase, RDS Proxy) quebra prepared statements silenciosamente — cada transação pega conexão diferente, statement não persiste. Sintomas: prepared statement "s_1" does not exist intermitente sob carga.
Soluções:
pg com ?statement_cache_size=0, Drizzle/Kysely sem .prepare().include shape rígido.sql<number> tag, Drizzle ok via sql<T>, Prisma limita a _count/_sum.where mutável.sql; Prisma força $queryRaw sem types.// app/orders/page.tsx — Server Component, Drizzle no edge
import { db } from '@/lib/db';
export default async function OrdersPage({ params }: { params: { tenant: string } }) {
const orders = await db.query.orders.findMany({
where: (o, { eq }) => eq(o.tenantId, params.tenant),
with: { tenant: { columns: { name: true } } },
limit: 50,
});
return <OrderTable orders={orders} />;
}
// app/orders/actions.ts — Server Action
'use server';
export async function createOrder(formData: FormData) {
const tenantId = await getTenantFromSession();
await db.insert(orders).values({ tenantId, total: Number(formData.get('total')) });
revalidatePath('/orders');
}
Prisma 6 RSC compat: ok com Accelerate, mas bundle pesa em route handlers edge-runtime. Drizzle ganha em Vercel Edge / Cloudflare Workers por default.
findMany sem select/omit — payload 5-10x maior, bloat de network e memory..findMany sem with explícito quando precisa de relations — perde type-narrow, força segunda query.kysely-codegen em CI — schema drifta, types mentem em runtime.BEGIN/COMMIT sem ganho de atomicidade.COPY FROM STDIN ou INSERT ... VALUES ($1...),($N...) em batch.$extends do Prisma com lógica pesada (auth, logging síncrono) — roda em toda query, lateniza tudo.02-10 §2.2-§2.5 (intros individuais Drizzle/Prisma/Kysely/TypeORM), §2.6 (N+1), §2.10 (raw SQL escape hatch), §2.16 (N+1 deep + DataLoader), §2.17 (zero-downtime migrations), 02-09 (Postgres native features que ORMs expõem: CTEs, window, JSON), 02-13 (auth + RLS multi-tenant via $extends ou middleware Drizzle), 04-08 §2.22 (edge runtime constraints), 03-04 §2.21 (release-please pra version bumps de ORM lib).
Você precisa, sem consultar:
include/eager load gera JOIN vs query secundária e qual prefere por quê.Reescrever Logística API sobre ORM com tipagem forte, mantendo o schema do 02-09.
schema.ts.drizzle-kit generate.with).sql template (caso real onde Drizzle API não fica natural).POST /orders/:id/events deve atualizar orders.status E inserir em order_events na mesma transação.priority int NOT NULL DEFAULT 0.assigned_courier_id (FK nullable).order_events.event_type pra order_events.kind usando expand-contract: 2 migrations + ajuste no código entre elas.logger, Prisma log).$queryRaw/sql arbitrário pra contornar dificuldade, só onde realmente faz sentido (e justifique).embedding em produto/order com query de similaridade via sql template.Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Qual é o problema clássico N+1 em ORMs e como detectá-lo?
Q2Em uma transação Drizzle/Prisma, qual o erro comum a evitar?
Q3Em pgbouncer modo `transaction`, por que prepared statements falham silenciosamente?
Q4Em uma migration zero-downtime, qual o padrão correto para renomear `payout_cents` para `payout_value`?
Q5Sobre DataLoader pattern, qual regra é crítica para evitar bugs?
Destrava
02-10 é prereq dos seguintes módulos: