É comum sistemas que utilizam orientação a objetos conterem classes que representam coisas reais do domínio de negócio. Um sistema para uma escola talvez precise de classes como Aluno, Professor, Curso, Turma, etc. Essas classes de entidades geralmente possuem relacionamentos. Por exemplo uma Aluno deve estar matriculado em pelo menos um curso, ou um Professor lecione em várias Turmas e assim por diante. Esses relacionamentos entre as classes precisam ser expressos em código, isso significa que as classes devem ser informadas de como elas devem interagir umas com as outras. O framework Rails oferece recursos para que isso seja feito de modo bastante produtivo. O tópico seguinte apresenta uma introdução aos principais tipos de relacionamentos que podem haver entre as classes do modelo de uma aplicação.

Tipos de relacionamentos

Existem diversas formas de dois objetos se associarem entre si, os relacionamentos mais comuns entre classes são:

  • Um para um. Onde um objeto pode estar associação a apenas outro objeto de um determinado tipo. Por exemplo: Um aluno frequenta um curso.
  • Um para muitos. Neste caso um objeto poderá se associar com um ou vários objetos de outra classe. Por exemplo: Uma turma é formada por vários alunos.
  • Muitos para muitos. Um objeto pode participar de vários relacionamentos com outros objetos de determinado tipo. Por exemplo: Um professor ensina em várias turmar, e cada turma tem muitos professores.

Para aplicar esses relacionamentos em uma aplicação Rails temos disponível as seguintes estruturas: belongs_to (pertence a), has_one (tem um), has_many (tem muitos), has_and_belongs_to_many (tem e pertence a muitos), entre outras. A seguir analisaremos cada um destes métodos explorando em que casos eles podem ser usados bem como os recursos que trazem para a aplicação e os efeitos ou modificações que estes causam no banco de dados.

Belongs To

Esse tipo de associação é comumente chamada de One to One (um para um) e é usada para indicar que determinado modelo “pertence a” outro. Na Listagem 1 temos o relacionamento de um modelo chamado Artigo com outo de nome Autor, esse bloco de código indica que cada artigo pertencerá a um autor.

Listagem 1. Exemplo de uso da associação belongs_to.

class Artigo < ActiveRecord::Base
    belongs_to :autor
  end
   
  class Autor < ActiveRecord::Base
  end

Para que o exemplo funcione corretamente é preciso que a tabela que armazena os artigos no banco de dados tenham uma referência ao autor de cada artigo. Então o arquivo de migração que cria a tabela relacionada aos artigos deve conter, além dos atributos do artigo, uma coluna para guardar o identificador do autor. A Listagem 2 mostra o arquivo de criação das tabelas para os artigos e autores.

Listagem 2. Exemplo de arquivos de migração para as classes Artigo e Autor.

class CreateArtigos < ActiveRecord::Migration
    def change
      create_table :artigos do |t|
        t.string :titulo
        t.text :texto
        t.date :data_publicacao
        t.integer :autor_id
   
        t.timestamps null: false
      end
    end
  end
   
  class CreateAutors < ActiveRecord::Migration
    def change
      create_table :autors do |t|
        t.string :nome
        t.text :curriculo
   
        t.timestamps null: false
      end
    end
  end

Note que há uma coluna do tipo integer para armazenar o identificador do autor de um artigo. Essa linha ainda poderia ser escrita de outra forma, usando a opção references, como mostra a Listagem 3.

Os dois códigos têm efeitos semelhantes: ambos criam na tabela artigos uma coluna com o nome autor_id para ser chave estrangeira da tabela autores.

Listagem 3. Exemplo de arquivo de migração utilizando a opção references.

class CreateArtigos < ActiveRecord::Migration
    def change
      create_table :artigos do |t|
        t.string :titulo
        t.text :texto
        t.date :data_publicacao
        t.references :autor
   
        t.timestamps null: false
      end
    end
  end

A associação belongs_to adiciona alguns métodos para facilitar a manipulação dos objetos do relacionamento. Veja a descrição do funcionamento e exemplos de uso de dois desses métodos: o autor(force_reload = false) e autor = (autor).

Método de configuração artigo.autor = (autor)

