Skip to content

Instantly share code, notes, and snippets.

@geovannaotoni
Last active April 5, 2024 17:59
Show Gist options
  • Save geovannaotoni/21f799c9d7b728bb0e15c3468791f5e0 to your computer and use it in GitHub Desktop.
Save geovannaotoni/21f799c9d7b728bb0e15c3468791f5e0 to your computer and use it in GitHub Desktop.
Principais Comandos Sequelize

Setup Inicial

  1. npm init -y
  2. npm install -E sequelize@6.3.4
  3. npm install -D -E sequelize-cli@6.2.0
  4. npm install -E mysql2@2.1.0
  5. npm i -E express@4.17.1 nodemon@2.0.15
  6. Criar o arquivo .env:
MYSQL_USER=root
MYSQL_PASSWORD=senha_mysql
MYSQL_DATABASE=nome_do_banco
MYSQL_HOST=localhost
  1. Criar o arquivo .sequelizerc com as configurações:
const path = require('path');
module.exports = {
  'config': path.resolve('src', 'config', 'config.js'),
  'models-path': path.resolve('src', 'models'),
  'seeders-path': path.resolve('src', 'seeders'),
  'migrations-path': path.resolve('src', 'migrations'),
};
  1. npx sequelize-cli init: Inicialização do Sequelize
  2. Alterar o arquivo config.js
const config = {
  username: process.env.MYSQL_USER,
  password: process.env.MYSQL_PASSWORD,
  host: process.env.MYSQL_HOST,
  dialect: 'mysql',
  port: 3306 // padrão
};

module.exports = {
  development: {
    ...config,
    database: 'database_dev',
  },
  test: {
    ...config,
    database: 'database_test',
  },
  production: {
    ...config,
    database: 'database_prod',
  },
};
  1. Configurar o docker-compose.yml:
version: '3'
services:
  database:
    image: mysql:8.0.29
    container_name: nome-do-container
    environment:
      MYSQL_ROOT_PASSWORD: senha_mysql
    ports:
      - "3306:3306"
  1. Subir o container com docker-compose up -d
  2. env $(cat .env) npx sequelize db:create: criar o banco de dados
  3. npx sequelize migration:generate --name nome-da-migration: criar o arquivo esqueleto da migration
  4. env $(cat .env) npx sequelize db:migrate: criar tabela
  5. env $(cat .env) npx sequelize db:migrate:undo: reverter
  6. npx sequelize seed:generate --name nome-do-seed: criar o esqueleto de uma nova seed
  7. env $(cat .env) npx sequelize db:seed:all: executa todos os arquivos de seed que ainda não foram executados
  8. env $(cat .env) npx sequelize db:seed --seed nome-do-seed.js: executa o arquivo de seed com o nome especificado
  9. env $(cat .env) npx sequelize db:seed:undo:all: desfaz todas as seeds que foram executadas
  10. No arquivo package.json
"dev": "nodemon src/server.js"
  1. env $(cat .env) npm run dev: iniciar o servidor

Conceitos

Migrations: Criação do banco de dados (tabelas, colunas)

Seeders: Inserção de dados iniciais

Models: Leitura e escrita dos dados (insert, update, delete, select)

Migrations

Para criar tabela:

module.exports = {
  up: async (queryInterface, Sequelize) => {
    return queryInterface.createTable('addresses', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER,
      },
      city: {
        allowNull: false,
        type: Sequelize.STRING,
      },
      street: {
        allowNull: false,
        type: Sequelize.STRING,
      },
      number: {
        allowNull: false,
        type: Sequelize.INTEGER,
        defaultValue: 0 // é possível colocar um valor default 
      },
      employeeId: {
        type: Sequelize.INTEGER,
        allowNull: false,
        // Configuram o que deve acontecer ao atualizar ou excluir um usuário
        onUpdate: 'CASCADE',
        onDelete: 'CASCADE',
        field: 'employee_id',
        references: { // Informa que o campo é uma Foreign Key (Chave estrangeira)
          model: 'employees', // Informa a tabela da referência da associação
          key: 'id', // Informa a coluna da referência que é a chave correspondente
        },
      },
    });
  },

  down: async (queryInterface, _Sequelize) => {
    return queryInterface.dropTable('addresses');
  },
};

