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.

Comentários estão fechados.