Esse método configura ou “seta” um determinado objeto para a associação. Ele faz mais do que apenas armazenar um objeto em uma variável, além disso esse método associa a chave primária do objeto que está sendo passado por parâmetro a chave estrangeira do objeto que tem o relacionamento belongs_to. No caso da Listagem 4 perceba que autor1 foi configurado para fazer parte da associação com artigo1, isso significa que o identificador de autor1 será armazenado na coluna que é chave estrangeira na tabela de artigos.

Listagem 4. Execução do método artigo.autor = (autor)


  ~/RailsProjects/tests_associations$ rails console
  irb(main):001:0> autor1 = Autor.new(nome: 'José Camilo Filho')
  irb(main):002:0> artigo1 = Artigo.new(nome: 'Criação de uma aplicação com Rails')
  irb(main):003:0> artigo1.autor=(autor1)
  irb(main):005:0> puts artigo1.autor.nome
  => José Camilo Filho

Método de recuperação artigo.autor(force_reload = false)

Esse método recupera o objeto associado. No caso do exemplo de artigos e autores ele retornaria o autor que está associado ao artigo em questão. A Listagem 5 mostra o exemplo de seu uso a partir do console do framework, que simula a aplicação sem a interface gráfica sendo executada no terminal. Para iniciar essa ferramenta basta digitar o comando rails console no terminal estando dentro do diretório da aplicação.

Listagem 5. Exemplo de execução do método autor(force_reload = false) a partir do rails console.

~/RailsProjects/tests_associations$ rails console
  irb(main):001:0>autor1=Autor.new(nome: 'João da Silva Souza')
  irb(main):002:0> autor1.save
  irb(main):003:0>artigo1=Artigo.new(titulo: 'Introdução ao Rails', data_publicacao: '01/12/2015')
  irb(main):004:0> artigo1.save
  irb(main):005:0> puts artigo1.autor.nome
  =>João da Silva Souza

Note que por padrão o parâmetro force_reload desse método é configurado com o valor false isso indica que não haverá uma consulta ao banco de dados para recuperar o objeto da associação, o objeto será recuperado do cache que o ActiveRecord faz. Caso esse parâmetro seja alterado para true uma nova consulta será feita ao banco como mostra a Listagem 6.

Listagem 6. Exemplo de execução do método autor(force_reload = true) a partir do rails console, fazendo uma nova consulta no banco de dados.

irb(main):006:0> puts artigo1.autor(true).nome
    Autor Load (0.3ms)  SELECT  "autores".* FROM "autores" WHERE "autores"."id" = ? LIMIT 1  [["id", 1]]
  =>João da Silva Souza

Has One

Essa associação é bem semelhante a belongs_to chegando a causar algumas confusões de entendimento entre as duas. O método has_one também é, assim como belongs_to, uma associação do tipo one to one, porém ela deve ser usada em situações diferentes e causa um efeito diferenciado.

Has One é também conhecido como a associação bidirecional a belongs_to. Ela não adiciona chave estrangeira ao modelo que a declara, essa é a diferença mais significativa dela para belongs_to. A Listagem 7 mostra um exemplo de aplicação desse relacionamento.

Listagem 7. Exemplo de uso de has_one

class Usuario < ActiveRecord::Base    has_one :conta
  end
   
  class Conta < ActiveRecord::Base
    belongs_to :usuario
  end

Note que no exemplo apresentado na Listagem 7 o has_one foi usado juntamente com belongs_to como um complemento. Isso torna possível que a classe Usuário tenha métodos de acesso ao objeto Conta ao qual está relacionado. Mas como já dito a chave estrangeira ficará na classe que contém a associação belongs_to, como mostra os arquivos de migrações apresentados na Listagem 8. Perceba que apenas a migração referente a classe Conta possui uma coluna do tipo references que se tornará uma chave estrangeira.

Listagem 8. Arquivos de migrações para as classes Usuário e Conta.

class CreateContas < ActiveRecord::Migration
    def change
      create_table :contas do |t|
        t.string :login
        t.string :senha
        t.references :usuario
   
        t.timestamps null: false
      end
    end
  end
   
  class CreateUsuarios < ActiveRecord::Migration
    def change
      create_table :usuarios do |t|
        t.string :nome
   
        t.timestamps null: false
      end
    end
  end