Para adicionar ou remover uma coluna:

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.addColumn('Users', 'phoneNum', {
      type: Sequelize.STRING,
      after: 'email',
    });
  },

  down: async (queryInterface, Sequelize) => {
    await queryInterface.removeColumn('Users', 'phoneNum');
   },
};

Seeders

module.exports = {
  up: async (queryInterface, Sequelize) => queryInterface.bulkInsert('Users',
    [
      {
        fullName: 'Leonardo',
        email: 'leo@test.com',
        createdAt: Sequelize.literal('CURRENT_TIMESTAMP'),
        updatedAt: Sequelize.literal('CURRENT_TIMESTAMP'),
      },
      {
        fullName: 'JEduardo',
        email: 'edu@test.com',
        createdAt: Sequelize.literal('CURRENT_TIMESTAMP'),
        updatedAt: Sequelize.literal('CURRENT_TIMESTAMP'),
      },
    ], {}),

  down: async (queryInterface) => queryInterface.bulkDelete('Users', null, {}),
};

*Obs: No Seeder, não é necessário passar o id (PK).

Models

  • Os métodos de criação de associações que o Sequelize disponibiliza são:
    • A.hasOne(B) e B.belongsTo(A): relacionamentos 1:1
    • A.hasMany(B) e B.belongsTo(A): relacionamentos 1:N
    • A.belongsToMany(B) e B.belongsToMany(A): relacionamentos N:N

Exemplo: Relacionamento 1:N

module.exports = (sequelize, DataTypes) => {
  const Address = sequelize.define('Address', {
    id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
    city: DataTypes.STRING,
    street: DataTypes.STRING,
    number: DataTypes.INTEGER,
    employeeId: { type: DataTypes.INTEGER, foreignKey: true }, // A declaração da Foreign Key é opcional no model
  },
  {
    timestamps: false, // remove a obrigatoriedade de utilizar os campos `createdAt` e `updatedAt`
    tableName: 'addresses',
    underscored: true, // automaticamente o model converte o camelCase do js para snake_case do banco de dados
  });

  Address.associate = (models) => {
    Address.belongsTo(models.Employee,
      { foreignKey: 'employeeId', as: 'employees' });
  };
  
  /*
  No model Employee:
  Employee.associate = (models) => {
    Employee.hasMany(models.Address,
      { foreignKey: 'employeeId', as: 'addresses' });
    };

  */

  return Address;
};

Exemplo: Relacionamentos N:N - tabela intermediária de junção

module.exports = (sequelize, DataTypes) => {
  const PatientSurgery = sequelize.define('PatientSurgery',
    {
      patientId: { 
        type: DataTypes.INTEGER, 
        primaryKey: true,
        references: {
          model: 'Patients', // nome da tabela à qual a chave estrangeira se refere
          key: 'patientId', // nome da coluna na tabela referenciada que é a chave primária
        }
      },
      surgeryId: { 
        type: DataTypes.INTEGER, 
        primaryKey: true,
        references: {
          model: 'Surgeries',
          key: 'surgeryId',
        }
      }
    },
    {
      timestamps: false,
      underscored: true,
      tableName: 'Patient_surgeries',
    }
  );

  PatientSurgery.associate = (models) => {
    models.Surgery.belongsToMany(models.Patient, {
      as: 'patients', // Nome da associação no model
      through: PatientSurgery, // Tabela de junção
      foreignKey: 'surgeryId', // Chave estrangeira na tabela de junção que referencia Surgery
      otherKey: 'patientId', // Chave estrangeira na tabela de junção que referencia Patient
    });

    models.Patient.belongsToMany(models.Surgery, {
      as: 'surgeries',
      through: PatientSurgery,
      foreignKey: 'patientId',
      otherKey: 'surgeryId',
    });
  };

  return PatientSurgery;
};

Operações

  • Importar o model no arquivo service. Exemplo:
// arquivo service
const { User } = require('../models');
  1. findAll: buscar todas as linhas da tabela. Equivale a fazer a query: SELECT * FROM Users
