Introdução ao Sequelize e Modelagem de Dados com TypeScript
Seja bem-vindo à nossa série de tutoriais sobre Sequelize com TypeScript! Sequelize é um ORM (Object-Relational Mapping) que permite que os desenvolvedores manipulem bancos de dados de maneira eficiente e elegante utilizando JavaScript e TypeScript. Esta série de artigos irá guiá-lo passo a passo, desde a instalação até os diferentes tipos de associações de modelos.
O objetivo desta série é oferecer uma visão detalhada de como trabalhar com Sequelize em um projeto TypeScript. Ao final da série, você será capaz de:
- Instalar e configurar o Sequelize em um projeto TypeScript.
- Entender os conceitos básicos do ORM e como ele se relaciona com bancos de dados.
- Modelar entidades e seus relacionamentos usando Sequelize.
- Realizar operações CRUD e explorar consultas avançadas.
Leia mais:
- Lidando com Erros em ASP.NET: Melhores Práticas para um Código Mais Seguro
- Por Que Testes de Unidade São Cruciais no Desenvolvimento de Software?
Iniciando o projeto
Crie uma nova pasta e navegue até ela:
mkdir tutorial-sequelize cd tutorial-sequelize
Inicie o projeto com NPM:
npm init -y
Instalando o TypeScript
Agora, vamos adicionar o TypeScript e algumas dependência para rodarmos a aplicação em desenvolvimento.
npm install -D typescript ts-node ts-node-dev tsconfig-paths @types/node
Após isso, temos que criar o arquivo de configurações do TypeScript, usando o comando:
npx tsc --init
Abra o arquivo tsconfig.json
e habilite as propriedades experimentalDecorators
e emitDecoratorMetadata
. Precisaremos delas mais tarde ao usarmos os decoradores de código do Sequelize.
Instalando o Express
O próximo passo é adicionar o Express, responsável por disponibilizar a aplicação como uma API Web.
npm install express npm install -D @types/express
Instalando o Sequelize
npm install sequelize sequelize-typescript reflect-metadata npm install -D @types/sequelize
E, em nosso exemplo, faremos uso do SQLite In Memory. Para sua instalação, execute o comando:
npm install sqlite3
Para mais detalhes, verifique o arquivo package.json
no GitHub do projeto.
Configurando a API com Express
Crie uma pasta chamado src
e, nela, o arquivo server.ts
conforme abaixo:
import "reflect-metadata"; import express from "express"; const app = express(); const PORT = 3000; app.use(express.json()); app.get("/", async (req, res) => { res.json("Tutorial Sequelize"); }); app.listen(PORT, () => { console.log(`Servidor rodando na porta ${PORT}`); });
Execute o comando npm run dev:server
, e depois acesse http://localhost:3000/
, que verá a aplicação inicial funcionando.
Modelo de Dados: Uma Biblioteca
Para fins didáticos, modelaremos uma biblioteca simples, que incluirá:
Entidades:
- Usuário: Representa uma pessoa que utiliza a biblioteca.
- Livro: Representa um livro na biblioteca.
- Empréstimo: Representa um empréstimo de livro por um usuário.
- Gênero: Representa um gênero literário.
Associações:
- Um Usuário pode ter vários Empréstimos (um-para-muitos).
- Um Empréstimo está associado a um Usuário e um Livro (um-para-um).
- Um Livro pode pertencer a vários Gêneros e vice-versa (muitos-para-muitos).
Associação “Um-Para-Um” com Sequelize e TypeScript
A associação “um-para-um” é um tipo de relacionamento onde uma entidade está diretamente relacionada a apenas uma outra entidade.
O que é associação “Um-Para-Um”?
No mundo dos bancos de dados relacionais, uma associação “um-para-um” ocorre quando um registro em uma tabela está associado a exatamente um registro em outra tabela. Um exemplo clássico é a relação entre uma pessoa e sua identidade. Uma pessoa possui exatamente uma identidade, e uma identidade pertence a exatamente uma pessoa.
Modelagem no Contexto da Nossa Biblioteca
Na nossa biblioteca, vamos considerar que cada Livro
possui um DetalhesLivro
, que são detalhes e informações exclusivas daquela obra. Portanto, a relação entre Livro
e
é uma associação “um-para-um”.DetalhesLivro
Modelo Livro:
import { AutoIncrement, BelongsToMany, Column, DataType, HasOne, Model, PrimaryKey, Table } from "sequelize-typescript"; import { DetalhesLivro, Genero, LivroGenero } from "."; @Table export default class Livro extends Model { @PrimaryKey @AutoIncrement @Column(DataType.INTEGER) id!: number; @Column(DataType.STRING) titulo!: string; @Column(DataType.BOOLEAN) disponivel!: boolean; @HasOne(() => DetalhesLivro) detalhes!: DetalhesLivro; @BelongsToMany(() => Genero, () => LivroGenero) generos!: Genero[]; }
Modelo DetalhesLivro:
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript"; import { Livro } from "."; @Table export default class DetalhesLivro extends Model { @PrimaryKey @AutoIncrement @Column(DataType.INTEGER) id!: number; @Column(DataType.STRING) sinopse!: string; @Column(DataType.STRING) primeiraFrase!: string; @ForeignKey(() => Livro) @Column(DataType.INTEGER) livroId!: number; @BelongsTo(() => Livro) livro!: Livro; }
Adicionar um novo Livro:
Ao criar um novo Livro
, podemos informar ao Sequelize para incluir os seus detalhes. Para isso, basta informar os dados, e adicionar o parâmetro include: [DetalhesLivro]
.
const livro = await Livro.create({ titulo: 'O Senhor dos Anéis', detalhes: { sinopse: 'Uma jornada épica na Terra Média.', primeiraFrase: 'Quando o Sr. Bilbo Bolseiro...' } }, { include: [DetalhesLivro] });
Buscar um livro e seus detalhes:
Usamos o método findAll()
para retornar todas as entidades Livro
. Aqui, assim como no exemplo anterior, utilizamos o parâmetro include: [DetalhesLivro]
para informar ao Sequelize que, além dos dados do Livro
, queremos retornar os dados do [DetalhesLivro]
.
const livrosComDetalhes = await Livro.findAll({ include: [DetalhesLivro] }); console.log(livrosComDetalhes[0].detalhes.sinopse);
Associação “Um-Para-Muitos” com Sequelize e TypeScript
Nessa seção, vamos explorar uma das associações mais comuns em bancos de dados relacionais: a associação “um-para-muitos”. Por meio da relação entre um usuário e seus vários empréstimos, ilustraremos este conceito.
Conceituando a Associação “Um-Para-Muitos”
A associação “um-para-muitos” refere-se à relação onde um registro em uma tabela pode estar relacionado a vários registros em outra tabela. No nosso caso, um usuário pode ter vários empréstimos, mas cada empréstimo pertence a um único usuário.
Um Usuário e Seus Empréstimos
Vamos considerar a relação entre um usuário e os livros que ele emprestou.
Modelo Usuário:
import { AutoIncrement, Column, DataType, HasMany, Model, PrimaryKey, Table } from "sequelize-typescript"; import { Emprestimo } from "."; @Table export default class Usuario extends Model { @PrimaryKey @AutoIncrement @Column(DataType.INTEGER) id!: number; @Column(DataType.STRING) nome!: string; @HasMany(() => Emprestimo) emprestimos!: Emprestimo[]; }
Modelo Emprestimo:
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript"; import { Livro, Usuario } from "."; @Table export default class Emprestimo extends Model { @PrimaryKey @AutoIncrement @Column(DataType.INTEGER) id!: number; @ForeignKey(() => Usuario) @Column(DataType.INTEGER) usuarioId!: number; @BelongsTo(() => Usuario) usuario!: Usuario; @ForeignKey(() => Livro) @Column(DataType.INTEGER) livroId!: number; @BelongsTo(() => Livro) livro!: Livro; @Column(DataType.DATE) dataEmprestimo!: Date; @Column(DataType.DATE) dataPrevistaDevolucao!: Date; @Column(DataType.DATE) dataEfetivaDevolucao?: Date; get atrasado(): boolean { const hoje = new Date(); return hoje > this.dataPrevistaDevolucao; } }
Emprestar um Livro para um Usuário:
Para emprestar um livro a um usuário, basta apenas que informemos o usuarioId
e livroId
diretamente na criação de um Emprestimo
.
const emprestimo = await Emprestimo.create({ usuarioId: 1, livroId: 2, dataEmprestimo: new Date(), dataDevolucaoPrevista: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 dias a partir de agora });
Perceba que, da mesma forma que no exemplo do DetalhesLivro
, ao utilizar o findAll()
para retornar os empréstimos de um livro, será preciso acrescentar o include: [Emprestimo]
.
Dessa maneira, o Sequelize saberá que deve incluir as informações referentes aos empréstimos de cada livro.
Buscar Empréstimos de um Usuário:
Para realizar uma busca por usuário pelo seu identificador, utilizamos a função findByPk()
. Informamos qual o usuarioId
e em seguida orientamos para trazer os dados da associação Emprestimo
.
const usuarioComEmprestimos = await Usuario.findByPk(1, { include: [Emprestimo] }); console.log(usuarioComEmprestimos.emprestimos);
Com a associação “um-para-muitos”, temos a capacidade de organizar e acessar informações relacionadas de maneira eficiente. Esse tipo de relação é um dos pilares dos bancos de dados relacionais, permitindo que lidemos com complexidades em nosso conjunto de dados.
Associação “Muitos-Para-Muitos” com Sequelize e TypeScript
A associação “muitos-para-muitos” ocorre quando registros em uma tabela podem estar relacionados a múltiplos registros em outra tabela e vice-versa. No nosso exemplo, um livro pode pertencer a vários gêneros, e um gênero pode ser associado a vários livros.
Estruturando a Associação
Para efetivar uma associação “muitos-para-muitos” geralmente precisamos de uma tabela intermediária. No nosso caso, essa tabela intermediária será LivroGenero
.
Modelo Livro:
import { AutoIncrement, BelongsToMany, Column, DataType, HasOne, Model, PrimaryKey, Table } from "sequelize-typescript"; import { DetalhesLivro, Genero, LivroGenero } from "."; @Table export default class Livro extends Model { @PrimaryKey @AutoIncrement @Column(DataType.INTEGER) id!: number; @Column(DataType.STRING) titulo!: string; @Column(DataType.BOOLEAN) disponivel!: boolean; @HasOne(() => DetalhesLivro) detalhes!: DetalhesLivro; @BelongsToMany(() => Genero, () => LivroGenero) generos!: Genero[]; }
Modelo Genero:
import { AutoIncrement, BelongsToMany, Column, DataType, Model, PrimaryKey, Table } from "sequelize-typescript"; import { Livro, LivroGenero } from "."; @Table export default class Genero extends Model { @PrimaryKey @AutoIncrement @Column(DataType.INTEGER) id!: number; @Column(DataType.STRING) nome!: string; @BelongsToMany(() => Livro, () => LivroGenero) livros!: Livro[]; }
Modelo LivroGenero (Tabela Intermediária):
import { Column, DataType, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript"; import { Genero, Livro } from "."; @Table export default class LivroGenero extends Model { @PrimaryKey @ForeignKey(() => Livro) @Column(DataType.INTEGER) livroId!: number; @PrimaryKey @ForeignKey(() => Genero) @Column(DataType.INTEGER) generoId!: number; }
Associar um Livro a Gêneros:
Para associar um livro a um ou mais gêneros na sua criação, dê uma olhada no exemplo do DetalhesLivro
, segue a mesma lógica. E, tanto o livro, quanto os gêneros serão cadastrados casos não existam na base de dados.
Agora, caso uma entidade Livro
já esteja cadastrada no banco e o Genero
também, você pode seguir uma outra abordagem, veja abaixo.
const livro = await Livro.findByPk(1); const generoFantasia = await Genero.findOne({ where: { descricao: 'Fantasia' } }); const generoAventura = await Genero.findOne({ where: { descricao: 'Aventura' } }); await livro.$set('generos', [generoFantasia, generoAventura]);
É possível, ainda, utilizar o generoId
diretamente ao invés de consultá-lo e passar sua instância como parâmetro. Isso evita consultas desnecessárias e sobrecargas no banco de dados.
Você deve ter percebido o uso da função $set, e talvez, tenha estranhado. Bom, o Sequelize cria algumas funções de forma automática para lidarmos com associações, são as auto generated functions. Porém, como estamos utilizando a biblioteca sequelize-typescript
, devemos seguir uma abordagem um pouco diferente conforme a documentação.
Buscar todos os gêneros de um livro:
Para consultarmos todos os gêneros associados a um determinado livro, usamos:
const livroComGeneros = await Livro.findByPk(1, { include: [Genero] }); console.log(livroComGeneros.generos);
A associação “muitos-para-muitos” é uma ferramenta poderosa que nos permite lidar com relações complexas em bancos de dados. Com o Sequelize e o TypeScript, podemos representar e manipular essas relações de forma elegante e eficaz.
Conclusão e o que esperar a seguir
Neste artigo introdutório, fornecemos uma visão geral do que é o Sequelize, o propósito deste tutorial e os passos iniciais para começar a trabalhar com ele em TypeScript.
E se você quiser testar melhor os conceitos demonstrados, acesse o código no GitHub, lá tem um exemplo funcional. Nele temos a criação de rotas das APIs, bem como o arquivo para importá-las no Insomnia.
E assim avançamos ainda mais no mundo do Sequelize e TypeScript. Seu feedback é vital para continuarmos aprimorando e oferecendo conteúdo relevante. Se tiver algo a acrescentar ou perguntas, por favor, compartilhe nos comentários.
Os comentários estão fechados, mas trackbacks E pingbacks estão abertos.