Aula 9: Consultas, transações e seed com Prisma
O modelo da aula 8 está completo, mas vazio, e o service ainda não sabe tirar proveito de nenhuma relação. Esta aula fecha o ciclo de persistência do Módulo 2: povoar o banco de forma reproduzível, consultar as relações sem cair no erro de desempenho mais comum de qualquer ORM, reescrever o service por completo e usar transações nas duas operações que só fazem sentido inteiras — emprestar e devolver um exemplar.
| O que vem depois | Onde |
|---|---|
| Autenticação, guards e proteção de rotas | Aula 10 — Autenticação e autorização |
| Consumo desta API pelo cliente web | Módulo 3 — Next.js |
| Consumo da mesma API pelo cliente mobile | Módulo 4 — Flutter |
Objetivos
Ao final desta aula, você deve ser capaz de:
- Escrever consultas com filtro, ordenação, paginação,
includeeselect, e reconhecer o problema N+1. - Reescrever um service completo sobre um modelo relacional, incluindo relações obrigatórias e opcionais na mesma consulta.
- Traduzir o conjunto completo de erros do banco (
P2002,P2025,P2003,P2000,P1001) em códigos de status HTTP. - Usar transações para operações que alteram mais de uma tabela, e distinguir o que uma transação garante do que ela não garante.
- Popular o banco com um script de seed reproduzível, incluindo dados que exercitam um relacionamento opcional vazio.
Ambiente sugerido
Mesmo ambiente das aulas 7 e 8 — nenhuma dependência nova entra nesta aula.
Parte 1 — Consultando
Filtro, ordenação e paginação
const livros = await this.prisma.livro.findMany({
where: {
ano: { gte: 1900 },
titulo: { contains: 'sertão', mode: 'insensitive' },
},
orderBy: { titulo: 'asc' },
skip: (pagina - 1) * tamanho,
take: tamanho,
});
mode: 'insensitive' resolve, no banco, o mesmo problema que na aula 6 era
resolvido com .toLowerCase() em JavaScript — e com uma diferença importante:
agora o filtro roda antes da paginação, sobre a tabela inteira. Filtrar em
memória depois de paginar dá resultados errados, e é um erro comum.
Trazendo relacionamentos: include e select
// include: o registro completo + as relações pedidas
const comAutor = await this.prisma.livro.findUnique({
where: { id },
include: { autor: true, editora: true, categorias: true },
});
// select: exatamente os campos listados, e nada mais
const resumo = await this.prisma.livro.findMany({
select: {
id: true,
titulo: true,
autor: { select: { nome: true } },
_count: { select: { exemplares: true } },
},
});
include | select | |
|---|---|---|
| Traz os campos escalares do modelo | Sim, todos | Só os listados |
| Serve para relações | Sim | Sim |
| Podem ser usados juntos no mesmo nível | Não | Não |
| Quando preferir | Você quer o registro inteiro mais as relações | Você quer economizar banda ou esconder campos |
select é a resposta direta ao overfetching discutido na aula 4: a tela do
aplicativo mobile que mostra só título e autor não precisa carregar o ISBN, as
datas e a observação interna de cada livro.
include e nullinclude: { editora: true } num livro sem editoraId devolve editora: null
— não undefined, não erro, não omite o campo. É o mesmo contrato de "existe,
mas pode ser vazio" que o schema da aula 8 desenhou; o cliente que consumir esse
JSON precisa tratar editora como possivelmente nulo, do mesmo jeito que trata
categorias como possivelmente uma lista vazia.
O problema N+1
Este é o erro de desempenho mais comum com ORM, e ele não dá nenhum sinal em desenvolvimento:
// Errado: 1 consulta para a lista + 1 por livro = N+1 consultas
const livros = await this.prisma.livro.findMany();
for (const livro of livros) {
livro.autor = await this.prisma.autor.findUnique({ where: { id: livro.autorId } });
}
// Certo: 1 consulta
const livros = await this.prisma.livro.findMany({ include: { autor: true } });
Com 3 livros de seed, a diferença é imperceptível. Com 500, são 501 idas ao banco
para montar uma tela. A regra prática: se você escreveu uma consulta dentro de
um laço, quase certamente ela deveria ser um include.
Parte 2 — O service, completo
Este é o LivrosService final do Módulo 2 — mesma assinatura pública desde a
aula 6, agora com as relações obrigatórias e a opcional na mesma consulta:
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '../generated/prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CriarLivroDto } from './dto/criar-livro.dto';
import { AtualizarLivroDto } from './dto/atualizar-livro.dto';
import { ConsultarLivrosDto } from './dto/consultar-livros.dto';
@Injectable()
export class LivrosService {
constructor(private readonly prisma: PrismaService) {}
async listar(consulta: ConsultarLivrosDto) {
const { autor, pagina, tamanho } = consulta;
const where: Prisma.LivroWhereInput = autor
? { autor: { nome: { contains: autor, mode: 'insensitive' } } }
: {};
// Uma transação de leitura: os dois resultados enxergam o mesmo estado.
const [dados, total] = await this.prisma.$transaction([
this.prisma.livro.findMany({
where,
include: { autor: true, editora: true },
orderBy: { titulo: 'asc' },
skip: (pagina - 1) * tamanho,
take: tamanho,
}),
this.prisma.livro.count({ where }),
]);
return {
dados,
pagina,
tamanho,
total,
totalDePaginas: Math.ceil(total / tamanho) || 1,
};
}
async buscarPorId(id: number) {
const livro = await this.prisma.livro.findUnique({
where: { id },
include: { autor: true, editora: true, categorias: true },
});
if (!livro) {
throw new NotFoundException(`Livro ${id} não encontrado`);
}
return livro;
}
async resumo() {
return this.prisma.livro.findMany({
select: {
id: true,
titulo: true,
autor: { select: { nome: true } },
_count: { select: { exemplares: true } },
},
orderBy: { titulo: 'asc' },
});
}
async criar(dto: CriarLivroDto) {
try {
return await this.prisma.livro.create({
data: dto,
include: { autor: true, editora: true },
});
} catch (erro) {
this.traduzirErro(erro, dto.isbn);
}
}
async atualizar(id: number, dto: AtualizarLivroDto) {
try {
return await this.prisma.livro.update({
where: { id },
data: dto,
include: { autor: true, editora: true },
});
} catch (erro) {
this.traduzirErro(erro, dto.isbn, id);
}
}
async remover(id: number): Promise<void> {
try {
await this.prisma.livro.delete({ where: { id } });
} catch (erro) {
// O mesmo P2003 significa coisas diferentes conforme a operação: ao
// criar, é o autor (ou a editora) que não existe; ao remover, são os
// empréstimos que ainda apontam para os exemplares deste livro. Os
// exemplares somem em cascata, mas Emprestimo usa o padrão (Restrict)
// e barra a exclusão.
if (
erro instanceof Prisma.PrismaClientKnownRequestError &&
erro.code === 'P2003'
) {
throw new ConflictException(
`Livro ${id} não pode ser removido: há empréstimos vinculados aos seus exemplares`,
);
}
this.traduzirErro(erro, undefined, id);
}
}
private traduzirErro(erro: unknown, isbn?: string, id?: number): never {
if (erro instanceof Prisma.PrismaClientKnownRequestError) {
switch (erro.code) {
case 'P2002':
throw new ConflictException(`ISBN ${isbn ?? ''} já cadastrado`.trim());
case 'P2025':
throw new NotFoundException(`Livro ${id ?? ''} não encontrado`.trim());
case 'P2003':
throw new ConflictException('Autor ou editora informados não existem');
}
}
throw erro;
}
}
Compare com a versão da aula 7 e repare no que mudou: include aparece em toda
consulta que devolve um livro, agora trazendo autor e editora — a
segunda pode vir null, e não é um caso de erro; resumo é novo, e usa
select com _count para devolver só o que uma listagem enxuta precisa;
traduzirErro ganhou o caso P2003 para criação (chave estrangeira inválida),
e remover ganhou um caso especial para o mesmo código, porque o significado
de "chave estrangeira violada" depende de qual operação o produziu.
E no que não mudou, mais uma vez: as rotas, os DTOs e os códigos de status que os clientes web e mobile já esperam.
Parte 3 — Erros do banco, o mapeamento completo
A aula 7 tratou P2002 e P2025, os únicos que existiam num schema sem relação.
Com chaves estrangeiras em campo, a tabela completa:
| Código | Significado | Status | Mensagem ao cliente |
|---|---|---|---|
P2002 | Violação de restrição única | 409 | Qual valor já existe |
P2025 | Registro não encontrado para a operação | 404 | Qual recurso não existe |
P2003 | Violação de chave estrangeira | 409 ou 400 | Qual vínculo é inválido |
P2000 | Valor longo demais para a coluna | 400 | Qual campo excedeu |
P1001 | Não foi possível alcançar o banco | 503 | Mensagem genérica |
P2003 não identifica qual vínculo falhou — só que algum falhou. Em
LivrosService.criar, ele pode vir de autorId ou de editoraId inválidos. Em
LivrosService.remover, ele vem de Emprestimo barrando a exclusão em cascata
dos exemplares. O código do banco é o mesmo; a mensagem correta depende de
qual operação o gerou, e só quem escreve o catch sabe disso.
A mensagem original do P2002 inclui o nome da tabela e da coluna
(Unique constraint failed on the fields: (isbn)). Isso descreve a estrutura
interna do banco, que não faz parte do contrato e não deveria ser conhecida por
quem consome a API. Traduza para o vocabulário do domínio — foi o que a Parte 2
fez, e é a mesma regra do filtro de exceções da aula 6.
Parte 4 — Transações
Algumas operações só fazem sentido inteiras. Emprestar um exemplar é uma delas: grava o empréstimo e muda a situação do exemplar. Se a segunda falhar depois da primeira, o acervo fica com um exemplar emprestado que consta como disponível.
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
const PRAZO_EM_DIAS = 14;
@Injectable()
export class EmprestimosService {
constructor(private readonly prisma: PrismaService) {}
async emprestar(exemplarId: number, leitorId: number) {
return this.prisma.$transaction(async (tx) => {
const exemplar = await tx.exemplar.findUnique({ where: { id: exemplarId } });
if (!exemplar) {
throw new NotFoundException(`Exemplar ${exemplarId} não encontrado`);
}
if (exemplar.situacao !== 'DISPONIVEL') {
throw new BadRequestException(`Exemplar ${exemplarId} não está disponível`);
}
const previstoPara = new Date();
previstoPara.setDate(previstoPara.getDate() + PRAZO_EM_DIAS);
const emprestimo = await tx.emprestimo.create({
data: { exemplarId, leitorId, previstoPara },
include: { exemplar: { include: { livro: true } }, leitor: true },
});
await tx.exemplar.update({
where: { id: exemplarId },
data: { situacao: 'EMPRESTADO' },
});
return emprestimo;
});
}
}
Duas formas de transação, com usos diferentes:
| Forma | Sintaxe | Quando usar |
|---|---|---|
| Em lote | $transaction([consultaA, consultaB]) | As operações são independentes e conhecidas de antemão |
| Interativa | $transaction(async (tx) => ...) | Uma operação depende do resultado da anterior |
tx, não this.prismaUma consulta feita com this.prisma dentro do bloco roda fora da transação —
ela não enxerga as alterações pendentes e não é desfeita no rollback. O erro é
silencioso e só aparece sob concorrência. Se a variável se chama tx, use tx.
Transações interativas seguram uma conexão aberta e têm tempo limite. Nunca
coloque dentro delas chamadas a serviços externos — envio de e-mail, requisição
HTTP, upload. Faça isso depois do commit.
A devolução é a mesma transação, na direção inversa
Devolver um exemplar também grava em duas tabelas: marca a devolução no
empréstimo e libera o exemplar. E acrescenta duas verificações: um
empréstimo já devolvido não pode ser devolvido de novo, e só um exemplar que
está de fato EMPRESTADO pode voltar à circulação.
async devolver(id: number) {
return this.prisma.$transaction(async (tx) => {
// A situação do exemplar faz parte da decisão, então vem junto.
const emprestimo = await tx.emprestimo.findUnique({
where: { id },
include: { exemplar: true },
});
if (!emprestimo) {
throw new NotFoundException(`Empréstimo ${id} não encontrado`);
}
if (emprestimo.devolvidoEm) {
throw new BadRequestException(`Empréstimo ${id} já foi devolvido`);
}
if (emprestimo.exemplar.situacao !== 'EMPRESTADO') {
throw new BadRequestException(
`Exemplar ${emprestimo.exemplarId} não está emprestado`,
);
}
const devolvido = await tx.emprestimo.update({
where: { id },
data: { devolvidoEm: new Date() },
});
await tx.exemplar.update({
where: { id: emprestimo.exemplarId },
data: { situacao: 'DISPONIVEL' },
});
return devolvido;
});
}
emprestar só deixa sair um exemplar DISPONIVEL; devolver só deixa voltar
um EMPRESTADO. As duas guardam a mesma invariante — empréstimo em aberto
se e somente se exemplar EMPRESTADO —, cada uma de um lado. Encontrar um
empréstimo em aberto cujo exemplar está EM_REPARO, BAIXADO ou
DISPONIVEL significa que ela já foi rompida em algum outro ponto do sistema;
gravar DISPONIVEL por cima faria a API parecer funcionar e apagaria a
evidência.
Repare que essa é uma decisão de domínio, não uma imposição do framework. No seu domínio, a pergunta equivalente é qual par de estados não pode existir ao mesmo tempo — e quem impede que ele apareça.
A transação garante que as duas escritas aconteçam juntas ou nenhuma — e é só
isso que ela garante. Ela não impede que duas devoluções concorrentes do
mesmo empréstimo passem as duas pela checagem: no nível de isolamento padrão
do PostgreSQL, ambas podem ler devolvidoEm: null antes de qualquer uma
escrever, e a segunda sobrescreve a primeira sem erro.
Manter a leitura dentro do tx continua sendo o padrão correto — decisão e
escrita pertencem à mesma unidade —, mas fechar essa janela por completo exige
uma escrita condicional, que só grava se o empréstimo ainda estiver em aberto.
Vale saber que a distinção existe: confundir atomicidade com exclusão mútua é
a origem de uma classe inteira de defeitos que só aparecem sob concorrência.
Exposta no controller:
@Patch(':id/devolucao')
@ApiOperation({ summary: 'Registra a devolução de um empréstimo' })
@ApiBadRequestResponse({
description: 'Empréstimo já devolvido, ou exemplar não está emprestado',
})
@ApiNotFoundResponse({ description: 'Empréstimo não encontrado' })
devolver(@Param('id', ParseIntPipe) id: number) {
return this.emprestimosService.devolver(id);
}
Parte 5 — Seed
Um banco vazio impede de testar listagem, filtro, paginação e a relação opcional. O script de seed resolve isso de forma reproduzível — qualquer pessoa que clonar o projeto obtém o mesmo conjunto de dados.
import { PrismaPg } from '@prisma/adapter-pg';
import 'dotenv/config';
import { PrismaClient } from '../src/generated/prisma/client';
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });
async function main() {
// Ordem importa: apagar primeiro quem depende dos outros.
await prisma.emprestimo.deleteMany();
await prisma.exemplar.deleteMany();
await prisma.fichaCatalografica.deleteMany();
await prisma.livro.deleteMany();
await prisma.categoria.deleteMany();
await prisma.editora.deleteMany();
await prisma.autor.deleteMany();
await prisma.leitor.deleteMany();
// Auto-relacionamento: 'Literatura' é raiz (paiId nulo) e as duas
// seguintes apontam para ela. É a hierarquia modelada na aula 8.
const literatura = await prisma.categoria.create({ data: { nome: 'Literatura' } });
const romance = await prisma.categoria.create({
data: { nome: 'Romance', paiId: literatura.id },
});
const classico = await prisma.categoria.create({
data: { nome: 'Clássico brasileiro', paiId: literatura.id },
});
const record = await prisma.editora.create({ data: { nome: 'Record' } });
// Escrita aninhada: autor, livro, categorias e exemplares num comando só.
await prisma.autor.create({
data: {
nome: 'Machado de Assis',
nacionalidade: 'Brasileira',
livros: {
create: [
{
titulo: 'Dom Casmurro',
isbn: '9788525406958',
ano: 1899,
// Sem editoraId: exercita o relacionamento opcional vazio.
categorias: { connect: [{ id: romance.id }, { id: classico.id }] },
// Escrita aninhada no 1:1 — o Prisma resolve o livroId sozinho.
ficha: {
create: { cdd: '869.3', paginas: 256, sinopse: 'Bentinho e a suspeita que o consome.' },
},
exemplares: {
create: [{ tombo: 'DC-001' }, { tombo: 'DC-002' }, { tombo: 'DC-003' }],
},
},
{
titulo: 'Memórias Póstumas de Brás Cubas',
isbn: '9788535914849',
ano: 1881,
// Sem ficha: exercita o 1:1 vazio — o include devolve null.
categorias: { connect: [{ id: classico.id }] },
exemplares: { create: [{ tombo: 'MP-001' }] },
},
],
},
},
});
await prisma.autor.create({
data: {
nome: 'Clarice Lispector',
nacionalidade: 'Brasileira',
livros: {
create: [
{
titulo: 'A Hora da Estrela',
isbn: '9788520925829',
ano: 1977,
editoraId: record.id, // Este livro tem editora — contraste com os dois acima.
categorias: { connect: [{ id: romance.id }] },
ficha: {
create: { cdd: '869.3', paginas: 96, sinopse: 'A vida mínima de Macabéa, por quem a cria.' },
},
exemplares: { create: [{ tombo: 'HE-001' }, { tombo: 'HE-002' }] },
},
],
},
},
});
await prisma.leitor.createMany({
data: [
{ nome: 'Ana Souza', email: 'ana@exemplo.com' },
{ nome: 'Bruno Lima', email: 'bruno@exemplo.com' },
],
});
console.log('Seed concluído.');
}
main()
.catch((erro) => {
console.error(erro);
process.exit(1);
})
.finally(() => prisma.$disconnect());
O trecho mais instrutivo é a escrita aninhada: create dentro de create
grava autor, livros, vínculos de categoria e exemplares em uma única operação, e
o Prisma resolve as chaves estrangeiras sozinho. connect liga a um registro que
já existe; create cria um novo. E repare nos contrastes deliberados: dois dos
três livros não recebem editoraId e um deles fica sem ficha — o seed
também precisa exercitar os caminhos opcionais, não só os obrigatórios. As
categorias, por sua vez, são criadas em hierarquia: paiId nulo na raiz e
preenchido nas duas filhas, exatamente como o auto-relacionamento da aula 8
prevê.
Até a versão 6, migrate dev e migrate reset executavam o seed automaticamente.
Na versão 7 é preciso chamar npx prisma db seed explicitamente. O comando que
ele executa é declarado em prisma.config.ts, desde a aula 7.
Erros comuns
| Erro | Sintoma | Correção |
|---|---|---|
| Consulta dentro de laço | Lento sob volume, imperceptível em desenvolvimento | Trocar por include |
| Filtrar em memória depois de paginar | Página com menos itens que o esperado | Filtrar no where |
Esquecer include da relação opcional | Cliente recebe o campo ausente em vez de null | Sempre incluir editora: true explicitamente |
Usar this.prisma dentro de $transaction | A operação não é desfeita no rollback | Usar o tx recebido |
| Supor que a transação serializa requisições | Duas devoluções concorrentes passam as duas pela checagem | Transação garante atomicidade; exclusão exige escrita condicional |
| Gravar a situação nova sem conferir a anterior | Um exemplar EM_REPARO volta a DISPONIVEL na devolução | Guardar a invariante nos dois lados: só sai DISPONIVEL, só volta EMPRESTADO |
| Repassar a mensagem do Prisma | Vaza nome de tabela e coluna | Traduzir em traduzirErro |
Listagem sem take | Uma requisição carrega a tabela inteira | Paginar sempre |
deleteMany do seed na ordem errada | Violação de chave estrangeira | Apagar primeiro quem depende |
Laboratório 9 — Dados, consultas e transações de verdade
Continuação direta do laboratório 8. Ao final, o acervo está populado, o service consome as relações por completo e o empréstimo é uma operação transacional testada nos seus casos de falha.
Passo 1 — Popular o banco
Crie prisma/seed.ts conforme a Parte 5 e execute:
npx prisma db seed
Passo 2 — Confirmar com uma listagem
curl -s "http://localhost:3000/livros?tamanho=10" | head -40
Cada livro deve vir com o objeto autor aninhado. Localize, na resposta, o
livro que não tem editora — o campo editora deve aparecer como null, não
ausente.
Passo 3 — Inspecionar com o Prisma Studio
npx prisma studio
Percorra as sete tabelas e confirme visualmente:
- os três livros e seus
autorId— e que só um temeditoraIdpreenchido; - a tabela de junção que o Prisma criou sozinho para
Livro–Categoria; - os exemplares, todos com situação
DISPONIVEL.
Passo 4 — Reescrever o LivrosService por completo
Substitua LivrosService pela versão da Parte 2 — agora com include em toda
consulta que devolve um livro e traduzirErro cobrindo P2003.
Passo 5 — Expor o resumo
Acrescente ao controller:
@Get('resumo')
@ApiOperation({ summary: 'Lista o acervo resumido, com contagem de exemplares' })
resumo() {
return this.livrosService.resumo();
}
Exponha em GET /livros/resumo — antes de @Get(':id') no controller, pelo
motivo visto na aula 5. Compare o tamanho da resposta com a de GET /livros:
essa diferença é o overfetching que a aula 4 descreveu, agora medida.
Passo 6 — Implementar o empréstimo com transação
nest g module emprestimos
nest g service emprestimos --no-spec
nest g controller emprestimos --no-spec
Implemente emprestar e devolver conforme a Parte 4, e exponha:
@Post()
@HttpCode(HttpStatus.CREATED)
emprestar(@Body() dto: CriarEmprestimoDto) {
return this.emprestimosService.emprestar(dto.exemplarId, dto.leitorId);
}
Crie o DTO com @IsInt() e @Min(1) nos dois campos. Depois teste os cenários:
# 1. empréstimo válido — 201, e a situação do exemplar muda
curl -i -X POST http://localhost:3000/emprestimos \
-H 'Content-Type: application/json' -d '{"exemplarId":1,"leitorId":1}'
# 2. o mesmo exemplar de novo — 400, porque não está mais disponível
curl -i -X POST http://localhost:3000/emprestimos \
-H 'Content-Type: application/json' -d '{"exemplarId":1,"leitorId":2}'
# 3. exemplar inexistente — 404
curl -i -X POST http://localhost:3000/emprestimos \
-H 'Content-Type: application/json' -d '{"exemplarId":9999,"leitorId":1}'
Confirme no Prisma Studio que o exemplar 1 está EMPRESTADO e que existe um
registro em Emprestimo.
Agora prove que a transação funciona: inverta temporariamente a ordem dentro
do bloco, colocando o update da situação antes do create do empréstimo, e
force um erro no create usando um leitorId inexistente. A operação deve falhar
com erro de chave estrangeira e o exemplar deve continuar DISPONIVEL — se
ele tiver mudado, a transação não está envolvendo as duas operações. Desfaça a
inversão em seguida.
Implemente também devolver, e exponha PATCH /emprestimos/:id/devolucao.
Teste:
# 4. devolução válida — o exemplar volta a DISPONIVEL
curl -i -X PATCH http://localhost:3000/emprestimos/1/devolucao
# 5. devolver o mesmo empréstimo de novo — 400
curl -i -X PATCH http://localhost:3000/emprestimos/1/devolucao
# 6. remover o livro cujo exemplar tem (ou teve) empréstimo — 409
curl -i -X DELETE http://localhost:3000/livros/1
Para ver a segunda checagem agir, quebre a invariante de propósito: empreste
outro exemplar, mude a situação dele para EM_REPARO no Prisma Studio e só
então tente devolver. A resposta deve ser 400 com a mensagem sobre o
exemplar — e não um 200 que devolveria à circulação uma cópia que está na
oficina. Desfaça a alteração em seguida.
O cenário 6 é o efeito do onDelete implícito discutido na aula 8: mesmo com
o empréstimo já devolvido, o registro em Emprestimo continua existindo e
apontando para o exemplar — e Emprestimo barra a exclusão em cascata.
Identifique no seu domínio uma operação que altere duas tabelas e não faça
sentido pela metade: dar baixa em estoque ao confirmar um pedido, ocupar uma vaga
ao efetivar uma matrícula, reservar um assento ao emitir um bilhete. Implemente-a
com $transaction interativa. Se você não encontrar nenhuma, o seu modelo
provavelmente ainda está raso demais — reveja os relacionamentos.
Passo 7 — Verificar a documentação
Abra http://localhost:3000/docs e confirme:
- as operações de livros continuam documentadas, agora com
autorIdeeditoraIdopcional; GET /livros/resumoaparece documentado, antes deGET /livros/:id;- as operações de empréstimo e devolução aparecem com o corpo e a rota corretos.
Atividades propostas
- Escreva uma consulta que liste apenas os livros sem editora cadastrada
(
editoraId: null) e exponha-a como um novo endpoint ou como um filtro doGET /livrosexistente. - Pagine e filtre
GET /emprestimos, reaproveitando o padrão deConsultarLivrosDtoda aula 6 — pense em quais filtros fazem sentido para empréstimo (por leitor? por situação de devolução?). - Implemente um
GET /livros/:id/emprestimos, que liste o histórico de empréstimos de todos os exemplares de um livro — isso exige atravessar duas relações (Livro→Exemplar→Emprestimo) num únicoincludeaninhado.
Critérios de conclusão
-
npx prisma db seedpopula o banco de forma reproduzível, com pelo menos um livro sem editora; - a listagem traz o autor aninhado com uma única consulta, e a editora aparece como
nullquando ausente; -
GET /livros/resumousaselecte_count; - emprestar um exemplar indisponível devolve
400; - a transação de
emprestarfoi verificada: em caso de erro, nada é gravado pela metade; - devolver um empréstimo grava
devolvidoEme devolve o exemplar aDISPONIVEL; - devolver o mesmo empréstimo uma segunda vez devolve
400; - devolver um empréstimo cujo exemplar não está
EMPRESTADOdevolve400, em vez de sobrescrever a situação; - remover um livro com empréstimo vinculado a algum exemplar devolve
409, sem citar nome de tabela ou coluna na mensagem; - o seu projeto tem os mesmos elementos: relacionamentos, seed e pelo menos uma operação transacional.
Fechamento
Com esta aula o serviço fica completo em suas três camadas: contrato HTTP, regra de negócio e persistência — modelada, populada e consultada por completo. O resultado mais importante não é o banco funcionando, e sim algo que atravessa as três aulas do bloco de persistência: o armazenamento foi inteiramente substituído, depois modelado, depois povoado e consultado, sem que uma linha do controller ou dos DTOs originais da aula 6 mudasse por capricho — só quando o próprio contrato precisou evoluir, e de forma deliberada.
Esse desacoplamento é o que torna viável o resto do semestre. Os clientes web (Módulo 3) e mobile (Módulo 4) serão escritos contra o contrato publicado aqui, e vão assumir que ele é estável. Na aula 10, essa mesma base ganha autenticação e autorização — e o lugar onde elas entram, o guard, você já conhece desde a aula 5.
Exercícios (Checkpoints)
-
Identifique o problema no trecho a seguir, explique por que ele não aparece em desenvolvimento e reescreva-o:
const livros = await prisma.livro.findMany();for (const livro of livros) {livro.autor = await prisma.autor.findUnique({ where: { id: livro.autorId } });} -
Compare
includeeselecte decida qual usar em cada caso, justificando: (a) tela do aplicativo que lista título e nome do autor; (b) tela de detalhe do livro com categorias, exemplares e editora; (c) exportação completa do acervo para um relatório. -
Explique por que
include: { editora: true }num livro sem editora devolveeditora: nullem vez de omitir o campo, e por que essa diferença importa para quem escreve o cliente. -
Mapeie cada código de erro do Prisma (
P2002,P2025,P2003,P2000,P1001) para um status HTTP e escreva a mensagem que você devolveria ao cliente — sem revelar nome de tabela ou coluna. ParaP2003, explique por que o mesmo código produz mensagens diferentes conforme a operação sejacriarouremover. -
Analise o que aconteceria se, dentro de
$transaction(async (tx) => ...), uma das consultas usassethis.prismaem vez detx. Descreva o comportamento observável sob duas requisições concorrentes. -
Liste as duas escritas que
devolverprecisa fazer juntas e explique o que a transação garante — e o que ela não garante — quando duas devoluções do mesmo empréstimo chegam ao mesmo tempo. -
Projete, para o seu estudo de caso, uma consulta que precise de
includeem duas relações encadeadas (comoLivro→Exemplar→Emprestimo) e identifique o risco de N+1 se ela fosse escrita seminclude.
Referências
Principais
- Prisma — Prisma Client — consultas, filtros,
includeeselect - Prisma — Transações — em lote, interativas e níveis de isolamento
- Prisma — Referência de erros — significado de cada código
P####
Aprofundamento
- Prisma — Raw queries — quando a abstração não basta
- PostgreSQL — Índices — quando e por que criar
- PostgreSQL — Níveis de isolamento de transação — o que muda entre
READ COMMITTED(padrão) e os demais