const getAll = async () => {
  const users = await User.findAll();

  return users;
};
  • Com where:
const getCommentsByAccountId = async (accountId) => {
  const comments = await Comment.findAll({
    where: { accountId },
    attributes: { exclude: ['accountId'] },
  });
  return comments;
};

Obs: exclude retira a coluna desejada do retorno

  • Com relacionamento com outra tabela:
const getAll = async () => {
  const students = await Student.findAll({
    include: { model: Course, as: 'course', attributes: ['name', 'id'] },
  });
  return users;
};

Obs: attributes serve para trazer somente as colunas desejadas

  • Procurar por algum atributo específico: utilizar o where
const getByAuthor = async (author) => {
  const books = await Book.findAll({ 
    where: { author },
    order: [['title', 'ASC']], 
  });
  return books;
};

Obs: order serve para trazer os resultados ordenados

  1. findByPk: Buscar o objeto por id. Equivale a fazer a query: SELECT * FROM Users WHERE id=?
const getById = async (id) => {
  const user = await User.findByPk(id);

  return user;
};
  1. findOne: Combinado com a chave where para buscar por id e por outro valor. Equivale a fazer a query: SELECT * FROM Users WHERE id=? AND email=?
const getByIdAndEmail = async (id, email) => {
  const user = await User.findOne({ where: { id, email } });

  return user;
};
  1. create: inserir um objeto na tabela. Equivale a fazer a query: INSERT INTO Users (full_name, email) VALUES (?, ?)
const createUser = async (fullName, email) => {
  const newUser = await User.create({ fullName, email });

  return newUser;
};
  1. update: atualizar um objeto na tabela. Equivale a fazer a query: UPDATE Users SET full_name=?, email=? WHERE id=?*/
const updateUser = async (id, fullName, email) => {
  const [updatedUser] = await User.update(
    { fullName, email },
    { where: { id } },
  );

  return updatedUser; // é retornado 0 ou 1
};
  1. destroy: remover um objeto na tabela. Equivale a fazer a query: DELETE FROM Users WHERE id=?*/
const deleteUser = async (id) => {
  const user = await User.destroy(
    { where: { id } },
  );

  return user; // é retornado 0 ou 1
};

Transações no Sequelize

  • Unmanaged transactions: Transação não gerenciada. É preciso indicar manualmente a circunstância em que uma transação deve ser finalizada ou revertida (executar o commit ou rollback).
const insert = async ({ firstName, lastName, age, city, street, number }) => {
  // Jeito antigo - sem transações
  // const employee = await Employee.create({ firstName, lastName, age });
  // await Address.create({ city, street, number, employeeId: employee.id });
  // return employee;

  // Transação - respeitando a atomicidade
  const t = await sequelize.transaction();
  try {
    const employee = await Employee.create({ firstName, lastName, age }, { transaction: t });

    await Address.create({ city, street, number, employeeId: employee.id }, { transaction: t });

    await t.commit(); // finalizar a transação usando a função `commit`.
    return employee;
  } catch (e) {
    await t.rollback(); // reverter as operações anteriores com a função rollback
    console.log(e);
    throw e;
  }
};
  • Managed transactions: Transação gerenciada. O próprio Sequelize fica responsável por realizar o gerenciamento das transações e determina, em tempo de execução, quando deve finalizar ou reverter uma transação.
const insert = async ({ firstName, lastName, age, city, street, number }) => {
  const result = await sequelize.transaction(async (t) => {
    const employee = await Employee.create({ firstName, lastName, age }, { transaction: t });

    await Address.create({ city, street, number, employeeId: employee.id }, { transaction: t });
  
    return employee;
  });

  return result;
};

Testes

  1. Instalar as dependências: npm i mocha@10.0.0 chai@4.3.4 sinon@14.0.0 chai-http@4.3.0 sequelize-test-helpers@1.4.3 -D -E
  2. Criar o script de testes no arquivo package.json:
"test": "mocha tests/**/*$NAME*.test.js --exit"
  1. Importar os helpers:
