Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Schema e modelos no Prisma 7: campos e regras

Modele tabelas PostgreSQL com model, tipos, atributos, enums, map, defaults e constraints no schema.prisma, validando o SQL e um erro P1012 real.

Rodolfo Mori5 min de leitura

O schema.prisma descreve entidades, campos e relações que o Prisma usa para gerar Client e migrations. Num banco relacional, um model normalmente mapeia para uma tabela; tipos escalares viram colunas, e atributos como @id, @unique e @@index viram regras que o PostgreSQL consegue garantir.

Nesta lição, você vai modelar o catálogo de uma livraria e conferir o SQL criado, em vez de parar na aparência do arquivo. O ambiente deve estar instalado com o fluxo do Prisma 7 e adapter PostgreSQL.

A planta antes da obra: o modelo mental do schema

Imagine a planta de uma loja. Ela indica paredes, portas, medidas e áreas que precisam existir antes de alguém organizar as prateleiras. A planta não é a loja construída, mas uma obra executada sem ela tende a acumular decisões invisíveis.

No mapeamento técnico, o schema é a planta declarativa, uma migration é o plano de obra em SQL e o PostgreSQL é a estrutura construída. Prisma Client também lê a planta para gerar tipos e operações. O limite da analogia: um banco existente pode ser a fonte da verdade e gerar o schema por introspecção; nem todo projeto começa com uma folha em branco.

Voltando ao comportamento real, salvar schema.prisma não altera tabela. Você precisa validar, criar/aplicar migration e gerar o Client. São efeitos diferentes da mesma descrição.

Generator e datasource não são modelos

O arquivo começa com dois blocos de infraestrutura:

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

datasource db {
  provider = "postgresql"
}

generator decide qual artefato será produzido e onde. datasource seleciona o conector PostgreSQL. A URL da CLI fica em prisma.config.ts no Prisma 7. Esses blocos não viram tabela e não representam entidades da livraria.

Um model mínimo já contém três decisões

Comece por Livro:

text
model Livro {
  id     Int    @id @default(autoincrement())
  titulo String
  slug   String @unique

  @@map("livros")
}

model Livro usa singular e PascalCase na API. @@map("livros") escolhe o nome físico da tabela sem obrigar você a escrever prisma.livros. O campo id é inteiro, chave primária e recebe uma sequência automática. titulo não tem ?, portanto é obrigatório. slug precisa ser único.

Valide antes de migrar:

bash
npx prisma format
npx prisma validate
Prisma schema loaded from prisma/schema.prisma. The schema at prisma/schema.prisma is valid 🚀

format organiza a escrita; validate verifica referências, tipos e atributos. Nenhum dos dois garante que a regra de negócio é boa. Um título com um caractere é String válido, ainda que sua aplicação queira exigir três.

Tipos escalares precisam combinar com o domínio

Prisma oferece tipos como String, Int, BigInt, Boolean, DateTime, Decimal, Json e Bytes. O conector decide o mapeamento padrão; um atributo @db escolhe um tipo nativo mais específico.

text
model Livro {
  id        Int      @id @default(autoincrement())
  titulo    String
  preco     Decimal  @db.Decimal(10, 2)
  estoque   Int      @default(0)
  resumo    String?
  criadoEm  DateTime @default(now()) @map("criado_em")

  @@map("livros")
}

Decimal(10, 2) reserva dez dígitos no total e dois depois da vírgula. String? permite NULL; não significa string vazia. now() é avaliado pelo banco no momento da inserção. @map separa criadoEm usado no TypeScript de criado_em usado no PostgreSQL.

Uma regra que depende de comparação entre campos, como estoque nunca negativo, pode exigir SQL customizado na migration. O schema cobre muito, mas não representa toda constraint possível do PostgreSQL.

Enum fecha o vocabulário de um estado

Status não deve aceitar qualquer texto. Declare as alternativas:

text
enum StatusLivro {
  RASCUNHO
  PUBLICADO
  ESGOTADO
}

model Livro {
  id     Int         @id @default(autoincrement())
  titulo String
  status StatusLivro @default(RASCUNHO)
}

No PostgreSQL testado, a migration criou um enum nativo. No Client, o campo só aceita os três valores. Isso reduz erro de digitação, mas não decide quando um livro deve mudar de PUBLICADO para ESGOTADO; a aplicação ainda implementa a transição.

Atributos de campo e de model têm alcances diferentes

Uma arroba atua no campo; duas atuam na estrutura inteira:

text
model Livro {
  id       Int    @id @default(autoincrement())
  slug     String @unique
  titulo   String
  autorId  Int    @map("autor_id")

  @@index([autorId])
  @@unique([titulo, autorId])
  @@map("livros")
}

@unique cria unicidade de uma coluna. @@unique([titulo, autorId]) combina duas: uma autora não cadastra o mesmo título duas vezes, mas outra autora pode. @@index([autorId]) cria um índice comum para buscas e joins pela foreign key. Unicidade também cria estrutura de índice no PostgreSQL, embora a intenção principal seja garantir a regra.

Não adicione índice em toda coluna “por garantia”. Cada índice ocupa espaço e custa trabalho em escrita. Ele deve responder a consulta e seletividade reais.

Confira o SQL produzido

Com o schema completo do teste, migrate dev --name inicial gerou estas partes:

sql
CREATE TYPE "StatusLivro" AS ENUM ('RASCUNHO', 'PUBLICADO', 'ESGOTADO');

