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.
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:
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:
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:
npx prisma format
npx prisma validateformat 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.
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:
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:
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:
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:
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:
model Livro {
id Int @id @default(autoincrement())
preco Money
}npx prisma validateValidation 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:
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() });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.
npx prisma format
npx prisma validate
npx prisma migrate dev --name adiciona_editora
npx prisma generateSeu 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.
Perguntas frequentes
Model no Prisma é igual a uma classe TypeScript?
Qual a diferença entre ponto e arroba dupla no schema?
String com interrogação e string vazia são iguais?
Devo usar Decimal para dinheiro?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- Prisma Schema — models — prisma.io
- Prisma Schema — database mapping — prisma.io
- Prisma Schema — referência — prisma.io
- Prisma ORM — PostgreSQL connector — prisma.io