A associação has_one também adiciona métodos para manipular o objeto relacionado. Os métodos tem funcionamento semelhante aos recebidos pela associação belongs_to. Veja na Listagem 9 o exemplo de uso deles.

Listagem 9. Exemplo de uso dos métodos adicionados por has_one.

~/RailsProjects/tests_associations$ rails console
  irb(main):001:0> usuario1 = Usuario.new(nome: 'João da Silva')
  irb(main):002:0> conta1 = Conta.new(login: 'joao', senha: '12345')
  irb(main):003:0> usuario1.conta=conta1
  irb(main):004:0> puts "Login: #{usuario1.conta.login} - Senha: #{usuario1.conta.senha}"
  =>Login: joao - Senha: 12345

A linha 3 do exemplo acima mostra o uso do método de atribuição “usuario.conta=”, já a linha 4 usa o método que recupera o objeto da associação (usuario.conta) para imprimir no terminal o login e senha do usuário.

Has Many

Essa associação é usada para indicar que um modelo tem nenhum ou muitos elementos de outro modelo da aplicação, chamada frequentemente de one to many (um para muitos). Assim como a associação has_one, has_many não adiciona em que a usa nenhuma coluna, na verdade ela é usada em conjunto com belong_to que indica por meio de uma chave estrangeira com quem o objeto filho se relaciona. Um exemplo da aplicação dessa estrutura é apresentado na Listagem 10. Perceba que ao passo que uma equipe tem muitos (has_many) integrantes, um integrante pertence a (belongs_to) uma equipe.

Listagem 10. Uso do relacionamento has_many.

class Equipe < ActiveRecord::Base
    has_many :integrantes
  end
   
  class Integrante < ActiveRecord::Base
    belongs_to :equipe
  end

As migrações para os modelos apresentados na Listagem 10 poderiam ser semelhantes ao código mostrado na Listagem 11.

Listagem 11. Exemplo de migrações para as classes Equipe e Integrante.

class CreateEquipes < ActiveRecord::Migration
    def change
      create_table :equipes do |t|
        t.string :nome
   
        t.timestamps null: false
      end
    end
  end
   
  class CreateIntegrantes < ActiveRecord::Migration
    def change
      create_table :integrantes do |t|
        t.string :nome
        t.references :equipe
   
        t.timestamps null: false
      end
    end
  end

Essa associação também adiciona um conjunto bastante diverso de métodos para facilitar o uso dos objetos que fazem parte do relacionamento. No caso do relacionamento das classes Equipe e Integrante alguns métodos que a classe Equipe receberia são: equipe.integrantes=, equipe.integrantes<<, equipe.integrantes(force_reload = false), equipe.integrantes.destroy, equipe.integrantes_ids entre outros. Veja abaixo alguns detalhes sobre esses métodos.

Métodos de configuração equipe.integrantes= e equipe.integrantes <<

Esses métodos tem como objetivo adicionar valores na variável da associação. O método “=” aceita uma lista de objetos já “<<” aceita tanto um único valor, como uma lista contendo vários objetos. Seguindo ainda com o exemplo de equipe e integrantes veja a Listagem 12 que usa esses métodos para inicializar a lista de integrantes com valores.

Listagem 12. Adicionando valores a lista de integrantes.

~/RailsProjects/tests_associations$ rails console
  irb(main):001:0> integrante1 = Integrante.new(nome: 'josé')
  irb(main):002:0> integrante2 = Integrante.new(nome: 'carlos')
  irb(main):003:0> integrante3 = Integrante.new(nome: 'silva')
  irb(main):004:0> integrante4 = Integrante.new(nome: 'souza')
  irb(main):005:0> equipe1 = Equipe.new(nome: 'equipe de testes')
  irb(main):006:0> equipe1.integrantes.empty?
  => true
  irb(main):007:0> equipe1.integrantes << integrante1
  irb(main):008:0> equipe1.integrantes = [integrante2, integrante3, integrante4]
  irb(main):009:0> equipe1.integrantes.size
  => 3

