fbpx
Invista no exterior sem taxas com a Nomad

Introdução ao Sequelize e Modelagem de Dados com TypeScript

137

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:

  1. Instalar e configurar o Sequelize em um projeto TypeScript.
  2. Entender os conceitos básicos do ORM e como ele se relaciona com bancos de dados.
  3. Modelar entidades e seus relacionamentos usando Sequelize.
  4. Realizar operações CRUD e explorar consultas avançadas.

Leia mais:

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 DetalhesLivro é uma associação “um-para-um”.

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.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More