const { sequelize, dataTypes, checkModelName, checkPropertyExists } = require('sequelize-test-helpers');
  1. Exemplo de testes para a Model:
const BookModel = require('../../src/models/book.model');

describe('O model de Book', () => {
  const Book = BookModel(sequelize, dataTypes);
  const book = new Book();

  describe('possui o nome "Book"', () => {
    checkModelName(Book)('Book');
  });

  describe('possui as propriedades "title", "author", "pageQuantity"', () => {
    ['title', 'author', 'pageQuantity'].forEach(checkPropertyExists(book));
  });
});
  1. Exemplo de testes para Service
const { expect } = require('chai');
const { stub } = require('sinon');

const { Book } = require('../../src/models');
const BookService = require('../../src/services/book.service');

const testBook = {
  id: 1,
  title: 'Cem Anos de Solidão',
  author: 'Gabriel García Márquez',
  pageQuantity: 419,
  createdAt: '2023-08-23 01:00:00',
  updatedAt: '2023-08-23 01:00:00',
};

describe('BookService', () => {
  describe('#getAll', () => {
    const findAllStub = stub(Book, 'findAll');
    let books;

    describe('Não existe nenhum livro cadastrado', () => {
      before(async () => {
        findAllStub.resolves([]);
        books = await BookService.getAll();
      });

      after(() => {
        findAllStub.reset();
      });

      it('called Book.findAll', () => {
        expect(Book.findAll.calledOnce).to.be.equals(true);
      });

      it('a resposta é um array', () => {
        expect(books).to.be.an('array');
      });

      it('o array está vazio', () => {
        expect(books).to.be.empty;
      });
    });

    describe('Existem livros cadastrados', () => {
      before(async () => {
        findAllStub.resolves([testBook]);
        books = await BookService.getAll();
      });

      after(() => {
        findAllStub.restore();
      });

      it('called Book.findAll', async () => {
        expect(Book.findAll.calledOnce).to.be.equals(true);
      });

      it('a resposta é um array', async () => {
        expect(books).to.be.an('array');
      });

      it('o array deve retornar objetos', async () => {
        expect(books).to.be.deep.equal([testBook]);
      });
    });
  });
});

Dockerizando a aplicação inteira:

  1. Arquivo package.json:
"scripts": {
    "dev": "nodemon src/server.js",
    "db:reset": "npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate && npx sequelize-cli db:seed:all"
  },
  1. Arquivo docker-compose.yml:
version: '3.1'
services:
  node: 
    build: .
    ports: 
      - 5556:3000
    environment:
      MYSQL_USERNAME: root
      MYSQL_PASSWORD: senha
      MYSQL_DATABASE: nome-do-banco
      MYSQL_HOST: db # serviço do container
      MYSQL_PORT: 3306 # porta do container
    volumes:
      - ./src:/app/src
  db:
    image: mysql:8.0.23
    ports:
      - 5555:3306
    environment:
      MYSQL_ROOT_PASSWORD: senha
  1. Dockerfile do backend:
FROM node:18-alpine
WORKDIR /app
COPY package* ./
RUN npm clean-install
COPY . .
CMD ["npm", "run", "dev"]
  1. Arquivo config
const config = {
  username: process.env.MYSQL_USER || 'root',
  password: process.env.MYSQL_PASSWORD || 'senha',
  host: process.env.MYSQL_HOST || '127.0.0.1', // === localhost
  port: process.env.MYSQL_PORT || 5555, // padrão é 3306
  dialect: 'mysql',
};

module.exports = {
  development: {
    ...config,
    database: process.env.MYSQL_DATABASE || 'nome-do-banco_dev',
  },
  test: {
    ...config,
    database: 'nome-do-banco_test',
  },
  production: {
    ...config,
    database: 'nome-do-banco',
  },
};
  1. Iniciando a aplicação no Docker Compose:
  • docker-compose up -d
  • docker-compose exec node sh
  • npm run db:reset

Todos os direitos reservados à Trybe e à documentação oficial do Sequelize (https://sequelize.org/docs/v6/getting-started/), cujos materiais serviram de base para a construção deste gist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment