quarta-feira, 7 de novembro de 2012

Auto-relacionamento Many-to-Many com Rails

Nas aplicações que a maioria de nós desenvolve, é relativamente comum ter auto-relacionamentos, em que uma entidade referencia ela mesma através de um ou mais atributos. Também é comum ter relacionamentos muitos-para-muitos (many-to-many), em que duas entidades podem ser referenciadas por um ou mais registros da outra. Porém, há uma terceira possibilidade, que são os auto-relacionamentos many-to-many, cujo primeiro exemplo que vem à mente é o das redes sociais, tão comuns hoje em dia, onde o perfil de uma pessoa pode possuir vários amigos, que na verdade são outros perfis também de pessoas que também possuem vários amigos e assim por diante, tudo em uma única entidade Pessoa. Na continuidade vou ilustrar as duas primeiras situações, mais comuns, e em seguida mostrar como a terceira situação pode ser implementada com o framework ActiveRecord, incluído por padrão no Rails.

Auto-relacionamento



Um exemplo de auto-relacionamento é uma pessoa que possui um pai. A pessoa e seu pai não devem estar em duas tabelas distintas no banco de dados, pois ambos, na verdade, são pessoas. Em outras palavras, umas pessoa possui referência para outra pessoa através de um atributo 'pai', conforme exemplo abaixo em UML:


Indo para o Rails, o primeiro passo é gerar o modelo através do seguinte comando:

rails generate model Pessoa nome:string idade:integer pai_id:integer

O Rails criará uma Migration para gerar a respectiva tabela no banco de dados:
class CreatePessoas < ActiveRecord::Migration
  def change
    create_table :pessoas do |t|
      t.string :nome
      t.integer :idade
      t.integer :pai_id

      t.timestamps
    end
  end
end

E gerará o modelo, isto é, uma classe que herda de ActiveRecord::Base, na qual teremos somente o trabalho de especificar os relacionamentos, conforme abaixo:
class Pessoa < ActiveRecord::Base
  attr_accessible :idade, :nome, :pai_id

  has_many :filhos, class_name: "Pessoa", foreign_key: "pai_id"
  belongs_to :pai, class_name: "Pessoa"
end

Os atributos 'class_name' são necessários porque, por convenção, o Rails buscaria classes com os nomes Filho e Pai.

Feito isso, está pronto o auto-relacionamento.


Relacionamento Many-to-Many


Um exemplo de relacionamento many-to-many é uma pessoa que leu vários livros, e, de igual forma, cada livro foi lido por várias pessoas. Não basta haver somente as tabelas de pessoas e de livros, mas tem que haver uma tabela adicional que guarde a relação entre essas duas, ou seja, uma tabela associativa. Tendo três tabelas, teremos as seguintes classes:



Indo para o Rails, o primeiro passo é gerar os três modelos:

rails generate model Pessoa nome:string
rails generate model Livro titulo:string
rails generate model Leitura pessoa_id:integer livro_id:integer

O Rails criará três Migrations para gerar as tabelas no banco de dados:

class CreatePessoas < ActiveRecord::Migration
  def change
    create_table :pessoas do |t|
      t.string :nome

      t.timestamps
    end
  end
end

class CreateLivros < ActiveRecord::Migration
  def change
    create_table :livros do |t|
      t.string :titulo

      t.timestamps
    end
  end
end

class CreateLeituras < ActiveRecord::Migration
  def change
    create_table :leituras do |t|
      t.integer :pessoa_id
      t.integer :livro_id

      t.timestamps
    end
  end
end

E gerará os três modelos, isto é, três classes que herdam de ActiveRecord::Base, nas quais teremos somente o trabalho de especificar os relacionamentos, conforme abaixo:

class Pessoa < ActiveRecord::Base
  attr_accessible :nome

  has_many :leituras
  has_many :livros, through: :leituras
end

class Livro < ActiveRecord::Base
  attr_accessible :titulo

  has_many :leituras
  has_many :leitores, through: :leituras, source: :pessoa
end

class Leitura < ActiveRecord::Base
  attr_accessible :livro_id, :pessoa_id

  belongs_to :pessoa
  belongs_to :livro
end

Similarmente ao que foi demonstrado no exemplo do auto-relacionamento, na nossa classe Livro o atributo 'source' é necessário porque, por convenção, o Rails buscaria um atributo "leitor" na classe associativa.

Feito isso, está pronto o relacionamento many-to-many.


Auto-relacionamento Many-to-Many


