DEV Community

Gabriel_Silvestre
Gabriel_Silvestre

Posted on

ORM - Associations

Tabela de Conteúdos


Relacionamentos

O que são?

São relações restritas entre entidades de diferentes tabelas, onde uma entidade da Tabela A, pode ter um ou mais relacionamentos com uma entidade da Tabela B e vice-versa.

Mais informações

Voltar ao topo


Relacionamentos no Sequelize

Criação

Criamos os relacionamentos entre tabelas a partir das Migrations do Sequelize, lá definimos entre quais tabelas será o relacionamento.

Sintaxe

Dentro de uma Migration iremos definir qual campo será a Foreign Key, qual será seu comportamento em casos de atualização ou deleção, e por fim com qual campo de outra tabela irá se relacionar.

module.exports = {
  up: async (queryInterface, Sequelize) => {
    return queryInterface.createTable('Addresses', {
      /* ----- CHAVES COMUNS -----  */
      // 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,
      // },
      /* ----- CHAVES COMUNS -----  */

      /* ----- CHAVE ESTRANGEIRA ----- */
      customerId: {  // <- definição/configuração da FK
        type: Sequelize.INTEGER,
        allowNull: false,
        onUpdate: 'CASCADE',  // <- diz o que fazer quando um endereço sofrer alteração
        onDelete: 'CASCADE',  // <- diz o que fazer quando um endereço for deletado
        field: 'customer_id',
        references: {  // <- aqui definimos a referência para a PK da tabela Customer
          model: 'Customers',  // <- informamos a tabela que terá a PK
          key: 'id',  // <- informamos qual a PK da tabela
        },
      },
      /* ----- CHAVE ESTRANGEIRA ----- */
    });
  },

  down: async (queryInterface, _Sequelize) => {
    return queryInterface.dropTable('Addresses');
  },
};
Enter fullscreen mode Exit fullscreen mode

Mapeamento

Tendo definido os relacionamentos na Migration, agora iremos mapeá-los nas Models, para que o comportamento do Sequelize aconteça de forma correta para a busca, criação, atualização ou deleção de um entidade.

Sintaxe

Dentro da Model iremos definir a Foreign Key e o tipo de relacionamento entre as tabelas, para isso iremos utilizar métodos de associação fornecidos pelo próprio Sequelize, esses que serão abordados mais a frente.

Nos exemplos a seguir iremos mapear o relacionamento nas duas Models, porém isso não é necessário, o mapeamento é obrigatório somente na entidade que vamos usar como base em nossas buscas no DB. Por exemplo:

"Ao pesquisarmos os endereços DOS clientes, estaremos utilizando o Model Customer como base em nossa busca, logo seria necessário o mapeamento apenas de Customer."

"Já ao pesquisarmos os clientes POR endereço, estaremos utilizando o Model Address como base, então apenas o seu mapeamento seria necessário."

"Agora se quisermos realizar a pesquisa em ambas as "direções" então o mapeamento de ambas as Models será necessária."

// models/Customer.js

module.exports = (sequelize, DataTypes) => {
  const Customer = sequelize.define('Customer', {
    id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
    firstName: DataTypes.STRING,
    lastName: DataTypes.STRING,
    email: DataTypes.STRING,
  },
  {
    timestamps: false,  // deixa os campos createdAt e updatedAt opcionais
    tableName: 'Customers',
    underscored: true,
  });

  /* ----------------------- */
  Customer.associate = (models) => {  // <- associando as colunas
    Customer./*método de relacionamento*/(models.Address,
      { foreignKey: /*chave estrangeira*/, as: /*"aliases", opcional*/ });
  };
  /* ----------------------- */

  return User;
};
Enter fullscreen mode Exit fullscreen mode
// models/Address.js

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,
    /* ----- CHAVE ESTRANGEIRA ----- */
    customer_id: { type: DataTypes.INTEGER, foreignKey: true },  // <- é opcional defini-la
    /* ----- CHAVE ESTRANGEIRA ----- */
  },
  {
    timestamps: false,  // deixa os campos createdAt e updatedAt opcionais
    tableName: 'Addresses',
    underscored: true,
  });

  /* ----------------------- */
  Address.associate = (models) => {  // <- associando as colunas
    Address./*método de relacionamento*/(models.Customer,
      { foreignKey: 'customer_id', as: 'customer' });
  };
  /* ----------------------- */

  return Address;
};
Enter fullscreen mode Exit fullscreen mode

Voltar ao topo


1:1

Métodos de associação

Para associarmos tabelas com um Relacionamento 1:1 utilizamos os métodos .hasOne() e .belongTo(), esses que podem ser "traduzidos" da seguinte forma:

