ORM com Prisma: modelagem e consultas

1. Introdução ao Prisma ORM

Prisma é um ORM (Object-Relational Mapping) moderno para Node.js e TypeScript que simplifica drasticamente a interação com bancos de dados relacionais. Diferente de alternativas como Sequelize (que usa uma abordagem baseada em classes e herança) ou TypeORM (que exige decorators e configurações complexas), o Prisma adota uma abordagem declarativa: você define seus modelos em um arquivo schema, e o Prisma gera automaticamente um cliente tipado e seguro.

No ecossistema React + Node.js, o Prisma se destaca por oferecer:
- Type safety nativo: consultas são verificadas em tempo de compilação
- Auto-complete: IDEs modernas sugerem campos e relacionamentos
- Migrations declarativas: mudanças no schema geram migrations automaticamente

O fluxo básico é: Schema → Migrations → Client → Queries. Você define os modelos, executa migrations para sincronizar o banco, gera o cliente e então realiza consultas tipadas.

2. Configuração inicial e instalação

Para começar, instale o Prisma CLI e as dependências necessárias:

npm install prisma @prisma/client
npx prisma init

O comando prisma init cria a estrutura de pastas:
- prisma/schema.prisma — arquivo de definição de modelos
- .env — variáveis de ambiente (ex: DATABASE_URL)

Configure o schema.prisma para usar PostgreSQL:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

No .env, defina a string de conexão:

DATABASE_URL="postgresql://usuario:senha@localhost:5432/meubanco"

3. Modelagem de dados com Prisma Schema

Vamos modelar um sistema de blog com usuários, posts e categorias:

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        Int       @id @default(autoincrement())
  title     String
  content   String?
  published Boolean   @default(false)
  author    User      @relation(fields: [authorId], references: [id])
  authorId  Int
  categories Category[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Category {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

Principais elementos:
- @id: chave primária
- @default(autoincrement()): auto incremento
- @unique: campo único
- @relation: define relacionamentos (aqui, 1:N entre User e Post)
- Campos opcionais: name String? (aceita null)
- Cardinalidade: Post[] indica que um User tem muitos posts

Para relacionamentos N:N (Post ↔ Category), o Prisma cria automaticamente uma tabela de junção.

4. Migrations e sincronização com o banco

Para criar e aplicar a primeira migration:

npx prisma migrate dev --name init

Este comando:
1. Compara o schema com o banco atual
2. Gera um arquivo SQL de migration em prisma/migrations/
3. Executa a migration no banco
4. Gera o Prisma Client

Para prototipagem rápida sem migrations:

npx prisma db push

Para aplicar migrations em produção:

npx prisma migrate deploy

O histórico de migrations fica em prisma/migrations/. Para rollback, use prisma migrate reset (cuidado: perde dados).

5. Prisma Client: consultas básicas

Instale e use o Prisma Client no Node.js:

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

// CREATE
const newUser = await prisma.user.create({
  data: {
    email: 'joao@email.com',
    name: 'João Silva',
    posts: {
      create: { title: 'Primeiro Post' }
    }
  }
})

// READ - único
const user = await prisma.user.findUnique({
  where: { email: 'joao@email.com' }
})

// READ - múltiplos com filtros
const publishedPosts = await prisma.post.findMany({
  where: { published: true },
  orderBy: { createdAt: 'desc' },
  take: 10,
  skip: 0,
  select: {
    id: true,
    title: true,
    author: { select: { name: true } }
  }
})

// UPDATE
const updatedPost = await prisma.post.update({
  where: { id: 1 },
  data: { published: true }
})

// DELETE
await prisma.post.delete({ where: { id: 1 } })

6. Consultas avançadas e relacionamentos

Para carregar relacionamentos (eager loading):

const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      include: { categories: true }
    }
  }
})

Filtros aninhados com AND/OR:

const filteredPosts = await prisma.post.findMany({
  where: {
    AND: [
      { published: true },
      { title: { contains: 'Prisma' } }
    ],
    OR: [
      { authorId: 1 },
      { authorId: 2 }
    ]
  }
})

Operações em lote e agregações:

// Atualizar múltiplos
await prisma.post.updateMany({
  where: { published: false },
  data: { published: true }
})

// Agregações
const stats = await prisma.post.aggregate({
  _count: { id: true },
  _avg: { authorId: true }
})

// Contagem com filtro
const total = await prisma.post.count({
  where: { published: true }
})

7. Integração com React e boas práticas

Crie uma API REST com Express que usa Prisma:

// controllers/postController.js
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

export const getPosts = async (req, res) => {
  try {
    const posts = await prisma.post.findMany({
      include: { author: { select: { name: true } } }
    })
    res.json(posts)
  } catch (error) {
    if (error instanceof PrismaClientKnownRequestError) {
      res.status(400).json({ error: error.message })
    } else {
      res.status(500).json({ error: 'Erro interno' })
    }
  }
}

Separe em camadas: controllers (HTTP), services (lógica de negócio) e repositórios (acesso a dados com Prisma). No frontend React, consuma a API:

// components/PostList.jsx
import { useEffect, useState } from 'react'

export function PostList() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    fetch('/api/posts')
      .then(res => res.json())
      .then(setPosts)
  }, [])

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title} - {post.author.name}</li>
      ))}
    </ul>
  )
}

8. Dicas de performance e manutenção

Para consultas complexas não suportadas pelo Prisma Client, use raw queries:

const rawResult = await prisma.$queryRaw`
  SELECT u.name, COUNT(p.id) as post_count
  FROM "User" u
  LEFT JOIN "Post" p ON u.id = p.author_id
  GROUP BY u.id
  HAVING COUNT(p.id) > 5
`

Adicione índices no schema para otimizar consultas frequentes:

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  published Boolean  @default(false)
  authorId  Int
  createdAt DateTime @default(now())

  @@index([authorId, published])
  @@index([createdAt])
}

Para produção, use prisma migrate deploy em vez de prisma migrate dev. Versionamento de migrations é essencial — nunca edite migrations já aplicadas em produção.


Referências