Esta semana recebi dois e-mails com a mesma dúvida: como funciona o relacionamento muitos-para-muitos no ActiveRecord. Aparentemente, este é um assunto que muitas pessoas têm dúvidas, mas cuja resposta é bastante simples, até. Você pode fazer tal relacionamento de duas maneiras diferentes, como veremos abaixo.

Utilizando o método has_and_belongs_to_many

O método has_and_belongs_to_many possui uma convenção para se nomear a tabela que persistirá os relacionamentos. Você deve colocar ambas as tabelas relacionadas no nome da tabela de relacionamentos, ordenados alfabeticamente e separados por underscore. Se você tem as tabelas "posts" e "categories", o nome de sua tabela de relacionamentos será "categories_posts". Os campos também possuem uma convenção, que é colocar o nome da tabela no singular, adicionando o sufixo "_id". Neste caso, teríamos um arquivo de migração como este:

class AddPostsAndCategories < ActiveRecord::Migration
  def self.up
    create_table :categories_posts do |t|
      t.references :category, :post
    end
  end
 
  def self.down
    drop_table :categories_posts
  end
end

Para relacionar ambos os modelos, precisaríamos colocar o seguinte método no modelo Post:

class Post < ActiveRecord::Base
  has_and_belongs_to_many :categories
end

Já no modelo Category, o relacionamento seria o seguinte:

class Category < ActiveRecord::Base
  has_and_belongs_to_many :posts
end

Muito simples! Apenas com estas poucas linhas você já pode usar diversos métodos, adicionados pelo próprio ActiveRecord.

post = Post.find(:first)
category = Category.find(:category)
 
# creating relationship through post
post.categories << category
 
# creating relationship through category
category.posts << post
 
# getting all posts
categories.posts.each do |post|
	puts post.title
end
 
# getting all categories from a post
post.categories.each do |cat|
	puts cat.title
end
 
# getting all the post ids
category.post_ids

O método has_and_belongs_to_many possui uma grande desvantagem. Você não pode adicionar campos extras aos relacionamentos. No entanto, o método has_many permite que você faça isso.

Utilizando o método has_many

Ao contrário do método has_and_belongs_to_many, você pode ter quantos campos adicionais precisar se fizer o relacionamento com o método has_many. Isso é extremamente útil e bastante utilizado por plugins que lidam com ActiveRecord. A convenção para este tipo de relacionamento é um pouco diferente. Sua tabela pode ter um nome específico que identifica o tipo de relacionamento que está sendo feito, já que ele terá um modelo próprio, que fará o papel intermediário entre as tabelas relacionadas.

Crie um modelo chamado Categorization. Ele deve ter os seguintes campos:

class CreateCategorizations < ActiveRecord::Migration
  def self.up
    create_table :categorizations do |t|
      t.references :post, :category
      t.timestamps
    end
  end
 
  def self.down
    drop_table :categorizations
  end
end
 

Agora, você precisa criar relacionamentos entre este modelo e os demais (Post e Category). No seu modelo Categorization, adicione os seguintes relacionamentos:

class Categorization < ActiveRecord::Base
  belongs_to :category
  belongs_to :post
end

E em cada um dos outros modelos faça o relacionamento contrário, utilizando o método has_many.

class Post < ActiveRecord::Base
  has_many :categorizations
end
class Category < ActiveRecord::Base
  has_many :categorizations
end

E aqui entra realmente a parte importante: você irá usar um relacionamento comumente chamado de "has_many :through", que fará uma consulta SQL, unindo os resultados em uma única consulta. No seu modelo Post, adicione mais um relacionamento.

class Post < ActiveRecord::Base
  has_many :categorizations
  has_many :categories,
    :through => :categorizations
end

Faça a mesma coisa no modelo Category.

class Category < ActiveRecord::Base
  has_many :categorizations
  has_many :posts,
    :through => :categorizations
end

Pronto! Seu relacionamento já foi criado. Para testar, faça algo como isto usando o console.

post = Post.find(:first)
# get post categories
post.categories.collect(&:title).to_sentence
 
category = Category.find(:first)
# get posts by categories
category.posts.collect(&:title).to_sentence

Para criar uma nova associação, você precisa fazer isso através do relacionamento categorizations; caso contrário, você receberá um erro dizendo que não possui um campo para o id.

# through post
post.categorizations.create(:category => category)
 
# through category
category.categorizations.create(:post => post)

É isso! Se você tem alguma dúvida, envie um e-mail. Se for um assunto que me interessa, ou que interessa a mais pessoas, eu posso escrever algo aqui!

Comentários #

#1 Lauro disse:
17 Abr 08, 03:15AM

Muito bom, simples e direto ao ponto. Routes é uma boa pedida pra um tutorial

