Comparação de ORMs

Usuários & Posts

GitHub

Gerenciando dados com Drizzle ORM

1 usuário

R

Rafael César

rafael@gmail.com

24 anos
Novo Usuário
Novo Post

Comparativo de código

Drizzle x Prisma

Schema e configuração lado a lado, com pontos rápidos sobre o que muda em cada ORM.

Schema — Drizzle
Tipagem direto em TypeScript, relations declaradas com helpers.
src/db/schema.ts
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { relations } from 'drizzle-orm'

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  email: text('email').unique().notNull(),
  birthDate: text('birth_date').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
})

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}))

export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  content: text('content').notNull(),
  userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
})

export const postsRelations = relations(posts, ({ one }) => ({
  user: one(users, {
    fields: [posts.userId],
    references: [users.id],
  }),
}))
Schema — Prisma
DSL própria com mapeamento para snake_case via @map.
prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "sqlite"
}

model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  birthDate String   @map("birth_date")
  createdAt DateTime @default(now()) @map("created_at")

  posts Post[]

  @@map("users")
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  userId    Int      @map("user_id")
  createdAt DateTime @default(now()) @map("created_at")

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("posts")
}
Config — Drizzle
Usa drizzle-kit apontando para o schema em TS e pasta de migrações local.
drizzle.config.ts
import 'dotenv/config'
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
  out: './drizzle',
  schema: './src/db/schema.ts',
  dialect: 'sqlite',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
})
Config — Prisma
Define schema, migrations em prisma/migrations e lê DATABASE_URL.
prisma.config.ts
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import 'dotenv/config'
import { defineConfig } from 'prisma/config'

export default defineConfig({
  schema: 'prisma/schema.prisma',
  migrations: {
    path: 'prisma/migrations',
  },
  datasource: {
    url: process.env['DATABASE_URL'],
  },
})

Clients

Instanciação das conexões

Como cada ORM cria o client conectado ao SQLite/libSQL e expõe helpers tipados.

Client — Drizzle
Conecta com @libsql/client, injeta o schema e reexporta tabelas.
src/db/index.ts
import 'dotenv/config'
import { drizzle } from 'drizzle-orm/libsql'
import { createClient } from '@libsql/client'
import * as schema from './schema'

const client = createClient({
  url: process.env.DATABASE_URL!,
})

export const db = drizzle(client, { schema })

export * from './schema'
Client — Prisma
Usa PrismaLibSql como adapter e instancia o client gerado.
src/db/prisma.ts
import { PrismaLibSql } from '@prisma/adapter-libsql'
import { PrismaClient } from '@/generated/prisma/client'

const adapter = new PrismaLibSql({
  url: process.env.DATABASE_URL!,
})

export const prisma = new PrismaClient({ adapter })

Requisições CRUD

Actions server lado a lado

Como o fluxo de criar, ler e deletar é escrito em cada ORM.

CRUD — Drizzle
Query builder em TS usando db.insert, db.query e helpers como eq.
src/app/drizzle/actions.ts
'use server'

import { db, users, posts } from '@/db'
import { revalidatePath } from 'next/cache'
import { eq } from 'drizzle-orm'

export async function createUserDrizzle(formData: FormData) {
  const name = formData.get('name') as string
  const email = formData.get('email') as string
  const birthDate = formData.get('birthDate') as string

  if (!name || !email || !birthDate) {
    return { error: 'Todos os campos são obrigatórios' }
  }

  try {
    await db.insert(users).values({
      name,
      email,
      birthDate,
    })
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    if (error instanceof Error && error.message.includes('UNIQUE')) {
      return { error: 'Este email já está cadastrado' }
    }
    return { error: 'Erro ao criar usuário' }
  }
}

export async function createPostDrizzle(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string
  const userId = formData.get('userId') as string

  if (!title || !content || !userId) {
    return { error: 'Todos os campos são obrigatórios' }
  }

  try {
    await db.insert(posts).values({
      title,
      content,
      userId: parseInt(userId),
    })
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    return { error: 'Erro ao criar post' }
  }
}

export async function updateUserDrizzle(formData: FormData) {
  const id = formData.get('id')
  const name = formData.get('name') as string
  const email = formData.get('email') as string
  const birthDate = formData.get('birthDate') as string

  const userId = Number(id)

  if (!id || Number.isNaN(userId)) {
    return { error: 'ID do usuário é obrigatório' }
  }

  const updateData: Partial<typeof users.$inferInsert> = {}

  if (name) updateData.name = name
  if (email) updateData.email = email
  if (birthDate) updateData.birthDate = birthDate

  if (Object.keys(updateData).length === 0) {
    return { error: 'Nenhum campo para atualizar' }
  }

  try {
    await db.update(users).set(updateData).where(eq(users.id, userId))
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    if (error instanceof Error && error.message.includes('UNIQUE')) {
      return { error: 'Este email já está cadastrado' }
    }
    return { error: 'Erro ao atualizar usuário' }
  }
}

export async function updatePostDrizzle(formData: FormData) {
  const id = formData.get('id')
  const title = formData.get('title') as string
  const content = formData.get('content') as string
  const userId = formData.get('userId') as string

  const postId = Number(id)
  const parsedUserId = userId ? Number(userId) : undefined

  if (!id || Number.isNaN(postId)) {
    return { error: 'ID do post é obrigatório' }
  }

  if (parsedUserId !== undefined && Number.isNaN(parsedUserId)) {
    return { error: 'Usuário do post inválido' }
  }

  const updateData: Partial<typeof posts.$inferInsert> = {}

  if (title) updateData.title = title
  if (content) updateData.content = content
  if (parsedUserId !== undefined) updateData.userId = parsedUserId

  if (Object.keys(updateData).length === 0) {
    return { error: 'Nenhum campo para atualizar' }
  }

  try {
    await db.update(posts).set(updateData).where(eq(posts.id, postId))
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    return { error: 'Erro ao atualizar post' }
  }
}