"A Tabela A possui uma (hasOne) chave estrangeira na Tabela B, que por sua vez, tem uma chave estrangeira que pertence a (belongTo) Tabela A".

Em outras palavras, utilizamos o .hasTo() na tabela que esta recebendo a chave estrangeira e o .belongTo() na tabela que está provendo a chave estrangeira.

Sintaxe

Address.associate = (models) => {  // A chave estrangeira da Tabela Address pertence a Tabela User
  Address.BelongsTo(models.User,
    { foreignKey: 'address_id', as: 'users' });
};

User.associate = (models) => {  // <- A Tabela User possui uma chave estrangeira na Tabela Address
  User.hasOne(models.Address,
    { foreignKey: 'address_id', as: 'addresses' });
};
Enter fullscreen mode Exit fullscreen mode

Voltar ao topo


1:N

Métodos de associação

Já para o relacionamento 1:N (ou N:1), utilizamos os métodos .hasMany() e .belongsToMany(), esses que podem ser "traduzidos" para o português da seguinte forma:

"A Tabela A possui várias (hasMany) chaves estrangeiras na Tabela B, que por sua vez, tem uma chave estrangeira que pertence a (belongTo) Tabela A".

Simplificando ainda mais, utilizamos o .hasMany() na tabela que esta recebendo a chave estrangeira e o .belongTo() na tabela que está provendo a chave estrangeira.

Sintaxe

// Address.associate = (models) => {  // A Relação de pertencimento (belongsTo) se mantém 
//   Address.belongsTo(models.User,
//     { foreignKey: 'address_id', as: 'users' });
// };

User.associate = (models) => { // <- Alteramos somente o método de associação, de hasOne para hasMany
  User.hasMany(models.Address,
    { foreignKey: 'address_id', as: 'addresses' });
};
Enter fullscreen mode Exit fullscreen mode

Voltar ao topo


N:N

Métodos de associação

O relacionamento de muitos para muitos (N:N) normalmente é constituído de múltiplos relacionamentos de 1:N ou 1:1:1, sendo assim, normalmente se utiliza uma tabela intermediária para relacionar outras duas tabelas. Dessa forma utilizamos apenas o método .belongsToMany() na tabela intermediária, informando que as FK recebidas pertencem a outras tabelas.

Sintaxe

// models/UserAddress.js

UserAddress.associate = (models) => {
  models.Address.belongsToMany(models.User, {  // <- Ligação entre Address e User
    as: 'users',
    through: UserAddress,  // <- A chave through define a tabela intermediária
    foreignKey: 'address_id',  // <- A Foreign Key vinda de Address será seu id, logo address_id
    otherKey: 'user_id',  // <- Definimos qual a coluna que se relacionará com a Foreign Key de Address
  });
    models.User.belongsToMany(models.Address, {  // <- Ligação entre User e Address
    as: 'addresses',
    through: UserAddress,
    foreignKey: 'user_id',  // <- A Foreign Key vinda de User será seu id, logo user_id
    otherKey: 'address_id',  // <- Definimos qual a coluna que se relacionará com a Foreign Key de User
  });
};
Enter fullscreen mode Exit fullscreen mode

Voltar ao topo


Utilizando Relacionamentos

O que faz?

Aqui iremos abordar sobre os tipos de loading (carregamento) que podemos realizar utilizando os relacionamentos entre as tabelas, são esses loadings: Eager Loading e Lazy Loading.

Eager Loading

Retorna todos os dados de uma só vez, não levando em consideração se esses dados serão ou não utilizados. Útil para requisições na qual já sabemos que iremos precisar de todos os dados em questão.

Sintaxe

O Sequelize não possui uma sintaxe própria para definir um Eager Loading, na verdade o que define esse tipo de loading é a falta de "opções", ou seja, sempre que realizarmos uma requisição a tal endpoint ele sempre retornará a mesma coisa, independente do parâmetro, query ou body passados através do Request.

Lazy Loading

Irá retornar apenas o básico de determinada tabela, aumentando, ou detalhando, mais o retorno de acordo com a requisição recebida. Esse loading permite respostas (Responses) mais curtas e apenas com as informações que realmente iremos utilizar.

Sintaxe

Assim como o Eager Loading, o Lazy Loading não possui nenhuma sintaxe especial do próprio Sequelize, o que o diferencia é a possibilidade de ajustar a resposta (Response) de acordo com os parâmetros, queries ou body recebidos.

Voltar ao topo


Transações

O que são?

São operações indivisíveis e independentes de quaisquer outras operações no DB, ou seja, esse tipo de operação não deve depender de terceiros, bem como só deverá ser concluída caso todas as ações internas obtenham sucesso.