CREATE TABLE "livros" (
    "id" SERIAL NOT NULL,
    "titulo" TEXT NOT NULL,
    "slug" TEXT NOT NULL,
    "preco" DECIMAL(10,2) NOT NULL,
    "estoque" INTEGER NOT NULL DEFAULT 0,
    "status" "StatusLivro" NOT NULL DEFAULT 'RASCUNHO',
    "autor_id" INTEGER NOT NULL,
    "criado_em" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT "livros_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "livros_slug_key" ON "livros"("slug");
CREATE INDEX "livros_autor_id_idx" ON "livros"("autor_id");

Essa leitura fecha a ponte entre modelo e banco. String virou TEXT; ausência de ? virou NOT NULL; @default(0) virou default; @unique virou índice único. A lição de SQL CRUD ajuda a reconhecer as operações que usarão essa estrutura.

Modelos completos ligam nome da aplicação e nome físico

Agora inclua Autor e a relação que será aprofundada depois:

text
model Autor {
  id        Int      @id @default(autoincrement())
  nome      String
  email     String   @unique
  livros    Livro[]
  criadoEm  DateTime @default(now()) @map("criado_em")

  @@map("autores")
}

model Livro {
  id       Int     @id @default(autoincrement())
  titulo   String
  slug     String  @unique
  preco    Decimal @db.Decimal(10, 2)
  autorId  Int     @map("autor_id")
  autor    Autor   @relation(fields: [autorId], references: [id], onDelete: Restrict)

  @@index([autorId])
  @@map("livros")
}

Os campos autor e livros são relation fields usados pelo Prisma; autorId é o escalar persistido. A relation declara qual campo guarda a chave e qual coluna do outro model ele referencia. A regra exige o lado oposto para o Client navegar nos dois sentidos.

Um tipo inventado produz P1012

Prisma não aceita um tipo chamado Money só porque o domínio usa esse nome:

text
model Livro {
  id    Int   @id @default(autoincrement())
  preco Money
}
bash
npx prisma validate
Error code: P1012 error: Type "Money" is neither a built-in type, nor refers to another model, composite type, or enum. --> prisma/schema.prisma:13

Validation Error Count: 1 Prisma CLI Version : 7.9.1

Leia o diagnóstico por partes: Money não é escalar embutido, model, composite ou enum. Para dinheiro no PostgreSQL, troque pelo tipo conhecido Decimal e adicione @db.Decimal(10, 2). O erro aconteceu antes de qualquer ALTER TABLE, então o banco permaneceu intacto.

O Client comprova que o schema virou contrato

Depois de corrigir, migrar e gerar, a API conhece campos e retornos:

ts
const livro = await prisma.livro.create({
  data: {
    titulo: "Olhos d'água",
    slug: "olhos-dagua",
    preco: "49.90",
    autorId: 1,
  },
  select: { titulo: true, preco: true },
});

console.log({ titulo: livro.titulo, preco: livro.preco.toString() });
{ titulo: "Olhos d'água", preco: '49.9' }

O Decimal retornado não é um number comum; neste output ele foi convertido com toString. Se você acrescentar livro.campoInexistente, a checagem TypeScript falha porque o Client foi gerado do schema. Se alterar o model e esquecer prisma generate, a API continua antiga até regenerar.

Missão: modele uma editora e prove no SQL

Crie Editora com id, nome @unique, site? e criadoEm. Mapeie a tabela para editoras e a data para criado_em. Acrescente a Livro um editoraId, uma relation obrigatória e índice da foreign key.

bash
npx prisma format
npx prisma validate
npx prisma migrate dev --name adiciona_editora
npx prisma generate

Seu critério de sucesso tem quatro provas: validação passa; o SQL contém tabela, foreign key e índice; o Client exige editora ao criar Livro; e escrever site: 123 falha na checagem. Depois remova o campo oposto em Editora e observe o P1012 antes de corrigir.

Com o contrato gerado, o próximo passo é executar CRUD com Prisma Client e acompanhar como os métodos alteram linhas reais.

  • prisma schema
  • modelos prisma
  • schema prisma
  • postgresql
  • constraints
  • tipos prisma

Perguntas frequentes

Model no Prisma é igual a uma classe TypeScript?
Não. Um model descreve uma entidade persistida, gera tipos e operações do Client e normalmente mapeia para uma tabela. Ele não cria métodos de instância como uma classe da aplicação.
Qual a diferença entre ponto e arroba dupla no schema?
Atributos com uma arroba atuam num campo, como @id ou @unique. Atributos com duas atuam no model, como @@index, @@unique e @@map.
String com interrogação e string vazia são iguais?
Não. String? permite NULL no banco e string ou null no Client. Uma string vazia é um valor textual presente; se ela é inválida para o negócio, a aplicação precisa validar.
Devo usar Decimal para dinheiro?
Sim quando precisa de precisão decimal. Neste exemplo, Decimal com @db.Decimal(10, 2) vira NUMERIC(10,2) no PostgreSQL e evita representar preço como ponto flutuante binário.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, Prisma ORM 7.9.1, @prisma/adapter-pg 7.9.1, PostgreSQL 18.6 em postgres:18-alpine e Docker 29.5.3, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. Prisma Schema — models — prisma.io
  2. Prisma Schema — database mapping — prisma.io
  3. Prisma Schema — referência — prisma.io
  4. Prisma ORM — PostgreSQL connector — prisma.io

Continue por aqui