#2 WEBtudinho » Artigo » Quem programa em Rails só sabe... disse:
17 Abr 08, 04:44AM

[...] Eu vi uma indicação do Nando Vieira para um post que ele deve ter escrito hoje, sobre relacionamento Has And Belongs To Many, o famoso HABTM. Fui ler, numa boa, uma dica excelente por sinal. No entanto, enquanto eu lia acendeu uma luzinha [...]

#3 Vinícius Ebersol disse:
17 Abr 08, 04:48AM

A dica é ótima! Este tipo de relacionamento realmente deixa quem está iniciando com Rails meio confuso, e esta dica com certeza vai permitir que os "railers" iniciantes tenham maior facilidade para compreender este relacionamento que é uma maravilha!

#4 Aguinelo Pedroso » Melhores da semana #001 - Frameworks, pr... disse:
18 Abr 08, 03:44PM

[...] Relacionamento muitos-para-muitos com ActiveRecord no Rails, Entenda como funciona este tipo de relacionamento e como aplicar algumas t

#5 Bruno Siqueira disse:
23 Abr 08, 07:01PM

Estou começando a desenvolver em rails agora e este artigo tirou uma grande dúvida minha. Muito bem escrito.
Parabéns

#6 Tiago Albineli Motta disse:
14 Maio 08, 12:09PM

Muito bom o artigo. Bem sucinto!

#7 Luiz Arão Carvalho disse:
16 Maio 08, 11:40AM

Aew Nando.
Otimo Tutorial.
sou iniciante no Rails
e estava com um problemão exatamente nessa parte
de relacionamentos NxN usando Through.
Brigadaum cara.
e sucessos!

#8 Niler Barcelos disse:
24 Jan 09, 10:37AM

E ai nando blza??

Muito bom este post, to aprendendo rails e estou com dificuldades de entender relacionamentos, mas depois deste post as coisas começaram a ficar mais claras.

Muito bom! Abraço!!

#9 Leonardo disse:
15 Mar 09, 07:23PM

Fala Nando!
Parabéns pelo artigo.
Mutcho bueno ;)

Abraços!

#10 Kleber disse:
12 Set 09, 04:27PM

Opa Nando!

Uma dúvida, e em caso de haver duas tabelas com o mesmo sufixo?

Por ex: forum_categories e outra chamada forums

Como fica nesse caso? tanto faz?

Abraço!

#11 Igor disse:
15 Set 09, 02:20PM

Nando, muito esclarecedor esse post! Só fazendo uma adendo, a tabela gerada para associar os 2 models usando HABTM não pode ter PK. Assim, o método self.up da migration deve estar dessa forma:

http://pastie.org/617656

Abraço!

#12 Rafael disse:
11 Jan 10, 06:15PM

Muito bom! Me ajudou aqui!

#13 Eduardo Hertz disse:
28 Jan 10, 08:55PM

Olá, Nando.

Parabéns pelo seu post. É de muita ajuda. Está nos meus favoritos com certeza.
Me surgiu uma dúvida para um problema que eu tenho.

Vamos supor que eu tenho o modelo Friends e quero fazer uma relação N:N do modelo Friends com ele mesmo.

Friends N:N Friends

Tem ideia de como ficaria??

Aguardo uma resposta. Obrigado.

Abraços,

#14 Marcelino Santana Truocchio disse:
06 Dez 10, 09:30PM

Olá Nando,

Primeiro gostaria de elogiar pelo post, mto claro, como poucos que se encontra.
A minha duvida no caso é exatamente a mesma do Eduardo. Se puder me ajudar a clarear um pouco a mente.

De já agradeço,

Abraços

#15 Nando Vieira disse:
07 Dez 10, 09:09AM

Eduardo e Marcelino: Pegando o caso de User. A melhor coisa é ter uma segunda tabela que irá lidar apenas com a associação dos registros. Neste caso, o modelo pode se chamar Friendship. Você terá Friendship#user_id, que irá referenciar quem começou a amizade, e Friendship#friend_id, que é a pessoa adicionada como amigo.

Para retornar os amigos, você pode usar o has_many :through.

É interessante dar uma olhada no plugin http://github.com/fnando/has_friends, que lida exatamente com esta situação.

Deixe um comentário





Não é aceito código HTML: adicione-o no pastie.org ou paste.milk-it.net e poste apenas o link.

Se este é seu primeiro comentário, ele terá que ser aprovado antes de ser exibido.

jQuery: Dominando o framework

Você quer aprender a usar jQuery de verdade? Então chegou a hora! Neste workshop você verá como funciona este framework de JavaScript, entendendo todos os aspectos que fazem do jQuery uma das melhores ferramentas para desenvolvimento de interfaces.

Saiba mais Fechar

Conheça também o HOWTO