Finalmente, chegamos no auto-relacionamento many-to-many, onde há uma combinação entre os dois tipos de relacionamento exemplificados até aqui e, necessariamente, a adição de alguns tratamentos especiais para manter a consistência da base de dados.
Vou pular o diagrama dessa vez, porque acho que não é tão necessário (e é bem chato de fazer também).

Um bom exemplo de auto-relacionamento many-to-many é o das redes sociais. Vamos imaginar uma estrutura de dados em que pessoas estão conectadas a outras pessoas. Teremos uma tabela de pessoas e um tabela associativa que guardará o relacionamento entre as pessoas.
Geramos os modelos através do Rails:

rails generate model Pessoa nome:string
rails generate model Amizade pessoa_id:integer amigo_id:integer

Estes comandos geram as Migrations e os modelos, nos quais incluiremos os relacionamentos, conforme abaixo:

class CreatePessoas < ActiveRecord::Migration
  def change
    create_table :pessoas do |t|
      t.string :nome

      t.timestamps
    end
  end
end

class CreateAmizades < ActiveRecord::Migration
  def change
    create_table :amizades do |t|
      t.integer :pessoa_id
      t.integer :amigo_id

      t.timestamps
    end
  end
end

class Pessoa < ActiveRecord::Base
  attr_accessible :nome

  has_many :amizades
  has_many :amigos, through: :amizades, source: :amigo
end

class Amizade < ActiveRecord::Base
  attr_accessible :pessoa_id, :amigo_id

  belongs_to :pessoa
  belongs_to :amigo, class_name: "Pessoa"
end

Os relacionamentos estão bem definidos, funcionando, porém ainda temos um problema: As amizades são unilaterais. Quem eu tenho como amigo não necessariamente me tem como amigo também, isso forma uma falsa amizade que não se sustentará por muito tempo, mas aí já foge um pouco ao aspecto técnico deste post...
O que precisamos é garantir que a amizade sempre seja bilateral e, caso seja rompida por um dos lados, o rompimento também sem bilateral. Para realizar isso, podemos fazer uso dos callbacks oferecidos pelo Rails, que simplificam em muito nossa vida.
O modelo de Amizade com callbacks que garantam essa consistência nos dados fica conforme abaixo:

class Amizade < ActiveRecord::Base
  attr_accessible :pessoa_id, :amigo_id

  belongs_to :pessoa
  belongs_to :amigo, class_name: "Pessoa"

  after_create :criar_amizade_bilateral
  after_destroy :terminar_amizade_bilateral

  def criar_amizade_bilateral
    amigo.amigos << pessoa unless amigo.amigos.include?(pessoa)
  end

  def terminar_amizade_bilateral
    amigo.amigos.delete(pessoa)
  end
end

O método de class after_create define o nome do método que será sempre chamado após a criação de uma nova instância de Amizade. E o método after_destroy já dá pra imaginar o que faz.
Nas linhas seguintes, temos a implementação dos dois métodos. Na linha 11 a pessoa que está "firmando a amizade" é adicionada também aos amigos do amigo, exceto se já estiver dentre estes. E na linha 15, a pessoa que está desfazendo a amizade também é removida da lista de amigos do ex-amigo.

Pronto!! Muito simples!

Passos omitidos


Em prol da objetividade omiti alguns passos:
  • Para construir os exemplos foram utilizadas as versões 3.2.3 do Rails e 1.9.3 do Ruby
  • Os comandos geradores do Rails devem ser executados na raiz do seu projeto.
  • Após a execução, também a partir da raiz será possível encontrar as Migrations dentro de db/migrate e os modelos em app/model
  • Os geradores somente geram as Migrations e os modelos, não gerando o banco de dados. Executando o comando rake db:migrate o banco de dados será criado a partir das Migrations.
  • Tendo criado o banco de dados e adicionado os relacionamentos nos modelos, é possível testar através do console nativo do rails acessível através do comando rails console
  • Nos modelos criados pelo Rails, já é incluída uma linha com o método attr_accessible que envolve questões de segurança que serão abordadas em um futuro post.
  • Há também muito mais para se falar sobre os callbacks do Rails, mas isso também vai ficar para um futuro post.

 

Referências


A referência principal foi o livro Rails Recipes, de Chad Fowler, que possui muitas receitas prontas e bem explicadas sobre situações comuns no desenvolvimento de aplicações com Rails. Pretendo utilizar várias outras receitas contidas neste livro para futuros posts.

Então, até a próxima!

Nenhum comentário:

Postar um comentário