Método de recuperação equipe.integrantes(force_reload = false)

Esse método retorna uma lista contendo todos os objetos associados. No caso do exemplo citado acima do relacionamento entre equipe e integrantes, esse método retornaria uma lista de objetos do tipo Integrante que estão associados ao objeto Equipe no qual tal método foi chamado. Veja na Listagem 13 um exemplo em que vários integrantes são adicionados a uma determinada equipe e depois disso o método equipe.integrantes é usado para recuperar a lista de integrantes associados.

Listagem 13. Exemplo de uso do método equipe.integrantes.

~/RailsProjects/tests_associations$ rails console
  irb(main):001:0> integrante1 = Integrante.new(nome: 'josé')
  irb(main):002:0> integrante2 = Integrante.new(nome: 'carlos')
  irb(main):003:0> integrante3 = Integrante.new(nome: 'silva')
  irb(main):004:0> equipe1 = Equipe.new(nome: 'equipe de testes')
  irb(main):005:0> equipe1.integrantes.empty?
  => true
  irb(main):006:0>equipe1.integrantes<<[integrante1, integrante2, integrante3]
  irb(main):007:0>equipe1.integrantes.size
  =>3
  irb(main):008:0>equipe1.integrantes.each do |i|
  irb(main):009:1* puts i.nome
  irb(main):010:1>end
  =>josé
  =>carlos
  =>silva

Perceba que esse método recebe por padrão o valor false para o atributo force_reload. Caso seja necessário que a busca venha direto do banco de dados ao invés dos dados já carregados o método deve ser chamado passando o valor true como argumento, conforme Listagem 14.

Listagem 14. Listando os integrantes diretamente do banco de dados.

irb(main):001:0>equipe1.integrantes(true).each do |i|
  irb(main):002:1* puts i.nome
  irb(main):003:1>end
  Integrante Load (0.5ms) SELECT "integrantes".* FROM "integrantes" WHERE "integrantes"."equipe_id" = ?  [["equipe_id", 1]]
  =>josé
  =>carlos
  =>silva

Método de remoção equipe1.integrantes.destroy

Esse método remove do banco de dados o objeto passado como parâmetro. Caso o método receba uma lista, todos os elementos contidos nela serão excluídos. A Listagem 15 mostra o seu funcionamento.

Listagem 15. Removendo objetos da associação com destroy.

~/RailsProjects/tests_associations$ rails console
  irb(main):001:0> integrante1 = Integrante.new(nome: 'josé')
  irb(main):002:0> integrante2 = Integrante.new(nome: 'carlos')
  irb(main):003:0> integrante3 = Integrante.new(nome: 'silva')
  irb(main):004:0> equipe1 = Equipe.new(nome: 'equipe de testes')
  irb(main):005:0> equipe1.integrantes<<[integrante1, integrante2, integrante3]
  irb(main):006:0> equipe1.integrantes.empty?
  => 3
  irb(main):007:0> equipe1.save
  irb(main):008:0> equipe1.integrantes.destroy(integrante1)
  SQL (0.5ms)  DELETE FROM "integrantes" WHERE "integrantes"."id" = ?  [["id", 1]]
  irb(main):009:0> equipe1.integrantes.destroy([integrante2, integrante3])
  SQL (0.4ms)  DELETE FROM "integrantes" WHERE "integrantes"."id" = ?  [["id", 2]]
  SQL (0.1ms)  DELETE FROM "integrantes" WHERE "integrantes"."id" = ?  [["id", 3]]

Método de listagem de identificadores equipe.integrante_ids

O método equipe.integrante_ids retorna um array contendo todos os identificadores dos objetos na lista. A Listagem 16 mostra isso.


irb(main):001:0> integrante1 = Integrante.new(nome: 'josé')
  irb(main):002:0> integrante2 = Integrante.new(nome: 'carlos')
  irb(main):003:0> integrante3 = Integrante.new(nome: 'silva')
  irb(main):004:0> equipe1.integrantes =[integrante1, integrante2, integrante3]
  irb(main):005:0> equipe1.integrante_ids
  => [1, 2, 3]

Has And Belongs To Many