O que fazem?

Transações garantem a integridade dos dados, visto que um determinado conjunto de ações precisa ser concluído com sucesso para que o DB seja realmente modificado, caso uma dessas ações falhe teremos dados inconsistentes salvos.

Para exemplificar podemos pensar em uma transferência bancaria, onde o Usuário A irá transferir R$ 100,00 para o Usuário B, de forma bem simplista, precisamos realizar o conjunto de duas ações:

  1. Subtrair tal valor da conta do Usuário A
  2. Adicionar o mesmo valor na conta do Usuário B.

Caso qualquer uma dessas ações falhe, os dados ficariam inconsistentes, para isso temos as Transações. Ao definirmos esse conjunto de ações como uma transação, garantimos que o DB será modificado apenas se todas as ações estabelecidas forem concluídas com sucesso, caso contrário todas as modificações, dessa transação, serão desfeitas.

Quando usar?

É altamente recomendável utilizar Transações em operações que irão modificar duas ou mais tabelas, dessa forma garantimos que todas as tabelas sejam modificadas de acordo.

Transações também podem ser usadas para operações em tabelas únicas, porém seu uso não é tão impactante, afinal caso algum erro ocorra a Query inteira deixará de ser executada.

Sintaxe

No Sequelize temos dois tipos de Transações, as Unmanaged Transactions e as Managed Transactions, a diferença geral entre ambas é, respectivamente, o fato de que em uma precisamos definir o caso de sucesso e fracasso manualmente, enquanto a outra faz isso por conta própria.

Unmanaged Transactions

Nesse caso primeiros iniciamos a transação através do método .transaction(), disponibilizado pelo sequelize, salvando seu retorno em uma variável. Após isso executamos nossas operações no DB passando a transaction criada como parâmetro para a operação.

E por fim definimos os casos de sucesso e fracasso, para sucesso utilizamos o método .commit() e para fracasso usamos .rollback().

O exemplo a seguir reúne todo o código dentro dentro de uma só camada, NÃO É O RECOMENDÁVEL,
mas para facilitar a exemplificação da sintaxe faremos dessa forma.

// src/index.js

app.post('/customer', async (req, res) => {  // <- Operações com banco de dados são assíncronas
  const registerTransaction = await sequelize.transaction();

  try {
    const { firstName, lastName, email, city, street, number } = req.body;

    const customer = await Customer.create(
      { firstName, lastName, email },
      { transaction: registerTransaction },  // <- Aqui definimos a transação que essa ação pertence.
    );

    await Address.create(
      { city, street, number, customer_id: customer.id },
      { transaction: registerTransaction },
    );

    await registerTransaction.commit();  // <- Caso não haja nenhum erro o Sequelize irá "commitar" as mudanças

    return res.status(201).json({ message: 'Successful register' })
  } catch (err) {
    await registerTransaction.rollback();  // <- Se houver algum erro iremos cair no catch e reverter as alterações
    console.log(err);

    return res.status(500).json({ message: 'Something\'s gone wrong' });
  }
});
Enter fullscreen mode Exit fullscreen mode

Managed Transactions

Essas Transações são mais simples, pois conseguem definir por conta própria quando o conjunto de ações é concluído com sucesso ou não. O sucesso é definido caso nenhum erro seja lançado durante a execução, então se a nossa condição de falha for somente o lançamento de um erro, essa Transação é mais simples e "clean" de ser utilizada.

Para utilizar a Managed Transactions utilizamos o método .transaction() passando uma callback como argumento, essa que deverá ser assíncrona, no caso de querermos utilizar o async/await.

Ainda precisamos utilizar o bloco try/catch, pois apesar da Transação conseguir lidar sozinha com sucesso e falha, ainda precisamo tratar o erro caso houver e retornar uma resposta (Response) ao cliente informando o que aconteceu.

// src/index.js

app.post('/customer', async (req, res) => {  // <- Operações com banco de dados são assíncronas
  try {
    const { firstName, lastName, email, city, street, number } = req.body;

    await sequelize.transaction(async (registerTransaction) => {  // <- A variável de transação vira um parâmetro
      const customer = await Customer.create(
        { firstName, lastName, email },
        { transaction: registerTransaction },  // <- Aqui definimos a transação que essa ação pertence.
      );

      await Address.create(
        { city, street, number, customer_id: customer.id },
        { transaction: registerTransaction },
      );

      return res.status(201).json({ message: 'Successful register' })
    });
  } catch (err) {
    console.log(err);
    return res.status(500).json({ message: 'Something\'s gone wrong' });
  }
});
Enter fullscreen mode Exit fullscreen mode

Voltar ao topo

Discussion (0)