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 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