Essa opção cria uma conexão do tipo many to many (muitos para muitos) entre dois modelos da aplicação. Por exemplo, aplicações que armazenam professores e as turmas que eles lecionam, geralmente permitem que um professor ensine a mais de uma turma e que uma turma tenha mais de um professor, esse relacionamento pode ser traduzido pela associação muitos para muitos. A Listagem 17 mostra como ficariam duas classes de modelo com esse relacionamento. Note que a associação deve ser usada nas duas entidades que participam do relacionamento.

Listagem 17. Exemplo de associação has_and_belongs_to_many.

class Professor < ActiveRecord::Base
    has_and_belongs_to_many :turmas
  end
   
  class Turma < ActiveRecord::Base
    has_and_belongs_ to_many :professores
  end

No banco de dados a associação muitos para muitos é refletida de uma maneira diferente das demais. Não há modificações nas tabelas que fazem parte do relacionamento, na verdade uma nova tabela é criada para armazenar o identificador de cada modelo participante do relacionamento. Essa tabela é chamada de join table ou tabela de junção, o seu nome é a concatenação do nome dos dois modelos respeitando-se a ordem alfabética. A Listagem 18 mostra os arquivos de migração para criação das tabelas para professores, turmas e para a join table professores_turmas. Veja que não há atributos de referencias nas tabelas professores e turmas, mas a tabela professores_turmas aponta para as duas tabelas do relacionamento.

Listagem 18. Arquivos de migração para as entidades Professor, Turma e para a tabela de junção.

class CreateProfessores < ActiveRecord::Migration
    def change
      create_table :professores do |t|
        t.string :nome
        t.timestamps null: false
      end
    end
  end
   
  class CreateTurmas < ActiveRecord::Migration
    def change
      create_table :turmas do |t|
        t.string :nome
        t.timestamps null: false
      end
    end
  end
   
  class CreateJoinTableProfessorTurma < ActiveRecord::Migration
    def change
      create_join_table :professores, :turmas
    end
  end

Assim como os demais relacionamentos has_and_belongs_to_many também adiciona métodos as classes de modelo que o utilizam. Alguns dos métodos mais utilizados são: <<, clear entre outros. Veja abaixo exemplos de utilização destes.

Método de configuração professores<< ou turmas<<

Assim como em outros tipos de relacionamentos o método << adiciona objetos nas variáveis que representam as associações. A Listagem 19 mostra um exemplo de adição de turmas para um determinado professor.

~/RailsProjects/tests_associations$ rails console
  irb(main):001:0> professor1 = Professor.new(nome: 'João da Silva')
  irb(main):002:0> turma1 = Turma.new(nome: '1º A')
  irb(main):003:0> turma2 = Turma.new(nome: '1º B')
  irb(main):004:0> turma3 = Turma.new(nome: '1º C')
  irb(main):005:0> professor1.turmas.size
  => 0
  irb(main):006:0> professor1.turmas<<[turma1, turma2, turma3]
  irb(main):007:0> professor1.turmas.size
  => 3

Método Clear

Esse método exclui todos os objetos da coleção na qual foi chamado. Ele remove todas as linhas de relacionamento da tabela de junção mas sem excluir os objetos das duas respectivas tabelas. Veja na Listagem 20 que depois da chamada ao método clear nas turmas (linha 7) elas são removidas do relacionamento, mas não são excluídas da tabela turmas (linha 8).

~/RailsProjects/tests_associations$ rails console
  irb(main):001:0> professor1 = Professor.new(nome: 'João da Silva')
  irb(main):002:0> turma1 = Turma.new(nome: '1º A')
  irb(main):003:0> turma2 = Turma.new(nome: '1º B')
  irb(main):004:0> turma3 = Turma.new(nome: '1º C')
  irb(main):005:0> professor1.turmas<<[turma1, turma2, turma3]
  irb(main):006:0> professor1.turmas.size
  => 3
  irb(main):007:0> professor1.turmas.clear
  irb(main):008:0> professor1.turmas.size
  => 0
  irb(main):009:0> Turma.all.size
  => 3

Referências

Rails Guides
http://guides.rubyonrails.org/association_basics.html

API Ruby on Rails
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html