export async function getUsersDrizzle() {
  return await db.query.users.findMany({
    with: {
      posts: {
        orderBy: (posts, { desc }) => [desc(posts.createdAt)],
      },
    },
    orderBy: (users, { desc }) => [desc(users.createdAt)],
  })
}

export async function deleteUserDrizzle(userId: number) {
  try {
    await db.delete(users).where(eq(users.id, userId))
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    return { error: 'Erro ao deletar usuário' }
  }
}

export async function deletePostDrizzle(postId: number) {
  try {
    await db.delete(posts).where(eq(posts.id, postId))
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    return { error: 'Erro ao deletar post' }
  }
}
CRUD — Prisma
Client gerado com métodos declarativos (prisma.user.create, findMany) e inclusão de relações via include.
src/app/prisma/actions.ts
'use server'

import { prisma } from '@/db/prisma'
import { revalidatePath } from 'next/cache'

export async function createUserPrisma(formData: FormData) {
  const name = formData.get('name') as string
  const email = formData.get('email') as string
  const birthDate = formData.get('birthDate') as string

  if (!name || !email || !birthDate) {
    return { error: 'Todos os campos são obrigatórios' }
  }

  try {
    await prisma.user.create({
      data: {
        name,
        email,
        birthDate,
      },
    })
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    if (error instanceof Error && error.message.includes('Unique')) {
      return { error: 'Este email já está cadastrado' }
    }
    return { error: 'Erro ao criar usuário' }
  }
}

export async function createPostPrisma(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string
  const userId = formData.get('userId') as string

  if (!title || !content || !userId) {
    return { error: 'Todos os campos são obrigatórios' }
  }

  try {
    await prisma.post.create({
      data: {
        title,
        content,
        userId: parseInt(userId),
      },
    })
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    return { error: 'Erro ao criar post' }
  }
}

export async function updateUserPrisma(formData: FormData) {
  const id = formData.get('id')
  const name = formData.get('name') as string
  const email = formData.get('email') as string
  const birthDate = formData.get('birthDate') as string

  const userId = Number(id)

  if (!id || Number.isNaN(userId)) {
    return { error: 'ID do usuário é obrigatório' }
  }

  const data: {
    name?: string
    email?: string
    birthDate?: string
  } = {}

  if (name) data.name = name
  if (email) data.email = email
  if (birthDate) data.birthDate = birthDate

  if (Object.keys(data).length === 0) {
    return { error: 'Nenhum campo para atualizar' }
  }

  try {
    await prisma.user.update({
      where: { id: userId },
      data,
    })
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    if (error instanceof Error && error.message.includes('Unique')) {
      return { error: 'Este email já está cadastrado' }
    }
    return { error: 'Erro ao atualizar usuário' }
  }
}

export async function updatePostPrisma(formData: FormData) {
  const id = formData.get('id')
  const title = formData.get('title') as string
  const content = formData.get('content') as string
  const userId = formData.get('userId') as string

  const postId = Number(id)
  const parsedUserId = userId ? Number(userId) : undefined

  if (!id || Number.isNaN(postId)) {
    return { error: 'ID do post é obrigatório' }
  }

  if (parsedUserId !== undefined && Number.isNaN(parsedUserId)) {
    return { error: 'Usuário do post inválido' }
  }

  const data: {
    title?: string
    content?: string
    userId?: number
  } = {}

  if (title) data.title = title
  if (content) data.content = content
  if (parsedUserId !== undefined) data.userId = parsedUserId

  if (Object.keys(data).length === 0) {
    return { error: 'Nenhum campo para atualizar' }
  }

  try {
    await prisma.post.update({
      where: { id: postId },
      data,
    })
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    return { error: 'Erro ao atualizar post' }
  }
}

export async function getUsersPrisma() {
  return await prisma.user.findMany({
    include: {
      posts: {
        orderBy: { createdAt: 'desc' },
      },
    },
    orderBy: { createdAt: 'desc' },
  })
}

export async function deleteUserPrisma(userId: number) {
  try {
    await prisma.user.delete({
      where: { id: userId },
    })
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    return { error: 'Erro ao deletar usuário' }
  }
}

export async function deletePostPrisma(postId: number) {
  try {
    await prisma.post.delete({
      where: { id: postId },
    })
    revalidatePath('/')
    return { success: true }
  } catch (error) {
    return { error: 'Erro ao deletar post' }
  }
}
Análise
Observações focadas em DX, performance e operação.
  • Migrações: Drizzle-kit gera SQL legível e roda sem passo de geração; Prisma Migrate mantém histórico completo (bom para times), mas adiciona o passo prisma migrate/generate no CI.
  • Tipos & validação: Drizzle reaproveita os tipos das tabelas direto no código (bom para reutilizar em forms/DTOs); Prisma gera tipos via client (precisa rodar generate). Em ambos os casos, vale usar um validador de runtime (ex.: Zod) para não confiar só na tipagem.
  • Tamanho e deploy: Drizzle não precisa de client gerado e tende a ser mais leve em bundling/edge; Prisma client é robusto mas maior (cuidado com lambdas frias/edge functions).
  • Ergonomia de query: Drizzle favorece quem quer controle SQL (builder explícito, fácil de ver o SQL final); Prisma brilha em CRUD rápido com include/select e writes aninhados (create com posts: { create: [...] }).
  • Ferramentas: Prisma entrega Studio, introspecção e migrations integradas; Drizzle tem CLI simples e SQL direto (ótimo para revisar diffs de banco). Nos dois fluxos, use revalidatePath ou cache por tags para evitar re-fetch completo após mutações maiores.