Associações auto referenciadas em Ruby on Rails

Artigos - 28/Out/2020 - por André Kanamura

Em algumas ocasiões quando estamos desenvolvendo uma aplicação pode haver a necessidade de criar associações de models com eles mesmos. Esse tipo de relação pode ser encontrada comumente em redes sociais, por exemplo, na forma como um pessoa pode seguir outras pessoas. No Twitter existem os seguidores, no Facebook, os amigos. Outro contexto em que encontramos essa associação é quando lemos alguma notícia e são apresentadas outras notícias relacionadas. Essa associação pode ser obtida usando um model intermediário que faz a ligação de um model a ele mesmo. Outra forma de conseguirmos isso seria por meio de associações auto referenciadas. Na documentação do Rails encontramos esse conceito com o nome Self Joins.

Utilizando um model intermediário

Vamos começar vendo como estabelecer essa associação auto referenciada com um model intermediário.

Utilizaremos como exemplo uma aplicação que mostra artigos com uma lista de artigos relacionados. Temos um model Article e precisamos criar um model intermediário, que vamos chamar de RelatedContent. Ele vai precisar ter duas associações, uma com o próprio article e outra cujo nome deve representar o seu papel na aplicação, por exemplo, related_article.

$ rails generate model related_content article:references related_article:references

É recomendado utilizar nomes que representem da melhor forma possível a natureza da relação entre os models. Aqui talvez isso tenha ficado um pouco estranho., mas em outros contextos, como no Linkedin, por exemplo, as conexões entre usuários poderiam ser representadas por Connections e connected_user.

Antes de rodar rails db:migrate, editamos a migração para direcionar a associação de related_article para a tabela articles.

class CreateRelatedContents < ActiveRecord::Migration[6.0]
  def change
    create_table :related_contents do |t|
      t.references :article, foreign_key: true
      t.references :related_article, foreign_key: { to_table: :articles }

      t.timestamps
    end
  end
end

Talvez seja importante lembrar que essa migração pode ficar um pouco diferente dependendo do contexto da sua aplicação. Aqui, por exemplo, não incluímos null: false nas referências, mas para o seu caso pode ser interessante. Depois disso, podemos rodar essa migração e incluir na classe related_content a implementação que estabelece associação com o método class_name:

class RelatedContent < ApplicationRecord
  belongs_to :article 
  belongs_to :related_article, class_name: "Article" 
end

A classe Article deve ter implementada a relação com o model RelatedContent com has_many :related_contents e isso já seria suficiente para acessar os artigos relacionados através de related_contents. Mas, para facilitar o acesso ao artigo relacionado, podemos incluir o método through em related_articles:

class Article < ApplicationRecord
  has_many :related_contents
  has_many :related_articles, through: :related_contents
end

Dessa maneira podemos obter todos os artigos relacionados com article.related_articles.

Agora vamos mostrar como conseguir os mesmos resultados sem tabelas intermediárias.

Self Joins

Para associarmos um model a ele mesmo sem tabelas intermediárias, podemos usar o conceito de Self Joins, conforme indicado na documentação do Rails.

Vamos começar com o mesmo model Article. Dentro dele vamos implementar duas associações, uma do tipo has_many :related_articles e outra belongs_to :main_article:

class Article < ApplicationRecord
  has_many :related_articles, foreign_key: "main_article_id", class_name: "Article"
  belongs_to :main_article, class_name: "Article", optional: true
 end

Assim, um article pode ser de dois tipos: "principal" (main_article), que poderia representar em nosso contexto o artigo que está sendo lido; ou "relacionado" (related_article). Um "artigo principal" poderia ter muitos "artigos relacionados". Note como esses models não existem, mas eles referenciam Article usando o método class_name. Além disso, para related_article precisamos definir a foreign_key para main_article_id (o objeto ao qual ele "pertence") e essa relação deve ser adicionada por uma migração:

$ rails generate migration add_main_article_to_article main_article:references

Editamos a migração para que ela fique assim e rodamos rails db:migrate:

class AddMainArticleToArticle < ActiveRecord::Migration[6.0]
  def change
    add_reference :articles, :main_article
  end
end

No seu caso, talvez o model ainda não exista, então sua migração será diferente. O importante é que no seu schema ele tenha a referência ao nome que escolheu para a relação do belongs_to. Estabelecendo essa ligação de um related_article com "main_article_id", conseguimos facilmente acessar os artigos relacionados de um artigo.

Conclusão

Utilizando Self Joins é possível criar associações auto referenciadas com facilidade, sem criar models a mais.

Referências

Foto de perfil do autor
André Kanamura

Dev na Campus Code